Clean up redundant hospital filtering in views

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
This commit is contained in:
ismail 2026-03-11 00:49:42 +03:00
parent c16e410fdd
commit b57ce661bb
10 changed files with 2189 additions and 2495 deletions

View File

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

View File

@ -140,10 +140,6 @@ def appreciation_list(request):
page_number = request.GET.get('page', 1) page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
departments = Department.objects.filter(status='active') departments = Department.objects.filter(status='active')
if not user.is_px_admin() and user.hospital: if not user.is_px_admin() and user.hospital:
@ -172,7 +168,6 @@ def appreciation_list(request):
'page_obj': page_obj, 'page_obj': page_obj,
'appreciations': page_obj.object_list, 'appreciations': page_obj.object_list,
'stats': stats, 'stats': stats,
'hospitals': hospitals,
'departments': departments, 'departments': departments,
'categories': categories, 'categories': categories,
'status_choices': AppreciationStatus.choices, 'status_choices': AppreciationStatus.choices,
@ -334,7 +329,6 @@ def appreciation_send(request):
categories = categories.filter(Q(hospital_id=request.user.hospital.id) | Q(hospital__isnull=True)) categories = categories.filter(Q(hospital_id=request.user.hospital.id) | Q(hospital__isnull=True))
context = { context = {
'hospitals': hospitals,
'categories': categories, 'categories': categories,
'visibility_choices': AppreciationVisibility.choices, 'visibility_choices': AppreciationVisibility.choices,
} }
@ -410,10 +404,6 @@ def leaderboard_view(request):
page_number = request.GET.get('page', 1) page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
departments = Department.objects.filter(status='active') departments = Department.objects.filter(status='active')
if not user.is_px_admin() and user.hospital: if not user.is_px_admin() and user.hospital:
@ -426,7 +416,6 @@ def leaderboard_view(request):
context = { context = {
'page_obj': page_obj, 'page_obj': page_obj,
'leaderboard': page_obj.object_list, 'leaderboard': page_obj.object_list,
'hospitals': hospitals,
'departments': departments, 'departments': departments,
'months': months, 'months': months,
'years': years, 'years': years,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1083,7 +1083,6 @@ def admin_evaluation(request):
).distinct().select_related('hospital', 'department') ).distinct().select_related('hospital', 'department')
context = { context = {
'hospitals': hospitals,
'departments': departments, 'departments': departments,
'staff_list': staff_queryset, 'staff_list': staff_queryset,
'selected_hospital_id': hospital_id, 'selected_hospital_id': hospital_id,

View File

@ -148,10 +148,6 @@ def feedback_list(request):
page_number = request.GET.get('page', 1) page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
departments = Department.objects.filter(status='active') departments = Department.objects.filter(status='active')
if not user.is_px_admin() and user.hospital: if not user.is_px_admin() and user.hospital:
@ -179,7 +175,6 @@ def feedback_list(request):
'page_obj': page_obj, 'page_obj': page_obj,
'feedbacks': page_obj.object_list, 'feedbacks': page_obj.object_list,
'stats': stats, 'stats': stats,
'hospitals': hospitals,
'departments': departments, 'departments': departments,
'assignable_users': assignable_users, 'assignable_users': assignable_users,
'status_choices': FeedbackStatus.choices, 'status_choices': FeedbackStatus.choices,

View File

@ -105,10 +105,6 @@ def journey_instance_list(request):
page_number = request.GET.get('page', 1) page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
departments = Department.objects.filter(status='active') departments = Department.objects.filter(status='active')
if not user.is_px_admin() and user.hospital: if not user.is_px_admin() and user.hospital:
@ -125,7 +121,6 @@ def journey_instance_list(request):
'page_obj': page_obj, 'page_obj': page_obj,
'journeys': page_obj.object_list, 'journeys': page_obj.object_list,
'stats': stats, 'stats': stats,
'hospitals': hospitals,
'departments': departments, 'departments': departments,
'filters': request.GET, 'filters': request.GET,
} }
@ -221,15 +216,10 @@ def journey_template_list(request):
page_number = request.GET.get('page', 1) page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
context = { context = {
'page_obj': page_obj, 'page_obj': page_obj,
'templates': page_obj.object_list, 'templates': page_obj.object_list,
'hospitals': hospitals,
'filters': request.GET, 'filters': request.GET,
} }

View File

