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
"""
from datetime import datetime
from django.contrib.auth.decorators import login_required
@ -25,14 +26,14 @@ import json
def serialize_queryset_values(queryset):
"""Properly serialize QuerySet values to JSON string."""
if queryset is None:
return '[]'
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
if hasattr(value, "hex"): # UUID object
row[key] = str(value)
# Convert Python None to JavaScript null
elif value is None:
@ -60,11 +61,11 @@ def analytics_dashboard(request):
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')
hospital_filter = request.GET.get("hospital")
if hospital_filter:
hospital = Hospital.objects.filter(id=hospital_filter).first()
elif user.hospital:
@ -75,7 +76,7 @@ def analytics_dashboard(request):
# Base querysets
complaints_queryset = Complaint.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()
if hospital:
@ -86,117 +87,128 @@ def analytics_dashboard(request):
# ============ 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()
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_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]
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]
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')
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')
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()
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_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]
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
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()
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]
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()
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')
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]
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
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')
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')
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]
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
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']
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:
@ -206,14 +218,12 @@ def analytics_dashboard(request):
# Average action completion time
closed_actions_with_time = actions_queryset.filter(
status='closed',
closed_at__isnull=False,
created_at__isnull=False
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']
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:
@ -224,17 +234,13 @@ def analytics_dashboard(request):
# ============ 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')
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
)
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()
@ -243,76 +249,52 @@ def analytics_dashboard(request):
else:
nps_score = 0
# Get hospitals for filter
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
# Build comprehensive KPI data
kpis = {
# Complaints
'total_complaints': total_complaints,
'open_complaints': open_complaints,
'in_progress_complaints': in_progress_complaints,
'resolved_complaints': resolved_complaints,
'closed_complaints': closed_complaints,
'overdue_complaints': overdue_complaints,
'avg_resolution_hours': round(avg_resolution_hours, 1),
'sla_compliance': round(sla_compliance, 1),
# Actions
'total_actions': total_actions,
'open_actions': open_actions,
'in_progress_actions': in_progress_actions,
'approved_actions': approved_actions,
'closed_actions': closed_actions,
'overdue_actions': overdue_actions,
'avg_action_days': round(avg_action_days, 1),
# Surveys
'total_surveys': total_surveys,
'avg_survey_score': round(avg_survey_score, 2),
'nps_score': round(nps_score, 1),
'negative_surveys': negative_surveys,
'completion_rate': round(completion_rate, 1),
# Feedback
'total_feedback': total_feedback,
'compliments': compliments,
'suggestions': suggestions,
'avg_rating': round(avg_rating, 2),
"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,
'hospitals': hospitals,
'selected_hospital': hospital,
# Complaint analytics - serialize properly for JSON
'complaint_sources': serialize_queryset_values(complaint_sources),
'top_domains': serialize_queryset_values(top_domains),
'top_categories': serialize_queryset_values(top_categories),
'severity_breakdown': serialize_queryset_values(severity_breakdown),
'status_breakdown': serialize_queryset_values(status_breakdown),
'complaint_trend': serialize_queryset_values(complaint_trend),
# Action analytics
'action_sources': serialize_queryset_values(action_sources),
'action_categories': serialize_queryset_values(action_categories),
# Survey analytics
'survey_types': serialize_queryset_values(survey_types),
'survey_score_trend': serialize_queryset_values(survey_score_trend),
# Feedback analytics
'sentiment_breakdown': serialize_queryset_values(sentiment_breakdown),
'feedback_categories': serialize_queryset_values(feedback_categories),
# Department rankings
'department_rankings': department_rankings,
"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)
return render(request, "analytics/dashboard.html", context)
@block_source_user
@ -320,34 +302,34 @@ def analytics_dashboard(request):
def kpi_list(request):
"""KPI definitions list view"""
queryset = KPI.objects.all()
# Apply filters
category_filter = request.GET.get('category')
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':
is_active = request.GET.get("is_active")
if is_active == "true":
queryset = queryset.filter(is_active=True)
elif is_active == 'false':
elif is_active == "false":
queryset = queryset.filter(is_active=False)
# Ordering
queryset = queryset.order_by('category', 'name')
queryset = queryset.order_by("category", "name")
# Pagination
page_size = int(request.GET.get('page_size', 25))
page_size = int(request.GET.get("page_size", 25))
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)
context = {
'page_obj': page_obj,
'kpis': page_obj.object_list,
'filters': request.GET,
"page_obj": page_obj,
"kpis": page_obj.object_list,
"filters": request.GET,
}
return render(request, 'analytics/kpi_list.html', context)
return render(request, "analytics/kpi_list.html", context)
@block_source_user
@ -355,7 +337,7 @@ def kpi_list(request):
def command_center(request):
"""
PX Command Center - Unified Dashboard
Comprehensive dashboard showing all PX360 metrics:
- Complaints, Surveys, Actions KPIs
- Interactive charts with ApexCharts
@ -363,53 +345,52 @@ def command_center(request):
- 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', ''),
"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')
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'])
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')
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,
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
custom_end=custom_end,
)
context = {
'filters': filters,
'hospitals': hospitals,
'departments': departments,
'kpis': kpis,
"filters": filters,
"departments": departments,
"kpis": kpis,
}
return render(request, 'analytics/command_center.html', context)
return render(request, "analytics/command_center.html", context)
@block_source_user
@ -417,39 +398,39 @@ def command_center(request):
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)
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')
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')
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,
@ -458,51 +439,62 @@ def command_center_api(request):
department_id=department_id,
kpi_category=kpi_category,
custom_start=custom_start,
custom_end=custom_end
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'
"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
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':
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
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))
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'
"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(
@ -512,64 +504,60 @@ def command_center_api(request):
hospital_id=hospital_id,
department_id=department_id,
custom_start=custom_start,
custom_end=custom_end
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]
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')
"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_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
"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
})
return JsonResponse({"kpis": kpis, "charts": charts, "tables": tables})
@block_source_user
@ -577,40 +565,40 @@ def command_center_api(request):
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)
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')
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')
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,
@ -619,18 +607,18 @@ def export_command_center(request, export_format):
department_id=department_id,
kpi_category=kpi_category,
custom_start=custom_start,
custom_end=custom_end
custom_end=custom_end,
)
chart_types = [
'complaints_trend',
'complaints_by_category',
'survey_satisfaction_trend',
'survey_distribution',
'department_performance',
'physician_leaderboard'
"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(
@ -640,77 +628,64 @@ def export_command_center(request, export_format):
hospital_id=hospital_id,
department_id=department_id,
custom_start=custom_start,
custom_end=custom_end
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]
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')
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'
)
)
.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': [
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'])
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_data = ExportService.prepare_dashboard_data(user=user, kpis=kpis, charts=charts, tables=tables)
# Export based on format
if export_format == 'excel':
if export_format == "excel":
return ExportService.export_to_excel(export_data)
elif export_format == 'pdf':
elif export_format == "pdf":
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_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')
if not user.is_px_admin() and user.hospital:
@ -172,7 +168,6 @@ def appreciation_list(request):
'page_obj': page_obj,
'appreciations': page_obj.object_list,
'stats': stats,
'hospitals': hospitals,
'departments': departments,
'categories': categories,
'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))
context = {
'hospitals': hospitals,
'categories': categories,
'visibility_choices': AppreciationVisibility.choices,
}
@ -410,10 +404,6 @@ def leaderboard_view(request):
page_number = request.GET.get('page', 1)
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')
if not user.is_px_admin() and user.hospital:
@ -426,7 +416,6 @@ def leaderboard_view(request):
context = {
'page_obj': page_obj,
'leaderboard': page_obj.object_list,
'hospitals': hospitals,
'departments': departments,
'months': months,
'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')
context = {
'hospitals': hospitals,
'departments': departments,
'staff_list': staff_queryset,
'selected_hospital_id': hospital_id,

View File

@ -148,10 +148,6 @@ def feedback_list(request):
page_number = request.GET.get('page', 1)
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')
if not user.is_px_admin() and user.hospital:
@ -179,7 +175,6 @@ def feedback_list(request):
'page_obj': page_obj,
'feedbacks': page_obj.object_list,
'stats': stats,
'hospitals': hospitals,
'departments': departments,
'assignable_users': assignable_users,
'status_choices': FeedbackStatus.choices,

View File

@ -105,10 +105,6 @@ def journey_instance_list(request):
page_number = request.GET.get('page', 1)
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')
if not user.is_px_admin() and user.hospital:
@ -125,7 +121,6 @@ def journey_instance_list(request):
'page_obj': page_obj,
'journeys': page_obj.object_list,
'stats': stats,
'hospitals': hospitals,
'departments': departments,
'filters': request.GET,
}
@ -221,15 +216,10 @@ def journey_template_list(request):
page_number = request.GET.get('page', 1)
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 = {
'page_obj': page_obj,
'templates': page_obj.object_list,
'hospitals': hospitals,
'filters': request.GET,
}