@ -1,6 +1,7 @@
""" """
Physicians Console UI views - Server-rendered templates for physician management Physicians Console UI views - Server-rendered templates for physician management
""" """
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Avg, Count, Q, Sum from django.db.models import Avg, Count, Q, Sum
@ -26,9 +27,11 @@ def physician_list(request):
""" """
# Base queryset with optimizations - only show staff marked as physicians # Base queryset with optimizations - only show staff marked as physicians
# Include both: staff with physician=True (from rating imports) OR staff_type='physician' # Include both: staff with physician=True (from rating imports) OR staff_type='physician'
queryset = Staff.objects.filter( queryset = (
Q(physician=True) | Q(staff_type=Staff.StaffType.PHYSICIAN) Staff.objects.filter(Q(physician=True) | Q(staff_type=Staff.StaffType.PHYSICIAN))
).select_related('hospital', 'department').distinct() .select_related("hospital", "department")
.distinct()
)
# Apply RBAC filters # Apply RBAC filters
user = request.user user = request.user
@ -40,50 +43,48 @@ def physician_list(request):
queryset = queryset.none() queryset = queryset.none()
# Apply filters # Apply filters
hospital_filter = request.GET.get('hospital') hospital_filter = request.GET.get("hospital")
if hospital_filter: if hospital_filter:
queryset = queryset.filter(hospital_id=hospital_filter) queryset = queryset.filter(hospital_id=hospital_filter)
department_filter = request.GET.get('department') department_filter = request.GET.get("department")
if department_filter: if department_filter:
queryset = queryset.filter(department_id=department_filter) queryset = queryset.filter(department_id=department_filter)
specialization_filter = request.GET.get('specialization') specialization_filter = request.GET.get("specialization")
if specialization_filter: if specialization_filter:
queryset = queryset.filter(specialization__icontains=specialization_filter) queryset = queryset.filter(specialization__icontains=specialization_filter)
status_filter = request.GET.get('status', 'active') status_filter = request.GET.get("status", "active")
if status_filter: if status_filter:
queryset = queryset.filter(status=status_filter) queryset = queryset.filter(status=status_filter)
# Search # Search
search_query = request.GET.get('search') search_query = request.GET.get("search")
if search_query: if search_query:
queryset = queryset.filter( queryset = queryset.filter(
Q(first_name__icontains=search_query) | Q(first_name__icontains=search_query)
Q(last_name__icontains=search_query) | | Q(last_name__icontains=search_query)
Q(license_number__icontains=search_query) | | Q(license_number__icontains=search_query)
Q(specialization__icontains=search_query) | Q(specialization__icontains=search_query)
) )
# Ordering # Ordering
order_by = request.GET.get('order_by', 'last_name') order_by = request.GET.get("order_by", "last_name")
queryset = queryset.order_by(order_by) queryset = queryset.order_by(order_by)
# Pagination # Pagination
page_size = int(request.GET.get('page_size', 25)) page_size = int(request.GET.get("page_size", 25))
paginator = Paginator(queryset, page_size) paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1) page_number = request.GET.get("page", 1)
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
# Get current month ratings for displayed physicians # Get current month ratings for displayed physicians
now = timezone.now() now = timezone.now()
physician_ids = [p.id for p in page_obj.object_list] physician_ids = [p.id for p in page_obj.object_list]
current_ratings = PhysicianMonthlyRating.objects.filter( current_ratings = PhysicianMonthlyRating.objects.filter(
staff_id__in=physician_ids, staff_id__in=physician_ids, year=now.year, month=now.month
year=now.year, ).select_related("staff")
month=now.month
).select_related('staff')
# Create rating lookup # Create rating lookup
ratings_dict = {r.staff_id: r for r in current_ratings} ratings_dict = {r.staff_id: r for r in current_ratings}
@ -92,217 +93,8 @@ def physician_list(request):
for physician in page_obj.object_list: for physician in page_obj.object_list:
physician.current_rating = ratings_dict.get(physician.id) physician.current_rating = ratings_dict.get(physician.id)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
departments = Department.objects.filter(status='active') departments = Department.objects.filter(status="active")
if not user.is_px_admin() and user.hospital:
departments = departments.filter(hospital=user.hospital)
# Get unique specializations (only from physicians)
specializations = Staff.objects.filter(
Q(physician=True) | Q(staff_type=Staff.StaffType.PHYSICIAN)
).values_list('specialization', flat=True).distinct().order_by('specialization')
# Statistics
stats = {
'total': queryset.count(),
'active': queryset.filter(status='active').count(),
}
context = {
'page_obj': page_obj,
'physicians': page_obj.object_list,
'stats': stats,
'hospitals': hospitals,
'departments': departments,
'specializations': specializations,
'filters': request.GET,
'current_year': now.year,
'current_month': now.month,
}
return render(request, 'physicians/physician_list.html', context)
@login_required
def physician_detail(request, pk):
"""
Physician detail view with performance metrics.
Features:
- Full physician details
- Current month rating
- Year-to-date performance
- Monthly ratings history (last 12 months)
- Performance trends
"""
physician = get_object_or_404(
Staff.objects.select_related('hospital', 'department'),
pk=pk
)
# Check permission
user = request.user
if not user.is_px_admin() and user.hospital:
if physician.hospital != user.hospital:
from django.http import Http404
raise Http404("Physician not found")
now = timezone.now()
current_year = now.year
current_month = now.month
# Get current month rating
current_month_rating = PhysicianMonthlyRating.objects.filter(
staff=physician,
year=current_year,
month=current_month
).first()
# Get previous month rating
prev_month = current_month - 1 if current_month > 1 else 12
prev_year = current_year if current_month > 1 else current_year - 1
previous_month_rating = PhysicianMonthlyRating.objects.filter(
staff=physician,
year=prev_year,
month=prev_month
).first()
# Get year-to-date stats
ytd_ratings = PhysicianMonthlyRating.objects.filter(
staff=physician,
year=current_year
)
ytd_stats = ytd_ratings.aggregate(
avg_rating=Avg('average_rating'),
total_surveys=Count('id')
)
# Get last 12 months ratings
ratings_history = PhysicianMonthlyRating.objects.filter(
staff=physician
).order_by('-year', '-month')[:12]
# Get best and worst months from all ratings (not just last 12 months)
all_ratings = PhysicianMonthlyRating.objects.filter(staff=physician)
best_month = all_ratings.order_by('-average_rating').first()
worst_month = all_ratings.order_by('average_rating').first()
# Determine trend
trend = 'stable'
trend_percentage = 0
if current_month_rating and previous_month_rating:
diff = float(current_month_rating.average_rating - previous_month_rating.average_rating)
if previous_month_rating.average_rating > 0:
trend_percentage = (diff / float(previous_month_rating.average_rating)) * 100
if diff > 0.1:
trend = 'improving'
elif diff < -0.1:
trend = 'declining'
context = {
'physician': physician,
'current_month_rating': current_month_rating,
'previous_month_rating': previous_month_rating,
'ytd_average': ytd_stats['avg_rating'],
'ytd_surveys': ytd_stats['total_surveys'],
'ratings_history': ratings_history,
'best_month': best_month,
'worst_month': worst_month,
'trend': trend,
'trend_percentage': abs(trend_percentage),
'current_year': current_year,
'current_month': current_month,
}
return render(request, 'physicians/physician_detail.html', context)
@login_required
def leaderboard(request):
"""
Physician leaderboard view.
Features:
- Top-rated physicians for selected period
- Filters (hospital, department, month/year)
- Ranking with trends
- Performance distribution
"""
# Get parameters
now = timezone.now()
year = int(request.GET.get('year', now.year))
month = int(request.GET.get('month', now.month))
hospital_filter = request.GET.get('hospital')
department_filter = request.GET.get('department')
limit = int(request.GET.get('limit', 20))
# Build queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.filter(
year=year,
month=month,
staff__physician=True
).select_related('staff', 'staff__hospital', 'staff__department')
# Apply RBAC filters
user = request.user
if not user.is_px_admin() and user.hospital:
queryset = queryset.filter(staff__hospital=user.hospital)
# Apply filters
if hospital_filter:
queryset = queryset.filter(staff__hospital_id=hospital_filter)
if department_filter:
queryset = queryset.filter(staff__department_id=department_filter)
# Order by rating
queryset = queryset.order_by('-average_rating')[:limit]
# Get previous month for trend
prev_month = month - 1 if month > 1 else 12
prev_year = year if month > 1 else year - 1
# Build leaderboard with trends
leaderboard = []
for rank, rating in enumerate(queryset, start=1):
# Get previous month rating for trend
prev_rating = PhysicianMonthlyRating.objects.filter(
staff=rating.staff,
year=prev_year,
month=prev_month
).first()
trend = 'stable'
trend_value = 0
if prev_rating:
diff = float(rating.average_rating - prev_rating.average_rating)
trend_value = diff
if diff > 0.1:
trend = 'up'
elif diff < -0.1:
trend = 'down'
leaderboard.append({
'rank': rank,
'rating': rating,
'physician': rating.staff,
'trend': trend,
'trend_value': trend_value,
'prev_rating': prev_rating
})
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
departments = Department.objects.filter(status='active')
if not user.is_px_admin() and user.hospital: if not user.is_px_admin() and user.hospital:
departments = departments.filter(hospital=user.hospital) departments = departments.filter(hospital=user.hospital)
@ -312,9 +104,7 @@ def leaderboard(request):
all_ratings = all_ratings.filter(staff__hospital=user.hospital) all_ratings = all_ratings.filter(staff__hospital=user.hospital)
stats = all_ratings.aggregate( stats = all_ratings.aggregate(
total_physicians=Count('id'), total_physicians=Count("id"), average_rating=Avg("average_rating"), total_surveys=Count("total_surveys")
average_rating=Avg('average_rating'),
total_surveys=Count('total_surveys')
) )
# Distribution # Distribution
@ -324,22 +114,17 @@ def leaderboard(request):
poor = all_ratings.filter(average_rating__lt=2.5).count() poor = all_ratings.filter(average_rating__lt=2.5).count()
context = { context = {
'leaderboard': leaderboard, "leaderboard": leaderboard,
'year': year, "year": year,
'month': month, "month": month,
'hospitals': hospitals, "hospitals": hospitals,
'departments': departments, "departments": departments,
'filters': request.GET, "filters": request.GET,
'stats': stats, "stats": stats,
'distribution': { "distribution": {"excellent": excellent, "good": good, "average": average, "poor": poor},
'excellent': excellent,
'good': good,
'average': average,
'poor': poor
}
} }
return render(request, 'physicians/leaderboard.html', context) return render(request, "physicians/leaderboard.html", context)
@login_required @login_required
@ -356,18 +141,11 @@ def physician_ratings_dashboard(request):
- Top physicians table - Top physicians table
""" """
now = timezone.now() now = timezone.now()
year = int(request.GET.get('year', now.year)) year = int(request.GET.get("year", now.year))
month = int(request.GET.get('month', now.month)) month = int(request.GET.get("month", now.month))
hospital_filter = request.GET.get('hospital') hospital_filter = request.GET.get("hospital")
department_filter = request.GET.get('department') department_filter = request.GET.get("department")
# Get filter options
user = request.user
hospitals = Hospital.objects.filter(status='active')
departments = Department.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
departments = departments.filter(hospital=user.hospital) departments = departments.filter(hospital=user.hospital)
# Get available years (2024 to current year) # Get available years (2024 to current year)
@ -376,13 +154,13 @@ def physician_ratings_dashboard(request):
years.reverse() # Most recent first years.reverse() # Most recent first
context = { context = {
'years': years, "years": years,
'hospitals': hospitals, "hospitals": hospitals,
'departments': departments, "departments": departments,
'filters': request.GET, "filters": request.GET,
} }
return render(request, 'physicians/physician_ratings_dashboard.html', context) return render(request, "physicians/physician_ratings_dashboard.html", context)
@login_required @login_required
@ -394,15 +172,15 @@ def physician_ratings_dashboard_api(request):
""" """
try: try:
now = timezone.now() now = timezone.now()
year = int(request.GET.get('year', now.year)) year = int(request.GET.get("year", now.year))
month = int(request.GET.get('month', now.month)) month = int(request.GET.get("month", now.month))
hospital_filter = request.GET.get('hospital') hospital_filter = request.GET.get("hospital")
department_filter = request.GET.get('department') department_filter = request.GET.get("department")
# Base queryset - only include staff marked as physicians # Base queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.filter( queryset = PhysicianMonthlyRating.objects.filter(staff__physician=True).select_related(
staff__physician=True "staff", "staff__hospital", "staff__department"
).select_related('staff', 'staff__hospital', 'staff__department') )
# Apply RBAC filters # Apply RBAC filters
user = request.user user = request.user
@ -420,9 +198,9 @@ def physician_ratings_dashboard_api(request):
# 1. Statistics # 1. Statistics
stats = current_period.aggregate( stats = current_period.aggregate(
total_physicians=Count('id', distinct=True), total_physicians=Count("id", distinct=True),
average_rating=Avg('average_rating'), average_rating=Avg("average_rating"),
total_surveys=Sum('total_surveys') total_surveys=Sum("total_surveys"),
) )
excellent_count = current_period.filter(average_rating__gte=4.5).count() excellent_count = current_period.filter(average_rating__gte=4.5).count()
@ -437,15 +215,16 @@ def physician_ratings_dashboard_api(request):
y -= 1 y -= 1
period_data = queryset.filter(year=y, month=m).aggregate( period_data = queryset.filter(year=y, month=m).aggregate(
avg=Avg('average_rating'), avg=Avg("average_rating"), surveys=Sum("total_surveys")
surveys=Sum('total_surveys')
) )
trend_data.append({ trend_data.append(
'period': f'{y}-{m:02d}', {
'average_rating': float(period_data['avg'] or 0), "period": f"{y}-{m:02d}",
'total_surveys': period_data['surveys'] or 0 "average_rating": float(period_data["avg"] or 0),
}) "total_surveys": period_data["surveys"] or 0,
}
)
# 3. Rating Distribution # 3. Rating Distribution
excellent = current_period.filter(average_rating__gte=4.5).count() excellent = current_period.filter(average_rating__gte=4.5).count()
@ -453,85 +232,83 @@ def physician_ratings_dashboard_api(request):
average = current_period.filter(average_rating__gte=2.5, average_rating__lt=3.5).count() average = current_period.filter(average_rating__gte=2.5, average_rating__lt=3.5).count()
poor = current_period.filter(average_rating__lt=2.5).count() poor = current_period.filter(average_rating__lt=2.5).count()
distribution = { distribution = {"excellent": excellent, "good": good, "average": average, "poor": poor}
'excellent': excellent,
'good': good,
'average': average,
'poor': poor
}
# 4. Department Comparison (top 10) # 4. Department Comparison (top 10)
dept_data = current_period.values('staff__department__name').annotate( dept_data = (
average_rating=Avg('average_rating'), current_period.values("staff__department__name")
total_surveys=Sum('total_surveys'), .annotate(
physician_count=Count('id', distinct=True) average_rating=Avg("average_rating"),
).filter(staff__department__isnull=False).order_by('-average_rating')[:10] total_surveys=Sum("total_surveys"),
physician_count=Count("id", distinct=True),
)
.filter(staff__department__isnull=False)
.order_by("-average_rating")[:10]
)
departments = [ departments = [
{ {
'name': item['staff__department__name'] or 'Unknown', "name": item["staff__department__name"] or "Unknown",
'average_rating': float(item['average_rating'] or 0), "average_rating": float(item["average_rating"] or 0),
'total_surveys': item['total_surveys'] or 0 "total_surveys": item["total_surveys"] or 0,
} }
for item in dept_data for item in dept_data
] ]
# 5. Sentiment Analysis # 5. Sentiment Analysis
sentiment = current_period.aggregate( sentiment = current_period.aggregate(
positive=Sum('positive_count'), positive=Sum("positive_count"), neutral=Sum("neutral_count"), negative=Sum("negative_count")
neutral=Sum('neutral_count'),
negative=Sum('negative_count')
) )
total_sentiment = (sentiment['positive'] or 0) + (sentiment['neutral'] or 0) + (sentiment['negative'] or 0) total_sentiment = (sentiment["positive"] or 0) + (sentiment["neutral"] or 0) + (sentiment["negative"] or 0)
if total_sentiment > 0: if total_sentiment > 0:
sentiment_pct = { sentiment_pct = {
'positive': ((sentiment['positive'] or 0) / total_sentiment) * 100, "positive": ((sentiment["positive"] or 0) / total_sentiment) * 100,
'neutral': ((sentiment['neutral'] or 0) / total_sentiment) * 100, "neutral": ((sentiment["neutral"] or 0) / total_sentiment) * 100,
'negative': ((sentiment['negative'] or 0) / total_sentiment) * 100 "negative": ((sentiment["negative"] or 0) / total_sentiment) * 100,
} }
else: else:
sentiment_pct = {'positive': 0, 'neutral': 0, 'negative': 0} sentiment_pct = {"positive": 0, "neutral": 0, "negative": 0}
# 6. Top 10 Physicians # 6. Top 10 Physicians
top_physicians = current_period.select_related( top_physicians = current_period.select_related("staff", "staff__hospital", "staff__department").order_by(
'staff', 'staff__hospital', 'staff__department' "-average_rating", "-total_surveys"
).order_by('-average_rating', '-total_surveys')[:10] )[:10]
physicians_list = [ physicians_list = [
{ {
'id': rating.staff.id, "id": rating.staff.id,
'name': rating.staff.get_full_name(), "name": rating.staff.get_full_name(),
'license_number': rating.staff.license_number, "license_number": rating.staff.license_number,
'specialization': rating.staff.specialization or '-', "specialization": rating.staff.specialization or "-",
'department': rating.staff.department.name if rating.staff.department else '-', "department": rating.staff.department.name if rating.staff.department else "-",
'hospital': rating.staff.hospital.name if rating.staff.hospital else '-', "hospital": rating.staff.hospital.name if rating.staff.hospital else "-",
'rating': float(rating.average_rating), "rating": float(rating.average_rating),
'surveys': rating.total_surveys "surveys": rating.total_surveys,
} }
for rating in top_physicians for rating in top_physicians
] ]
return JsonResponse({ return JsonResponse(
'statistics': { {
'total_physicians': stats['total_physicians'] or 0, "statistics": {
'average_rating': float(stats['average_rating'] or 0), "total_physicians": stats["total_physicians"] or 0,
'total_surveys': stats['total_surveys'] or 0, "average_rating": float(stats["average_rating"] or 0),
'excellent_count': excellent_count "total_surveys": stats["total_surveys"] or 0,
}, "excellent_count": excellent_count,
'trend': trend_data, },
'distribution': distribution, "trend": trend_data,
'departments': departments, "distribution": distribution,
'sentiment': sentiment_pct, "departments": departments,
'top_physicians': physicians_list "sentiment": sentiment_pct,
}) "top_physicians": physicians_list,
}
)
except Exception as e: except Exception as e:
import traceback import traceback
return JsonResponse({
'error': str(e), return JsonResponse({"error": str(e), "traceback": traceback.format_exc()}, status=500)
'traceback': traceback.format_exc()
}, status=500)
@login_required @login_required
@ -546,9 +323,9 @@ def ratings_list(request):
- Pagination - Pagination
""" """
# Base queryset - only include staff marked as physicians # Base queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.filter( queryset = PhysicianMonthlyRating.objects.filter(staff__physician=True).select_related(
staff__physician=True "staff", "staff__hospital", "staff__department"
).select_related('staff', 'staff__hospital', 'staff__department') )
# Apply RBAC filters # Apply RBAC filters
user = request.user user = request.user
@ -556,67 +333,63 @@ def ratings_list(request):
queryset = queryset.filter(staff__hospital=user.hospital) queryset = queryset.filter(staff__hospital=user.hospital)
# Apply filters # Apply filters
physician_filter = request.GET.get('physician') physician_filter = request.GET.get("physician")
if physician_filter: if physician_filter:
queryset = queryset.filter(staff_id=physician_filter) queryset = queryset.filter(staff_id=physician_filter)
hospital_filter = request.GET.get('hospital') hospital_filter = request.GET.get("hospital")
if hospital_filter: if hospital_filter:
queryset = queryset.filter(staff__hospital_id=hospital_filter) queryset = queryset.filter(staff__hospital_id=hospital_filter)
department_filter = request.GET.get('department') department_filter = request.GET.get("department")
if department_filter: if department_filter:
queryset = queryset.filter(staff__department_id=department_filter) queryset = queryset.filter(staff__department_id=department_filter)
year_filter = request.GET.get('year') year_filter = request.GET.get("year")
if year_filter: if year_filter:
queryset = queryset.filter(year=int(year_filter)) queryset = queryset.filter(year=int(year_filter))
month_filter = request.GET.get('month') month_filter = request.GET.get("month")
if month_filter: if month_filter:
queryset = queryset.filter(month=int(month_filter)) queryset = queryset.filter(month=int(month_filter))
# Search # Search
search_query = request.GET.get('search') search_query = request.GET.get("search")
if search_query: if search_query:
queryset = queryset.filter( queryset = queryset.filter(
Q(staff__first_name__icontains=search_query) | Q(staff__first_name__icontains=search_query)
Q(staff__last_name__icontains=search_query) | | Q(staff__last_name__icontains=search_query)
Q(staff__license_number__icontains=search_query) | Q(staff__license_number__icontains=search_query)
) )
# Ordering # Ordering
order_by = request.GET.get('order_by', '-year,-month,-average_rating') order_by = request.GET.get("order_by", "-year,-month,-average_rating")
queryset = queryset.order_by(*order_by.split(',')) queryset = queryset.order_by(*order_by.split(","))
# Pagination # Pagination
page_size = int(request.GET.get('page_size', 25)) page_size = int(request.GET.get("page_size", 25))
paginator = Paginator(queryset, page_size) paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1) page_number = request.GET.get("page", 1)
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
departments = Department.objects.filter(status='active') departments = Department.objects.filter(status="active")
if not user.is_px_admin() and user.hospital: if not user.is_px_admin() and user.hospital:
departments = departments.filter(hospital=user.hospital) departments = departments.filter(hospital=user.hospital)
# Get available years # Get available years
years = PhysicianMonthlyRating.objects.values_list('year', flat=True).distinct().order_by('-year') years = PhysicianMonthlyRating.objects.values_list("year", flat=True).distinct().order_by("-year")
context = { context = {
'page_obj': page_obj, "page_obj": page_obj,
'ratings': page_obj.object_list, "ratings": page_obj.object_list,
'hospitals': hospitals, "hospitals": hospitals,
'departments': departments, "departments": departments,
'years': years, "years": years,
'filters': request.GET, "filters": request.GET,
} }
return render(request, 'physicians/ratings_list.html', context) return render(request, "physicians/ratings_list.html", context)
@login_required @login_required
@ -632,16 +405,14 @@ def specialization_overview(request):
""" """
# Get parameters # Get parameters
now = timezone.now() now = timezone.now()
year = int(request.GET.get('year', now.year)) year = int(request.GET.get("year", now.year))
month = int(request.GET.get('month', now.month)) month = int(request.GET.get("month", now.month))
hospital_filter = request.GET.get('hospital') hospital_filter = request.GET.get("hospital")
# Base queryset - only include staff marked as physicians # Base queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.filter( queryset = PhysicianMonthlyRating.objects.filter(year=year, month=month, staff__physician=True).select_related(
year=year, "staff", "staff__hospital", "staff__department"
month=month, )
staff__physician=True
).select_related('staff', 'staff__hospital', 'staff__department')
# Apply RBAC filters # Apply RBAC filters
user = request.user user = request.user
@ -655,60 +426,57 @@ def specialization_overview(request):
# Aggregate by specialization # Aggregate by specialization
specialization_data = {} specialization_data = {}
for rating in queryset: for rating in queryset:
spec = rating.staff.specialization spec = rating.staff.specialization
if spec not in specialization_data: if spec not in specialization_data:
specialization_data[spec] = { specialization_data[spec] = {
'specialization': spec, "specialization": spec,
'physicians': [], "physicians": [],
'total_physicians': 0, "total_physicians": 0,
'total_surveys': 0, "total_surveys": 0,
'total_positive': 0, "total_positive": 0,
'total_neutral': 0, "total_neutral": 0,
'total_negative': 0, "total_negative": 0,
'ratings_sum': 0, "ratings_sum": 0,
} }
specialization_data[spec]['physicians'].append(rating) specialization_data[spec]["physicians"].append(rating)
specialization_data[spec]['total_physicians'] += 1 specialization_data[spec]["total_physicians"] += 1
specialization_data[spec]['total_surveys'] += rating.total_surveys specialization_data[spec]["total_surveys"] += rating.total_surveys
specialization_data[spec]['total_positive'] += rating.positive_count specialization_data[spec]["total_positive"] += rating.positive_count
specialization_data[spec]['total_neutral'] += rating.neutral_count specialization_data[spec]["total_neutral"] += rating.neutral_count
specialization_data[spec]['total_negative'] += rating.negative_count specialization_data[spec]["total_negative"] += rating.negative_count
specialization_data[spec]['ratings_sum'] += float(rating.average_rating) specialization_data[spec]["ratings_sum"] += float(rating.average_rating)
# Calculate averages # Calculate averages
specializations = [] specializations = []
for spec, data in specialization_data.items(): for spec, data in specialization_data.items():
avg_rating = data['ratings_sum'] / data['total_physicians'] if data['total_physicians'] > 0 else 0 avg_rating = data["ratings_sum"] / data["total_physicians"] if data["total_physicians"] > 0 else 0
specializations.append({ specializations.append(
'specialization': spec, {
'total_physicians': data['total_physicians'], "specialization": spec,
'average_rating': round(avg_rating, 2), "total_physicians": data["total_physicians"],
'total_surveys': data['total_surveys'], "average_rating": round(avg_rating, 2),
'positive_count': data['total_positive'], "total_surveys": data["total_surveys"],
'neutral_count': data['total_neutral'], "positive_count": data["total_positive"],
'negative_count': data['total_negative'], "neutral_count": data["total_neutral"],
'physicians': sorted(data['physicians'], key=lambda x: x.average_rating, reverse=True) "negative_count": data["total_negative"],
}) "physicians": sorted(data["physicians"], key=lambda x: x.average_rating, reverse=True),
}
)
# Sort by average rating # Sort by average rating
specializations.sort(key=lambda x: x['average_rating'], reverse=True) specializations.sort(key=lambda x: x["average_rating"], reverse=True)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
context = { context = {
'specializations': specializations, "specializations": specializations,
'year': year, "year": year,
'month': month, "month": month,
'hospitals': hospitals, "hospitals": hospitals,
'filters': request.GET, "filters": request.GET,
} }
return render(request, 'physicians/specialization_overview.html', context) return render(request, "physicians/specialization_overview.html", context)
@login_required @login_required
@ -724,16 +492,14 @@ def department_overview(request):
""" """
# Get parameters # Get parameters
now = timezone.now() now = timezone.now()
year = int(request.GET.get('year', now.year)) year = int(request.GET.get("year", now.year))
month = int(request.GET.get('month', now.month)) month = int(request.GET.get("month", now.month))
hospital_filter = request.GET.get('hospital') hospital_filter = request.GET.get("hospital")
# Base queryset - only include staff marked as physicians # Base queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.filter( queryset = PhysicianMonthlyRating.objects.filter(year=year, month=month, staff__physician=True).select_related(
year=year, "staff", "staff__hospital", "staff__department"
month=month, )
staff__physician=True
).select_related('staff', 'staff__hospital', 'staff__department')
# Apply RBAC filters # Apply RBAC filters
user = request.user user = request.user
@ -754,53 +520,51 @@ def department_overview(request):
dept_key = str(dept.id) dept_key = str(dept.id)
if dept_key not in department_data: if dept_key not in department_data:
department_data[dept_key] = { department_data[dept_key] = {
'department': dept, "department": dept,
'physicians': [], "physicians": [],
'total_physicians': 0, "total_physicians": 0,
'total_surveys': 0, "total_surveys": 0,
'total_positive': 0, "total_positive": 0,
'total_neutral': 0, "total_neutral": 0,
'total_negative': 0, "total_negative": 0,
'ratings_sum': 0, "ratings_sum": 0,
} }
department_data[dept_key]['physicians'].append(rating) department_data[dept_key]["physicians"].append(rating)
department_data[dept_key]['total_physicians'] += 1 department_data[dept_key]["total_physicians"] += 1
department_data[dept_key]['total_surveys'] += rating.total_surveys department_data[dept_key]["total_surveys"] += rating.total_surveys
department_data[dept_key]['total_positive'] += rating.positive_count department_data[dept_key]["total_positive"] += rating.positive_count
department_data[dept_key]['total_neutral'] += rating.neutral_count department_data[dept_key]["total_neutral"] += rating.neutral_count
department_data[dept_key]['total_negative'] += rating.negative_count department_data[dept_key]["total_negative"] += rating.negative_count
department_data[dept_key]['ratings_sum'] += float(rating.average_rating) department_data[dept_key]["ratings_sum"] += float(rating.average_rating)
# Calculate averages # Calculate averages
departments = [] departments = []
for dept_key, data in department_data.items(): for dept_key, data in department_data.items():
avg_rating = data['ratings_sum'] / data['total_physicians'] if data['total_physicians'] > 0 else 0 avg_rating = data["ratings_sum"] / data["total_physicians"] if data["total_physicians"] > 0 else 0
departments.append({ departments.append(
'department': data['department'], {
'total_physicians': data['total_physicians'], "department": data["department"],
'average_rating': round(avg_rating, 2), "total_physicians": data["total_physicians"],
'total_surveys': data['total_surveys'], "average_rating": round(avg_rating, 2),
'positive_count': data['total_positive'], "total_surveys": data["total_surveys"],
'neutral_count': data['total_neutral'], "positive_count": data["total_positive"],
'negative_count': data['total_negative'], "neutral_count": data["total_neutral"],
'physicians': sorted(data['physicians'], key=lambda x: x.average_rating, reverse=True) "negative_count": data["total_negative"],
}) "physicians": sorted(data["physicians"], key=lambda x: x.average_rating, reverse=True),
}
)
# Sort by average rating # Sort by average rating
departments.sort(key=lambda x: x['average_rating'], reverse=True) departments.sort(key=lambda x: x["average_rating"], reverse=True)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
context = { context = {
'departments': departments, "departments": departments,
'year': year, "year": year,
'month': month, "month": month,
'hospitals': hospitals, "hospitals": hospitals,
'filters': request.GET, "filters": request.GET,
} }
return render(request, 'physicians/department_overview.html', context) return render(request, "physicians/department_overview.html", context)

View File

@ -83,7 +83,6 @@ def project_list(request):
context = { context = {
'page_obj': page_obj, 'page_obj': page_obj,
'projects': page_obj.object_list, 'projects': page_obj.object_list,
'hospitals': hospitals,
'stats': stats, 'stats': stats,
'filters': request.GET, 'filters': request.GET,
} }

File diff suppressed because it is too large Load Diff