View File

@ -1,6 +1,7 @@
"""
Physicians Console UI views - Server-rendered templates for physician management
"""
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
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
# Include both: staff with physician=True (from rating imports) OR staff_type='physician'
queryset = Staff.objects.filter(
Q(physician=True) | Q(staff_type=Staff.StaffType.PHYSICIAN)
).select_related('hospital', 'department').distinct()
queryset = (
Staff.objects.filter(Q(physician=True) | Q(staff_type=Staff.StaffType.PHYSICIAN))
.select_related("hospital", "department")
.distinct()
)
# Apply RBAC filters
user = request.user
@ -40,50 +43,48 @@ def physician_list(request):
queryset = queryset.none()
# Apply filters
hospital_filter = request.GET.get('hospital')
hospital_filter = request.GET.get("hospital")
if hospital_filter:
queryset = queryset.filter(hospital_id=hospital_filter)
department_filter = request.GET.get('department')
department_filter = request.GET.get("department")
if department_filter:
queryset = queryset.filter(department_id=department_filter)
specialization_filter = request.GET.get('specialization')
specialization_filter = request.GET.get("specialization")
if 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:
queryset = queryset.filter(status=status_filter)
# Search
search_query = request.GET.get('search')
search_query = request.GET.get("search")
if search_query:
queryset = queryset.filter(
Q(first_name__icontains=search_query) |
Q(last_name__icontains=search_query) |
Q(license_number__icontains=search_query) |
Q(specialization__icontains=search_query)
Q(first_name__icontains=search_query)
| Q(last_name__icontains=search_query)
| Q(license_number__icontains=search_query)
| Q(specialization__icontains=search_query)
)
# 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)
# Pagination
page_size = int(request.GET.get('page_size', 25))
page_size = int(request.GET.get("page_size", 25))
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)
# Get current month ratings for displayed physicians
now = timezone.now()
physician_ids = [p.id for p in page_obj.object_list]
current_ratings = PhysicianMonthlyRating.objects.filter(
staff_id__in=physician_ids,
year=now.year,
month=now.month
).select_related('staff')
staff_id__in=physician_ids, year=now.year, month=now.month
).select_related("staff")
# Create rating lookup
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:
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')
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')
departments = Department.objects.filter(status="active")
if not user.is_px_admin() and user.hospital:
departments = departments.filter(hospital=user.hospital)
@ -312,9 +104,7 @@ def leaderboard(request):
all_ratings = all_ratings.filter(staff__hospital=user.hospital)
stats = all_ratings.aggregate(
total_physicians=Count('id'),
average_rating=Avg('average_rating'),
total_surveys=Count('total_surveys')
total_physicians=Count("id"), average_rating=Avg("average_rating"), total_surveys=Count("total_surveys")
)
# Distribution
@ -324,22 +114,17 @@ def leaderboard(request):
poor = all_ratings.filter(average_rating__lt=2.5).count()
context = {
'leaderboard': leaderboard,
'year': year,
'month': month,
'hospitals': hospitals,
'departments': departments,
'filters': request.GET,
'stats': stats,
'distribution': {
'excellent': excellent,
'good': good,
'average': average,
'poor': poor
}
"leaderboard": leaderboard,
"year": year,
"month": month,
"hospitals": hospitals,
"departments": departments,
"filters": request.GET,
"stats": stats,
"distribution": {"excellent": excellent, "good": good, "average": average, "poor": poor},
}
return render(request, 'physicians/leaderboard.html', context)
return render(request, "physicians/leaderboard.html", context)
@login_required
@ -356,18 +141,11 @@ def physician_ratings_dashboard(request):
- Top physicians table
"""
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')
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")
# 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)
# Get available years (2024 to current year)
@ -376,13 +154,13 @@ def physician_ratings_dashboard(request):
years.reverse() # Most recent first
context = {
'years': years,
'hospitals': hospitals,
'departments': departments,
'filters': request.GET,
"years": years,
"hospitals": hospitals,
"departments": departments,
"filters": request.GET,
}
return render(request, 'physicians/physician_ratings_dashboard.html', context)
return render(request, "physicians/physician_ratings_dashboard.html", context)
@login_required
@ -394,15 +172,15 @@ def physician_ratings_dashboard_api(request):
"""
try:
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')
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")
# Base queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.filter(
staff__physician=True
).select_related('staff', 'staff__hospital', 'staff__department')
queryset = PhysicianMonthlyRating.objects.filter(staff__physician=True).select_related(
"staff", "staff__hospital", "staff__department"
)
# Apply RBAC filters
user = request.user
@ -420,9 +198,9 @@ def physician_ratings_dashboard_api(request):
# 1. Statistics
stats = current_period.aggregate(
total_physicians=Count('id', distinct=True),
average_rating=Avg('average_rating'),
total_surveys=Sum('total_surveys')
total_physicians=Count("id", distinct=True),
average_rating=Avg("average_rating"),
total_surveys=Sum("total_surveys"),
)
excellent_count = current_period.filter(average_rating__gte=4.5).count()
@ -437,15 +215,16 @@ def physician_ratings_dashboard_api(request):
y -= 1
period_data = queryset.filter(year=y, month=m).aggregate(
avg=Avg('average_rating'),
surveys=Sum('total_surveys')
avg=Avg("average_rating"), surveys=Sum("total_surveys")
)
trend_data.append({
'period': f'{y}-{m:02d}',
'average_rating': float(period_data['avg'] or 0),
'total_surveys': period_data['surveys'] or 0
})
trend_data.append(
{
"period": f"{y}-{m:02d}",
"average_rating": float(period_data["avg"] or 0),
"total_surveys": period_data["surveys"] or 0,
}
)
# 3. Rating Distribution
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()
poor = current_period.filter(average_rating__lt=2.5).count()
distribution = {
'excellent': excellent,
'good': good,
'average': average,
'poor': poor
}
distribution = {"excellent": excellent, "good": good, "average": average, "poor": poor}
# 4. Department Comparison (top 10)
dept_data = current_period.values('staff__department__name').annotate(
average_rating=Avg('average_rating'),
total_surveys=Sum('total_surveys'),
physician_count=Count('id', distinct=True)
).filter(staff__department__isnull=False).order_by('-average_rating')[:10]
dept_data = (
current_period.values("staff__department__name")
.annotate(
average_rating=Avg("average_rating"),
total_surveys=Sum("total_surveys"),
physician_count=Count("id", distinct=True),
)
.filter(staff__department__isnull=False)
.order_by("-average_rating")[:10]
)
departments = [
{
'name': item['staff__department__name'] or 'Unknown',
'average_rating': float(item['average_rating'] or 0),
'total_surveys': item['total_surveys'] or 0
"name": item["staff__department__name"] or "Unknown",
"average_rating": float(item["average_rating"] or 0),
"total_surveys": item["total_surveys"] or 0,
}
for item in dept_data
]
# 5. Sentiment Analysis
sentiment = current_period.aggregate(
positive=Sum('positive_count'),
neutral=Sum('neutral_count'),
negative=Sum('negative_count')
positive=Sum("positive_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:
sentiment_pct = {
'positive': ((sentiment['positive'] or 0) / total_sentiment) * 100,
'neutral': ((sentiment['neutral'] or 0) / total_sentiment) * 100,
'negative': ((sentiment['negative'] or 0) / total_sentiment) * 100
"positive": ((sentiment["positive"] or 0) / total_sentiment) * 100,
"neutral": ((sentiment["neutral"] or 0) / total_sentiment) * 100,
"negative": ((sentiment["negative"] or 0) / total_sentiment) * 100,
}
else:
sentiment_pct = {'positive': 0, 'neutral': 0, 'negative': 0}
sentiment_pct = {"positive": 0, "neutral": 0, "negative": 0}
# 6. Top 10 Physicians
top_physicians = current_period.select_related(
'staff', 'staff__hospital', 'staff__department'
).order_by('-average_rating', '-total_surveys')[:10]
top_physicians = current_period.select_related("staff", "staff__hospital", "staff__department").order_by(
"-average_rating", "-total_surveys"
)[:10]
physicians_list = [
{
'id': rating.staff.id,
'name': rating.staff.get_full_name(),
'license_number': rating.staff.license_number,
'specialization': rating.staff.specialization or '-',
'department': rating.staff.department.name if rating.staff.department else '-',
'hospital': rating.staff.hospital.name if rating.staff.hospital else '-',
'rating': float(rating.average_rating),
'surveys': rating.total_surveys
"id": rating.staff.id,
"name": rating.staff.get_full_name(),
"license_number": rating.staff.license_number,
"specialization": rating.staff.specialization or "-",
"department": rating.staff.department.name if rating.staff.department else "-",
"hospital": rating.staff.hospital.name if rating.staff.hospital else "-",
"rating": float(rating.average_rating),
"surveys": rating.total_surveys,
}
for rating in top_physicians
]
return JsonResponse({
'statistics': {
'total_physicians': stats['total_physicians'] or 0,
'average_rating': float(stats['average_rating'] or 0),
'total_surveys': stats['total_surveys'] or 0,
'excellent_count': excellent_count
},
'trend': trend_data,
'distribution': distribution,
'departments': departments,
'sentiment': sentiment_pct,
'top_physicians': physicians_list
})
return JsonResponse(
{
"statistics": {
"total_physicians": stats["total_physicians"] or 0,
"average_rating": float(stats["average_rating"] or 0),
"total_surveys": stats["total_surveys"] or 0,
"excellent_count": excellent_count,
},
"trend": trend_data,
"distribution": distribution,
"departments": departments,
"sentiment": sentiment_pct,
"top_physicians": physicians_list,
}
)
except Exception as e:
import traceback
return JsonResponse({
'error': str(e),
'traceback': traceback.format_exc()
}, status=500)
return JsonResponse({"error": str(e), "traceback": traceback.format_exc()}, status=500)
@login_required
@ -546,9 +323,9 @@ def ratings_list(request):
- Pagination
"""
# Base queryset - only include staff marked as physicians
queryset = PhysicianMonthlyRating.objects.filter(
staff__physician=True
).select_related('staff', 'staff__hospital', 'staff__department')
queryset = PhysicianMonthlyRating.objects.filter(staff__physician=True).select_related(
"staff", "staff__hospital", "staff__department"
)
# Apply RBAC filters
user = request.user
@ -556,67 +333,63 @@ def ratings_list(request):
queryset = queryset.filter(staff__hospital=user.hospital)
# Apply filters
physician_filter = request.GET.get('physician')
physician_filter = request.GET.get("physician")
if physician_filter:
queryset = queryset.filter(staff_id=physician_filter)
hospital_filter = request.GET.get('hospital')
hospital_filter = request.GET.get("hospital")
if 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:
queryset = queryset.filter(staff__department_id=department_filter)
year_filter = request.GET.get('year')
year_filter = request.GET.get("year")
if year_filter:
queryset = queryset.filter(year=int(year_filter))
month_filter = request.GET.get('month')
month_filter = request.GET.get("month")
if month_filter:
queryset = queryset.filter(month=int(month_filter))
# Search
search_query = request.GET.get('search')
search_query = request.GET.get("search")
if search_query:
queryset = queryset.filter(
Q(staff__first_name__icontains=search_query) |
Q(staff__last_name__icontains=search_query) |
Q(staff__license_number__icontains=search_query)
Q(staff__first_name__icontains=search_query)
| Q(staff__last_name__icontains=search_query)
| Q(staff__license_number__icontains=search_query)
)
# Ordering
order_by = request.GET.get('order_by', '-year,-month,-average_rating')
queryset = queryset.order_by(*order_by.split(','))
order_by = request.GET.get("order_by", "-year,-month,-average_rating")
queryset = queryset.order_by(*order_by.split(","))
# Pagination
page_size = int(request.GET.get('page_size', 25))
page_size = int(request.GET.get("page_size", 25))
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)
# 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 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 = {
'page_obj': page_obj,
'ratings': page_obj.object_list,
'hospitals': hospitals,
'departments': departments,
'years': years,
'filters': request.GET,
"page_obj": page_obj,
"ratings": page_obj.object_list,
"hospitals": hospitals,
"departments": departments,
"years": years,
"filters": request.GET,
}
return render(request, 'physicians/ratings_list.html', context)
return render(request, "physicians/ratings_list.html", context)
@login_required
@ -632,16 +405,14 @@ def specialization_overview(request):
"""
# 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')
year = int(request.GET.get("year", now.year))
month = int(request.GET.get("month", now.month))
hospital_filter = request.GET.get("hospital")
# Base 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')
queryset = PhysicianMonthlyRating.objects.filter(year=year, month=month, staff__physician=True).select_related(
"staff", "staff__hospital", "staff__department"
)
# Apply RBAC filters
user = request.user
@ -655,60 +426,57 @@ def specialization_overview(request):
# Aggregate by specialization
specialization_data = {}
for rating in queryset:
spec = rating.staff.specialization
if spec not in specialization_data:
specialization_data[spec] = {
'specialization': spec,
'physicians': [],
'total_physicians': 0,
'total_surveys': 0,
'total_positive': 0,
'total_neutral': 0,
'total_negative': 0,
'ratings_sum': 0,
"specialization": spec,
"physicians": [],
"total_physicians": 0,
"total_surveys": 0,
"total_positive": 0,
"total_neutral": 0,
"total_negative": 0,
"ratings_sum": 0,
}
specialization_data[spec]['physicians'].append(rating)
specialization_data[spec]['total_physicians'] += 1
specialization_data[spec]['total_surveys'] += rating.total_surveys
specialization_data[spec]['total_positive'] += rating.positive_count
specialization_data[spec]['total_neutral'] += rating.neutral_count
specialization_data[spec]['total_negative'] += rating.negative_count
specialization_data[spec]['ratings_sum'] += float(rating.average_rating)
specialization_data[spec]["physicians"].append(rating)
specialization_data[spec]["total_physicians"] += 1
specialization_data[spec]["total_surveys"] += rating.total_surveys
specialization_data[spec]["total_positive"] += rating.positive_count
specialization_data[spec]["total_neutral"] += rating.neutral_count
specialization_data[spec]["total_negative"] += rating.negative_count
specialization_data[spec]["ratings_sum"] += float(rating.average_rating)
# Calculate averages
specializations = []
for spec, data in specialization_data.items():
avg_rating = data['ratings_sum'] / data['total_physicians'] if data['total_physicians'] > 0 else 0
specializations.append({
'specialization': spec,
'total_physicians': data['total_physicians'],
'average_rating': round(avg_rating, 2),
'total_surveys': data['total_surveys'],
'positive_count': data['total_positive'],
'neutral_count': data['total_neutral'],
'negative_count': data['total_negative'],
'physicians': sorted(data['physicians'], key=lambda x: x.average_rating, reverse=True)
})
avg_rating = data["ratings_sum"] / data["total_physicians"] if data["total_physicians"] > 0 else 0
specializations.append(
{
"specialization": spec,
"total_physicians": data["total_physicians"],
"average_rating": round(avg_rating, 2),
"total_surveys": data["total_surveys"],
"positive_count": data["total_positive"],
"neutral_count": data["total_neutral"],
"negative_count": data["total_negative"],
"physicians": sorted(data["physicians"], key=lambda x: x.average_rating, reverse=True),
}
)
# 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 = {
'specializations': specializations,
'year': year,
'month': month,
'hospitals': hospitals,
'filters': request.GET,
"specializations": specializations,
"year": year,
"month": month,
"hospitals": hospitals,
"filters": request.GET,
}
return render(request, 'physicians/specialization_overview.html', context)
return render(request, "physicians/specialization_overview.html", context)
@login_required
@ -724,16 +492,14 @@ def department_overview(request):
"""
# 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')
year = int(request.GET.get("year", now.year))
month = int(request.GET.get("month", now.month))
hospital_filter = request.GET.get("hospital")
# Base 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')
queryset = PhysicianMonthlyRating.objects.filter(year=year, month=month, staff__physician=True).select_related(
"staff", "staff__hospital", "staff__department"
)
# Apply RBAC filters
user = request.user
@ -754,53 +520,51 @@ def department_overview(request):
dept_key = str(dept.id)
if dept_key not in department_data:
department_data[dept_key] = {
'department': dept,
'physicians': [],
'total_physicians': 0,
'total_surveys': 0,
'total_positive': 0,
'total_neutral': 0,
'total_negative': 0,
'ratings_sum': 0,
"department": dept,
"physicians": [],
"total_physicians": 0,
"total_surveys": 0,
"total_positive": 0,
"total_neutral": 0,
"total_negative": 0,
"ratings_sum": 0,
}
department_data[dept_key]['physicians'].append(rating)
department_data[dept_key]['total_physicians'] += 1
department_data[dept_key]['total_surveys'] += rating.total_surveys
department_data[dept_key]['total_positive'] += rating.positive_count
department_data[dept_key]['total_neutral'] += rating.neutral_count
department_data[dept_key]['total_negative'] += rating.negative_count
department_data[dept_key]['ratings_sum'] += float(rating.average_rating)
department_data[dept_key]["physicians"].append(rating)
department_data[dept_key]["total_physicians"] += 1
department_data[dept_key]["total_surveys"] += rating.total_surveys
department_data[dept_key]["total_positive"] += rating.positive_count
department_data[dept_key]["total_neutral"] += rating.neutral_count
department_data[dept_key]["total_negative"] += rating.negative_count
department_data[dept_key]["ratings_sum"] += float(rating.average_rating)
# Calculate averages
departments = []
for dept_key, data in department_data.items():
avg_rating = data['ratings_sum'] / data['total_physicians'] if data['total_physicians'] > 0 else 0
departments.append({
'department': data['department'],
'total_physicians': data['total_physicians'],
'average_rating': round(avg_rating, 2),
'total_surveys': data['total_surveys'],
'positive_count': data['total_positive'],
'neutral_count': data['total_neutral'],
'negative_count': data['total_negative'],
'physicians': sorted(data['physicians'], key=lambda x: x.average_rating, reverse=True)
})
avg_rating = data["ratings_sum"] / data["total_physicians"] if data["total_physicians"] > 0 else 0
departments.append(
{
"department": data["department"],
"total_physicians": data["total_physicians"],
"average_rating": round(avg_rating, 2),
"total_surveys": data["total_surveys"],
"positive_count": data["total_positive"],
"neutral_count": data["total_neutral"],
"negative_count": data["total_negative"],
"physicians": sorted(data["physicians"], key=lambda x: x.average_rating, reverse=True),
}
)
# 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 = {
'departments': departments,
'year': year,
'month': month,
'hospitals': hospitals,
'filters': request.GET,
"departments": departments,
"year": year,
"month": month,
"hospitals": hospitals,
"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 = {
'page_obj': page_obj,
'projects': page_obj.object_list,
'hospitals': hospitals,
'stats': stats,
'filters': request.GET,
}

File diff suppressed because it is too large Load Diff