HH/apps/dashboard/views.py
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

2882 lines
118 KiB
Python

"""
Dashboard views - PX Command Center and analytics dashboards
"""
import json
from datetime import timedelta, datetime
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.paginator import Paginator
from django.db.models import Avg, Count, Q, Sum
from django.http import JsonResponse
from django.shortcuts import redirect, render, reverse
from django.utils import timezone
from django.views.generic import TemplateView
from django.utils.translation import gettext_lazy as _
from django.contrib import messages
class CommandCenterView(LoginRequiredMixin, TemplateView):
"""
PX Command Center Dashboard - Real-time control panel.
Shows:
- Red Alert Banner (urgent items requiring immediate attention)
- Top KPI cards (complaints, actions, surveys, etc.) with drill-down
- Charts (trends, satisfaction, leaderboards)
- Live feed (latest complaints, actions, events)
- Enhanced modules (Inquiries, Observations)
- Filters (date range, hospital, department)
Follows the "5-Second Rule": Critical signals dominant and comprehensible within 5 seconds.
Uses modular tile/card system with 30-60 second auto-refresh capability.
"""
template_name = "dashboard/command_center.html"
def dispatch(self, request, *args, **kwargs):
"""Check user type and redirect accordingly"""
if request.user.is_authenticated and request.user.is_source_user():
return redirect("px_sources:source_user_dashboard")
if request.user.is_authenticated and request.user.is_basic_staff():
if hasattr(request.user, "department") and request.user.department:
return redirect("organizations:department_detail", pk=request.user.department.pk)
if request.user.is_authenticated and request.user.is_champion() and request.user.department:
return redirect("organizations:department_detail", pk=request.user.department.pk)
if request.user.is_authenticated and request.user.is_department_manager() and request.user.department:
return redirect("organizations:department_detail", pk=request.user.department.pk)
if request.user.is_authenticated and request.user.is_px_admin() and not request.tenant_hospital:
return redirect("core:select_hospital")
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = self.request.user
# Import models
from apps.complaints.models import Complaint, Inquiry
from apps.px_action_center.models import PXAction
from apps.surveys.models import SurveyInstance
from apps.callcenter.models import CallCenterInteraction
from apps.integrations.models import InboundEvent
from apps.physicians.models import PhysicianMonthlyRating
from apps.organizations.models import Staff
from apps.observations.models import Observation
# Date filters
now = timezone.now()
last_24h = now - timedelta(hours=24)
last_7d = now - timedelta(days=7)
last_30d = now - timedelta(days=30)
last_60d = now - timedelta(days=60)
# Base querysets (filtered by user role and tenant_hospital)
if user.is_px_admin():
# PX Admins use their selected hospital from session
hospital = self.request.tenant_hospital
complaints_qs = Complaint.objects.filter(hospital=hospital) if hospital else Complaint.objects.none()
inquiries_qs = Inquiry.objects.filter(hospital=hospital) if hospital else Inquiry.objects.none()
actions_qs = PXAction.objects.filter(hospital=hospital) if hospital else PXAction.objects.none()
surveys_qs = (
SurveyInstance.objects.filter(hospital=hospital) if hospital else SurveyInstance.objects.none()
) # Filter by tenant hospital
calls_qs = (
CallCenterInteraction.objects.filter(hospital=hospital)
if hospital
else CallCenterInteraction.objects.none()
)
observations_qs = Observation.objects.filter(hospital=hospital) if hospital else Observation.objects.none()
elif user.is_hospital_admin() and user.hospital:
complaints_qs = Complaint.objects.filter(hospital=user.hospital)
inquiries_qs = Inquiry.objects.filter(hospital=user.hospital)
actions_qs = PXAction.objects.filter(hospital=user.hospital)
surveys_qs = SurveyInstance.objects.filter(hospital=user.hospital)
calls_qs = CallCenterInteraction.objects.filter(hospital=user.hospital)
observations_qs = Observation.objects.filter(hospital=user.hospital)
elif user.is_department_manager() and user.department:
complaints_qs = Complaint.objects.filter(department=user.department)
inquiries_qs = Inquiry.objects.filter(department=user.department)
actions_qs = PXAction.objects.filter(department=user.department)
surveys_qs = SurveyInstance.objects.filter(journey_instance__department=user.department)
calls_qs = CallCenterInteraction.objects.filter(department=user.department)
observations_qs = Observation.objects.filter(assigned_department=user.department)
elif user.is_champion() and user.department:
complaints_qs = Complaint.objects.filter(department=user.department)
inquiries_qs = Inquiry.objects.filter(
Q(department=user.department) | Q(outgoing_department=user.department)
)
actions_qs = PXAction.objects.filter(department=user.department)
surveys_qs = SurveyInstance.objects.none()
calls_qs = CallCenterInteraction.objects.none()
observations_qs = Observation.objects.filter(assigned_department=user.department)
elif user.is_director():
directed_depts = user.get_directed_departments()
if directed_depts.exists():
complaints_qs = Complaint.objects.filter(department__in=directed_depts)
inquiries_qs = Inquiry.objects.filter(department__in=directed_depts)
actions_qs = PXAction.objects.filter(department__in=directed_depts)
surveys_qs = SurveyInstance.objects.filter(journey_instance__department__in=directed_depts)
calls_qs = CallCenterInteraction.objects.filter(department__in=directed_depts)
observations_qs = Observation.objects.filter(assigned_department__in=directed_depts)
else:
complaints_qs = Complaint.objects.none()
inquiries_qs = Inquiry.objects.none()
actions_qs = PXAction.objects.none()
surveys_qs = SurveyInstance.objects.none()
calls_qs = CallCenterInteraction.objects.none()
observations_qs = Observation.objects.none()
else:
complaints_qs = Complaint.objects.none()
inquiries_qs = Inquiry.objects.none()
actions_qs = PXAction.objects.none()
surveys_qs = SurveyInstance.objects.none()
calls_qs = CallCenterInteraction.objects.none()
observations_qs = Observation.objects.none()
# ========================================
# RED ALERT ITEMS (5-Second Rule)
# ========================================
red_alerts = []
# Critical complaints
critical_complaints = complaints_qs.filter(severity="critical", status__in=["open", "in_progress"]).count()
if critical_complaints > 0:
red_alerts.append(
{
"type": "critical_complaints",
"label": _("Critical Complaints"),
"value": critical_complaints,
"icon": "alert-octagon",
"color": "red",
"url": f"{reverse('complaints:complaint_list')}?severity=critical&status=open,in_progress",
"priority": 1,
}
)
# Overdue complaints
overdue_complaints = complaints_qs.filter(is_overdue=True, status__in=["open", "in_progress"]).count()
if overdue_complaints > 0:
red_alerts.append(
{
"type": "overdue_complaints",
"label": _("Overdue Complaints"),
"value": overdue_complaints,
"icon": "clock",
"color": "orange",
"url": f"{reverse('complaints:complaint_list')}?is_overdue=true&status=open,in_progress",
"priority": 2,
}
)
# Escalated actions
escalated_actions = actions_qs.filter(escalation_level__gt=0, status__in=["open", "in_progress"]).count()
if escalated_actions > 0:
red_alerts.append(
{
"type": "escalated_actions",
"label": _("Escalated Actions"),
"value": escalated_actions,
"icon": "arrow-up-circle",
"color": "red",
"url": reverse("actions:action_list"),
"priority": 3,
}
)
# Negative surveys in last 24h
negative_surveys_24h = surveys_qs.filter(is_negative=True, completed_at__gte=last_24h).count()
if negative_surveys_24h > 0:
red_alerts.append(
{
"type": "negative_surveys",
"label": _("Negative Surveys (24h)"),
"value": negative_surveys_24h,
"icon": "frown",
"color": "yellow",
"url": reverse("surveys:instance_list"),
"priority": 4,
}
)
# Sort by priority
red_alerts.sort(key=lambda x: x["priority"])
context["red_alerts"] = red_alerts
context["has_red_alerts"] = len(red_alerts) > 0
# ========================================
# COMPLAINTS MODULE DATA
# ========================================
complaints_current = complaints_qs.filter(created_at__gte=last_30d).count()
complaints_previous = complaints_qs.filter(created_at__gte=last_60d, created_at__lt=last_30d).count()
complaints_variance = 0
if complaints_previous > 0:
complaints_variance = round(((complaints_current - complaints_previous) / complaints_previous) * 100, 1)
# Resolution time calculation
resolved_complaints = complaints_qs.filter(status="closed", closed_at__isnull=False, created_at__gte=last_30d)
avg_resolution_hours = 0
if resolved_complaints.exists():
total_hours = sum((c.closed_at - c.created_at).total_seconds() / 3600 for c in resolved_complaints)
avg_resolution_hours = round(total_hours / resolved_complaints.count(), 1)
# Complaints by severity for donut chart
complaints_by_severity = {
"critical": complaints_qs.filter(severity="critical", status__in=["open", "in_progress"]).count(),
"high": complaints_qs.filter(severity="high", status__in=["open", "in_progress"]).count(),
"medium": complaints_qs.filter(severity="medium", status__in=["open", "in_progress"]).count(),
"low": complaints_qs.filter(severity="low", status__in=["open", "in_progress"]).count(),
}
# Complaints by department for heatmap
complaints_by_department = list(
complaints_qs.filter(status__in=["open", "in_progress"], department__isnull=False)
.values("department__name")
.annotate(count=Count("id"))
.order_by("-count")[:10]
)
context["complaints_module"] = {
"total_active": complaints_qs.filter(status__in=["open", "in_progress"]).count(),
"current_period": complaints_current,
"previous_period": complaints_previous,
"variance": complaints_variance,
"variance_direction": "up" if complaints_variance > 0 else "down" if complaints_variance < 0 else "neutral",
"avg_resolution_hours": avg_resolution_hours,
"overdue": complaints_qs.filter(is_overdue=True, status__in=["open", "in_progress"]).count(),
"by_severity": complaints_by_severity,
"by_department": complaints_by_department,
"critical_new": complaints_qs.filter(severity="critical", created_at__gte=last_24h).count(),
}
# ========================================
# SURVEY INSIGHTS MODULE DATA
# ========================================
surveys_completed_30d = surveys_qs.filter(completed_at__gte=last_30d)
total_surveys_30d = surveys_completed_30d.count()
# Calculate average satisfaction
avg_satisfaction = (
surveys_completed_30d.filter(total_score__isnull=False).aggregate(Avg("total_score"))["total_score__avg"]
or 0
)
# NPS-style calculation (promoters - detractors)
positive_count = surveys_completed_30d.filter(is_negative=False).count()
negative_count = surveys_completed_30d.filter(is_negative=True).count()
nps_score = 0
if total_surveys_30d > 0:
nps_score = round(((positive_count - negative_count) / total_surveys_30d) * 100)
# Response rate (completed vs sent in last 30 days)
surveys_sent_30d = surveys_qs.filter(sent_at__gte=last_30d).count()
response_rate = 0
if surveys_sent_30d > 0:
response_rate = round((total_surveys_30d / surveys_sent_30d) * 100, 1)
context["survey_module"] = {
"avg_satisfaction": round(avg_satisfaction, 1),
"nps_score": nps_score,
"response_rate": response_rate,
"total_completed": total_surveys_30d,
"positive_count": positive_count,
"negative_count": negative_count,
"neutral_count": total_surveys_30d - positive_count - negative_count,
"negative_24h": surveys_qs.filter(is_negative=True, completed_at__gte=last_24h).count(),
}
# ========================================
# PX ACTIONS MODULE DATA
# ========================================
actions_open = actions_qs.filter(status="open").count()
actions_in_progress = actions_qs.filter(status="in_progress").count()
actions_pending_approval = actions_qs.filter(status="pending_approval").count()
actions_closed_30d = actions_qs.filter(status="closed", closed_at__gte=last_30d).count()
# Time to close calculation
closed_actions = actions_qs.filter(status="closed", closed_at__isnull=False, created_at__gte=last_30d)
avg_time_to_close_hours = 0
if closed_actions.exists():
total_hours = sum((a.closed_at - a.created_at).total_seconds() / 3600 for a in closed_actions)
avg_time_to_close_hours = round(total_hours / closed_actions.count(), 1)
# Actions by source for breakdown
actions_by_source = list(
actions_qs.filter(status__in=["open", "in_progress"])
.values("source_type")
.annotate(count=Count("id"))
.order_by("-count")
)
context["actions_module"] = {
"open": actions_open,
"in_progress": actions_in_progress,
"pending_approval": actions_pending_approval,
"overdue": actions_qs.filter(is_overdue=True, status__in=["open", "in_progress"]).count(),
"escalated": actions_qs.filter(escalation_level__gt=0, status__in=["open", "in_progress"]).count(),
"closed_30d": actions_closed_30d,
"avg_time_to_close_hours": avg_time_to_close_hours,
"by_source": actions_by_source,
"new_today": actions_qs.filter(created_at__gte=last_24h).count(),
}
# ========================================
# INQUIRIES MODULE DATA
# ========================================
inquiries_open = inquiries_qs.filter(status="open").count()
inquiries_in_progress = inquiries_qs.filter(status="in_progress").count()
inquiries_resolved_30d = inquiries_qs.filter(status="resolved", updated_at__gte=last_30d).count()
context["inquiries_module"] = {
"open": inquiries_open,
"in_progress": inquiries_in_progress,
"total_active": inquiries_open + inquiries_in_progress,
"resolved_30d": inquiries_resolved_30d,
"new_24h": inquiries_qs.filter(created_at__gte=last_24h).count(),
}
# ========================================
# OBSERVATIONS MODULE DATA
# ========================================
observations_new = observations_qs.filter(status="new").count()
observations_in_progress = observations_qs.filter(status="in_progress").count()
observations_critical = observations_qs.filter(
severity="critical", status__in=["new", "triaged", "assigned", "in_progress"]
).count()
# Observations by category
observations_by_category = list(
observations_qs.filter(status__in=["new", "triaged", "assigned", "in_progress"])
.values("category__name_en")
.annotate(count=Count("id"))
.order_by("-count")[:5]
)
context["observations_module"] = {
"new": observations_new,
"in_progress": observations_in_progress,
"critical": observations_critical,
"total_active": observations_new + observations_in_progress,
"resolved_30d": observations_qs.filter(status="resolved", resolved_at__gte=last_30d).count(),
"by_category": observations_by_category,
}
# ========================================
# COMMUNICATION/CALL CENTER MODULE DATA
# ========================================
calls_7d = calls_qs.filter(call_started_at__gte=last_7d)
total_calls = calls_7d.count()
low_rating_calls = calls_7d.filter(is_low_rating=True).count()
context["calls_module"] = {
"total_7d": total_calls,
"low_ratings": low_rating_calls,
"satisfaction_rate": round(((total_calls - low_rating_calls) / total_calls * 100), 1)
if total_calls > 0
else 0,
}
# ========================================
# LEGACY STATS (for backward compatibility)
# ========================================
context["stats"] = {
"total_complaints": context["complaints_module"]["total_active"],
"avg_resolution_time": f"{avg_resolution_hours}h",
"satisfaction_score": round(avg_satisfaction, 0),
"active_actions": context["actions_module"]["open"] + context["actions_module"]["in_progress"],
"new_today": context["actions_module"]["new_today"],
}
# ========================================
# LATEST ITEMS FOR LIVE FEED
# ========================================
# Latest high severity complaints
context["latest_complaints"] = (
complaints_qs.filter(severity__in=["high", "critical"])
.select_related("patient", "hospital", "department")
.order_by("-created_at")[:5]
)
# Latest escalated actions
context["latest_actions"] = (
actions_qs.filter(escalation_level__gt=0)
.select_related("hospital", "assigned_to")
.order_by("-escalated_at")[:5]
)
# Latest inquiries
context["latest_inquiries"] = (
inquiries_qs.filter(status__in=["open", "in_progress"])
.select_related("patient", "hospital", "department")
.order_by("-created_at")[:5]
)
# Latest observations
context["latest_observations"] = (
observations_qs.filter(status__in=["new", "triaged", "assigned"])
.select_related("hospital", "category")
.order_by("-created_at")[:5]
)
# Latest integration events
context["latest_events"] = (
InboundEvent.objects.filter(status="processed").select_related().order_by("-processed_at")[:10]
)
# ========================================
# PHYSICIAN LEADERBOARD
# ========================================
current_month_ratings = PhysicianMonthlyRating.objects.filter(year=now.year, month=now.month).select_related(
"staff", "staff__hospital", "staff__department"
)
# Filter by user role
if user.is_hospital_admin() and user.hospital:
current_month_ratings = current_month_ratings.filter(staff__hospital=user.hospital)
elif user.is_department_manager() and user.department:
current_month_ratings = current_month_ratings.filter(staff__department=user.department)
elif user.is_director():
directed_depts = user.get_directed_departments()
if directed_depts.exists():
current_month_ratings = current_month_ratings.filter(staff__department__in=directed_depts)
else:
current_month_ratings = current_month_ratings.none()
# Top 5 staff this month
context["top_physicians"] = current_month_ratings.order_by("-average_rating")[:5]
# Staff stats
physician_stats = current_month_ratings.aggregate(
total_physicians=Count("id"), avg_rating=Avg("average_rating"), total_surveys=Count("total_surveys")
)
context["physician_stats"] = physician_stats
# ========================================
# CHART DATA
# ========================================
context["chart_data"] = {
"complaints_trend": json.dumps(self.get_complaints_trend(complaints_qs, last_30d)),
"complaints_by_severity": json.dumps(complaints_by_severity),
"survey_satisfaction": avg_satisfaction,
"nps_trend": json.dumps(self.get_nps_trend(surveys_qs, last_30d)),
"actions_funnel": json.dumps(
{
"open": actions_open,
"in_progress": actions_in_progress,
"pending_approval": actions_pending_approval,
"closed": actions_closed_30d,
}
),
}
# Add hospital context
context["current_hospital"] = self.request.tenant_hospital
context["is_px_admin"] = user.is_px_admin()
# Last updated timestamp
context["last_updated"] = now.strftime("%Y-%m-%d %H:%M:%S")
return context
def get_complaints_trend(self, queryset, start_date):
"""Get complaints trend data for chart"""
# Group by day for last 30 days
data = []
for i in range(30):
date = start_date + timedelta(days=i)
count = queryset.filter(created_at__date=date.date()).count()
data.append({"date": date.strftime("%Y-%m-%d"), "count": count})
return data
def get_nps_trend(self, queryset, start_date):
"""Get NPS trend data for chart"""
data = []
for i in range(30):
date = start_date + timedelta(days=i)
day_surveys = queryset.filter(completed_at__date=date.date())
total = day_surveys.count()
if total > 0:
positive = day_surveys.filter(is_negative=False).count()
negative = day_surveys.filter(is_negative=True).count()
nps = round(((positive - negative) / total) * 100)
else:
nps = 0
data.append({"date": date.strftime("%Y-%m-%d"), "nps": nps})
return data
def get_survey_satisfaction(self, queryset, start_date):
"""Get survey satisfaction averages"""
return (
queryset.filter(completed_at__gte=start_date, total_score__isnull=False).aggregate(Avg("total_score"))[
"total_score__avg"
]
or 0
)
@login_required
def my_dashboard(request):
"""
My Dashboard - Personal view of all assigned items.
Shows:
- Summary cards with statistics
- Tabbed interface for 6 model types:
* Complaints
* Inquiries
* Observations
* PX Actions
* Tasks (QI Project Tasks)
* Feedback
- Date range filtering
- Search and filter controls
- Export functionality (CSV/Excel)
- Bulk actions support
- Charts showing trends
"""
# Redirect Source Users to their dashboard
if request.user.is_source_user():
return redirect("px_sources:source_user_dashboard")
user = request.user
# Get selected hospital for PX Admins (from middleware)
selected_hospital = getattr(request, "tenant_hospital", None)
# Get date range filter
date_range_days = int(request.GET.get("date_range", 30))
if date_range_days == -1: # All time
start_date = None
else:
start_date = timezone.now() - timedelta(days=date_range_days)
# Get active tab
active_tab = request.GET.get("tab", "complaints")
# Get search query
search_query = request.GET.get("search", "")
# Get status filter
status_filter = request.GET.get("status", "")
# Get priority/severity filter
priority_filter = request.GET.get("priority", "")
# Build querysets for all models
querysets = {}
# 1. Complaints
from apps.complaints.models import Complaint
complaints_qs = Complaint.objects.filter(assigned_to=user)
# Filter by selected hospital for PX Admins
if selected_hospital:
complaints_qs = complaints_qs.filter(hospital=selected_hospital)
if start_date:
complaints_qs = complaints_qs.filter(created_at__gte=start_date)
if search_query:
complaints_qs = complaints_qs.filter(Q(title__icontains=search_query) | Q(description__icontains=search_query))
if status_filter:
complaints_qs = complaints_qs.filter(status=status_filter)
if priority_filter:
complaints_qs = complaints_qs.filter(severity=priority_filter)
querysets["complaints"] = complaints_qs.select_related(
"patient", "hospital", "department", "source", "created_by"
).order_by("-created_at")
# 2. Inquiries
from apps.complaints.models import Inquiry
inquiries_qs = Inquiry.objects.filter(assigned_to=user)
# Filter by selected hospital for PX Admins
if selected_hospital:
inquiries_qs = inquiries_qs.filter(hospital=selected_hospital)
if start_date:
inquiries_qs = inquiries_qs.filter(created_at__gte=start_date)
if search_query:
inquiries_qs = inquiries_qs.filter(Q(subject__icontains=search_query) | Q(message__icontains=search_query))
if status_filter:
inquiries_qs = inquiries_qs.filter(status=status_filter)
querysets["inquiries"] = inquiries_qs.select_related("patient", "hospital", "department").order_by("-created_at")
# 3. Observations
from apps.observations.models import Observation
observations_qs = Observation.objects.filter(assigned_to=user)
# Filter by selected hospital for PX Admins
if selected_hospital:
observations_qs = observations_qs.filter(hospital=selected_hospital)
if start_date:
observations_qs = observations_qs.filter(created_at__gte=start_date)
if search_query:
observations_qs = observations_qs.filter(
Q(title__icontains=search_query) | Q(description__icontains=search_query)
)
if status_filter:
observations_qs = observations_qs.filter(status=status_filter)
if priority_filter:
observations_qs = observations_qs.filter(severity=priority_filter)
querysets["observations"] = observations_qs.select_related("hospital", "assigned_department").order_by(
"-created_at"
)
# 4. PX Actions
from apps.px_action_center.models import PXAction
actions_qs = PXAction.objects.filter(assigned_to=user)
# Filter by selected hospital for PX Admins
if selected_hospital:
actions_qs = actions_qs.filter(hospital=selected_hospital)
if start_date:
actions_qs = actions_qs.filter(created_at__gte=start_date)
if search_query:
actions_qs = actions_qs.filter(Q(title__icontains=search_query) | Q(description__icontains=search_query))
if status_filter:
actions_qs = actions_qs.filter(status=status_filter)
if priority_filter:
actions_qs = actions_qs.filter(severity=priority_filter)
querysets["actions"] = actions_qs.select_related("hospital", "department", "approved_by").order_by("-created_at")
# 5. QI Project Tasks
from apps.projects.models import QIProjectTask
tasks_qs = QIProjectTask.objects.filter(assigned_to=user)
# Filter by selected hospital for PX Admins (via project)
if selected_hospital:
tasks_qs = tasks_qs.filter(project__hospital=selected_hospital)
if start_date:
tasks_qs = tasks_qs.filter(created_at__gte=start_date)
if search_query:
tasks_qs = tasks_qs.filter(Q(title__icontains=search_query) | Q(description__icontains=search_query))
if status_filter:
tasks_qs = tasks_qs.filter(status=status_filter)
querysets["tasks"] = tasks_qs.select_related("project").order_by("-created_at")
# 6. Feedback
from apps.feedback.models import Feedback
feedback_qs = Feedback.objects.filter(assigned_to=user)
# Filter by selected hospital for PX Admins
if selected_hospital:
feedback_qs = feedback_qs.filter(hospital=selected_hospital)
if start_date:
feedback_qs = feedback_qs.filter(created_at__gte=start_date)
if search_query:
feedback_qs = feedback_qs.filter(Q(title__icontains=search_query) | Q(message__icontains=search_query))
if status_filter:
feedback_qs = feedback_qs.filter(status=status_filter)
if priority_filter:
feedback_qs = feedback_qs.filter(priority=priority_filter)
querysets["feedback"] = feedback_qs.select_related("hospital", "department", "patient").order_by("-created_at")
# Calculate statistics
stats = {}
total_stats = {"total": 0, "open": 0, "in_progress": 0, "resolved": 0, "closed": 0, "overdue": 0}
# Complaints stats
complaints_open = querysets["complaints"].filter(status="open").count()
complaints_in_progress = querysets["complaints"].filter(status="in_progress").count()
complaints_resolved = querysets["complaints"].filter(status="resolved").count()
complaints_closed = querysets["complaints"].filter(status="closed").count()
complaints_overdue = querysets["complaints"].filter(is_overdue=True).count()
stats["complaints"] = {
"total": querysets["complaints"].count(),
"open": complaints_open,
"in_progress": complaints_in_progress,
"resolved": complaints_resolved,
"closed": complaints_closed,
"overdue": complaints_overdue,
}
total_stats["total"] += stats["complaints"]["total"]
total_stats["open"] += complaints_open
total_stats["in_progress"] += complaints_in_progress
total_stats["resolved"] += complaints_resolved
total_stats["closed"] += complaints_closed
total_stats["overdue"] += complaints_overdue
# Inquiries stats
inquiries_open = querysets["inquiries"].filter(status="open").count()
inquiries_in_progress = querysets["inquiries"].filter(status="in_progress").count()
inquiries_resolved = querysets["inquiries"].filter(status="resolved").count()
inquiries_closed = querysets["inquiries"].filter(status="closed").count()
stats["inquiries"] = {
"total": querysets["inquiries"].count(),
"open": inquiries_open,
"in_progress": inquiries_in_progress,
"resolved": inquiries_resolved,
"closed": inquiries_closed,
"overdue": 0,
}
total_stats["total"] += stats["inquiries"]["total"]
total_stats["open"] += inquiries_open
total_stats["in_progress"] += inquiries_in_progress
total_stats["resolved"] += inquiries_resolved
total_stats["closed"] += inquiries_closed
# Observations stats
observations_open = querysets["observations"].filter(status="open").count()
observations_in_progress = querysets["observations"].filter(status="in_progress").count()
observations_closed = querysets["observations"].filter(status="closed").count()
# Observations don't have is_overdue field - set to 0
observations_overdue = 0
stats["observations"] = {
"total": querysets["observations"].count(),
"open": observations_open,
"in_progress": observations_in_progress,
"resolved": 0,
"closed": observations_closed,
"overdue": observations_overdue,
}
total_stats["total"] += stats["observations"]["total"]
total_stats["open"] += observations_open
total_stats["in_progress"] += observations_in_progress
total_stats["closed"] += observations_closed
total_stats["overdue"] += observations_overdue
# PX Actions stats
actions_open = querysets["actions"].filter(status="open").count()
actions_in_progress = querysets["actions"].filter(status="in_progress").count()
actions_closed = querysets["actions"].filter(status="closed").count()
actions_overdue = querysets["actions"].filter(is_overdue=True).count()
stats["actions"] = {
"total": querysets["actions"].count(),
"open": actions_open,
"in_progress": actions_in_progress,
"resolved": 0,
"closed": actions_closed,
"overdue": actions_overdue,
}
total_stats["total"] += stats["actions"]["total"]
total_stats["open"] += actions_open
total_stats["in_progress"] += actions_in_progress
total_stats["closed"] += actions_closed
total_stats["overdue"] += actions_overdue
# Tasks stats
tasks_open = querysets["tasks"].filter(status="open").count()
tasks_in_progress = querysets["tasks"].filter(status="in_progress").count()
tasks_closed = querysets["tasks"].filter(status="closed").count()
stats["tasks"] = {
"total": querysets["tasks"].count(),
"open": tasks_open,
"in_progress": tasks_in_progress,
"resolved": 0,
"closed": tasks_closed,
"overdue": 0,
}
total_stats["total"] += stats["tasks"]["total"]
total_stats["open"] += tasks_open
total_stats["in_progress"] += tasks_in_progress
total_stats["closed"] += tasks_closed
# Feedback stats
feedback_open = querysets["feedback"].filter(status="submitted").count()
feedback_in_progress = querysets["feedback"].filter(status="reviewed").count()
feedback_acknowledged = querysets["feedback"].filter(status="acknowledged").count()
feedback_closed = querysets["feedback"].filter(status="closed").count()
stats["feedback"] = {
"total": querysets["feedback"].count(),
"open": feedback_open,
"in_progress": feedback_in_progress,
"resolved": feedback_acknowledged,
"closed": feedback_closed,
"overdue": 0,
}
total_stats["total"] += stats["feedback"]["total"]
total_stats["open"] += feedback_open
total_stats["in_progress"] += feedback_in_progress
total_stats["resolved"] += feedback_acknowledged
total_stats["closed"] += feedback_closed
# Paginate all querysets
page_size = int(request.GET.get("page_size", 25))
paginated_data = {}
for tab_name, qs in querysets.items():
paginator = Paginator(qs, page_size)
page_number = request.GET.get(f"page_{tab_name}", 1)
paginated_data[tab_name] = paginator.get_page(page_number)
# Get chart data
chart_data = get_dashboard_chart_data(user, start_date, selected_hospital)
from apps.accounts.models import StaffActivityLog
activity_logs = StaffActivityLog.objects.filter(user=user).select_related("content_type")
if start_date:
activity_logs = activity_logs.filter(created_at__gte=start_date)
activity_logs = activity_logs[:50]
context = {
"stats": stats,
"total_stats": total_stats,
"paginated_data": paginated_data,
"active_tab": active_tab,
"date_range": date_range_days,
"search_query": search_query,
"status_filter": status_filter,
"priority_filter": priority_filter,
"chart_data": chart_data,
"selected_hospital": selected_hospital,
"activity_logs": activity_logs,
}
return render(request, "dashboard/my_dashboard.html", context)
def get_dashboard_chart_data(user, start_date=None, selected_hospital=None):
"""
Get chart data for dashboard trends.
Returns JSON-serializable data for ApexCharts.
"""
from apps.complaints.models import Complaint
from apps.px_action_center.models import PXAction
from apps.observations.models import Observation
from apps.feedback.models import Feedback
from apps.complaints.models import Inquiry
from apps.projects.models import QIProjectTask
# Default to last 30 days if no start_date
if not start_date:
start_date = timezone.now() - timedelta(days=30)
# Get completion trends
completion_data = []
labels = []
# Group by day for last 30 days
for i in range(30):
date = start_date + timedelta(days=i)
date_str = date.strftime("%Y-%m-%d")
labels.append(date.strftime("%b %d"))
completed_count = 0
# Check each model for completions on this date
# Apply hospital filter for PX Admins
complaint_qs = Complaint.objects.filter(assigned_to=user, status="closed", closed_at__date=date.date())
if selected_hospital:
complaint_qs = complaint_qs.filter(hospital=selected_hospital)
completed_count += complaint_qs.count()
inquiry_qs = Inquiry.objects.filter(assigned_to=user, status="closed", updated_at__date=date.date())
if selected_hospital:
inquiry_qs = inquiry_qs.filter(hospital=selected_hospital)
completed_count += inquiry_qs.count()
observation_qs = Observation.objects.filter(assigned_to=user, status="closed", updated_at__date=date.date())
if selected_hospital:
observation_qs = observation_qs.filter(hospital=selected_hospital)
completed_count += observation_qs.count()
action_qs = PXAction.objects.filter(assigned_to=user, status="closed", closed_at__date=date.date())
if selected_hospital:
action_qs = action_qs.filter(hospital=selected_hospital)
completed_count += action_qs.count()
task_qs = QIProjectTask.objects.filter(assigned_to=user, status="closed", completed_date=date.date())
if selected_hospital:
task_qs = task_qs.filter(project__hospital=selected_hospital)
completed_count += task_qs.count()
feedback_qs = Feedback.objects.filter(assigned_to=user, status="closed", closed_at__date=date.date())
if selected_hospital:
feedback_qs = feedback_qs.filter(hospital=selected_hospital)
completed_count += feedback_qs.count()
completion_data.append(completed_count)
return {"completion_trend": {"labels": labels, "data": completion_data}}
@login_required
def dashboard_bulk_action(request):
"""
Handle bulk actions on dashboard items.
Supported actions:
- bulk_assign: Assign to user
- bulk_status: Change status
"""
if request.method != "POST":
return JsonResponse({"success": False, "error": "POST required"}, status=405)
import json
try:
data = json.loads(request.body)
action = data.get("action")
tab_name = data.get("tab")
item_ids = data.get("item_ids", [])
if not action or not tab_name:
return JsonResponse({"success": False, "error": "Missing required fields"}, status=400)
# Route to appropriate handler based on tab
if tab_name == "complaints":
from apps.complaints.models import Complaint
queryset = Complaint.objects.filter(id__in=item_ids, assigned_to=request.user)
elif tab_name == "inquiries":
from apps.complaints.models import Inquiry
queryset = Inquiry.objects.filter(id__in=item_ids, assigned_to=request.user)
elif tab_name == "observations":
from apps.observations.models import Observation
queryset = Observation.objects.filter(id__in=item_ids, assigned_to=request.user)
elif tab_name == "actions":
from apps.px_action_center.models import PXAction
queryset = PXAction.objects.filter(id__in=item_ids, assigned_to=request.user)
elif tab_name == "tasks":
from apps.projects.models import QIProjectTask
queryset = QIProjectTask.objects.filter(id__in=item_ids, assigned_to=request.user)
elif tab_name == "feedback":
from apps.feedback.models import Feedback
queryset = Feedback.objects.filter(id__in=item_ids, assigned_to=request.user)
else:
return JsonResponse({"success": False, "error": "Invalid tab"}, status=400)
# Apply bulk action
if action == "bulk_status":
new_status = data.get("new_status")
if not new_status:
return JsonResponse({"success": False, "error": "Missing new_status"}, status=400)
count = queryset.update(status=new_status)
return JsonResponse({"success": True, "updated_count": count})
elif action == "bulk_assign":
user_id = data.get("user_id")
if not user_id:
return JsonResponse({"success": False, "error": "Missing user_id"}, status=400)
from apps.accounts.models import User
assignee = User.objects.get(id=user_id)
count = queryset.update(assigned_to=assignee, assigned_at=timezone.now())
return JsonResponse({"success": True, "updated_count": count})
else:
return JsonResponse({"success": False, "error": "Invalid action"}, status=400)
except json.JSONDecodeError:
return JsonResponse({"success": False, "error": "Invalid JSON"}, status=400)
except Exception as e:
return JsonResponse({"success": False, "error": str(e)}, status=500)
@login_required
def admin_evaluation(request):
"""
Admin Evaluation Dashboard - Staff performance analysis.
Shows:
- Performance metrics for all staff members
- Complaints: Source breakdown, status distribution, response time, activation time
- Inquiries: Status distribution, response time
- Multi-staff comparison
- Date range filtering
- Hospital/department filtering
Access: PX Admin and Hospital Admin only
"""
from django.core.exceptions import PermissionDenied
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
from apps.accounts.models import User
from apps.organizations.models import Hospital, Department
user = request.user
# Only PX Admins and Hospital Admins can access
if not (user.is_px_admin() or user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can access the Admin Evaluation dashboard.")
# Get date range filter
date_range = request.GET.get("date_range", "30d")
custom_start = request.GET.get("custom_start")
custom_end = request.GET.get("custom_end")
# Parse custom dates if provided
if custom_start:
from datetime import datetime
custom_start = datetime.fromisoformat(custom_start)
if custom_end:
from datetime import datetime
custom_end = datetime.fromisoformat(custom_end)
# Get hospital and department filters
hospital_id = request.GET.get("hospital_id")
department_id = request.GET.get("department_id")
# Get selected staff IDs for comparison
selected_staff_ids = request.GET.getlist("staff_ids")
status_filter = request.GET.get("status", "active")
# Get available hospitals (for PX Admins)
if user.is_px_admin():
hospitals = Hospital.objects.filter(status="active")
elif user.is_hospital_admin() and user.hospital:
hospitals = Hospital.objects.filter(id=user.hospital.id)
hospital_id = hospital_id or user.hospital.id # Default to user's hospital
else:
hospitals = Hospital.objects.none()
# Get available departments based on hospital filter
if hospital_id:
departments = Department.objects.filter(hospital_id=hospital_id, status="active")
elif user.hospital:
departments = Department.objects.filter(hospital=user.hospital, status="active")
else:
departments = Department.objects.none()
# Get staff performance metrics
performance_data = UnifiedAnalyticsService.get_staff_performance_metrics(
user=user,
date_range=date_range,
hospital_id=hospital_id,
department_id=department_id,
staff_ids=selected_staff_ids if selected_staff_ids else None,
custom_start=custom_start,
custom_end=custom_end,
)
# Get all staff for the dropdown
staff_queryset = User.objects.filter(groups__name="PX Employee")
if user.is_px_admin():
# PX Admins MUST filter by tenant_hospital
if request.tenant_hospital:
staff_queryset = staff_queryset.filter(hospital=request.tenant_hospital)
hospital_id = hospital_id or request.tenant_hospital.id
else:
staff_queryset = User.objects.none() # No access without selected hospital
elif user.is_hospital_admin() and user.hospital:
staff_queryset = staff_queryset.filter(hospital=user.hospital)
hospital_id = hospital_id or user.hospital.id
if department_id:
staff_queryset = staff_queryset.filter(department_id=department_id)
if status_filter == "active":
staff_queryset = staff_queryset.filter(is_active=True)
elif status_filter == "inactive":
staff_queryset = staff_queryset.filter(is_active=False)
# Only staff with assigned complaints or inquiries
staff_queryset = (
staff_queryset.filter(Q(assigned_complaints__isnull=False) | Q(assigned_inquiries__isnull=False))
.distinct()
.select_related("hospital", "department")
)
context = {
"departments": departments,
"staff_list": staff_queryset,
"selected_hospital_id": hospital_id,
"selected_department_id": department_id,
"selected_staff_ids": selected_staff_ids,
"date_range": date_range,
"custom_start": custom_start,
"custom_end": custom_end,
"performance_data": performance_data,
"status_filter": status_filter,
}
return render(request, "dashboard/admin_evaluation.html", context)
@login_required
def admin_evaluation_chart_data(request):
"""
API endpoint to get chart data for admin evaluation dashboard.
Access: PX Admin and Hospital Admin only
"""
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
if request.method != "GET":
return JsonResponse({"success": False, "error": "GET required"}, status=405)
user = request.user
# Only PX Admins and Hospital Admins can access
if not (user.is_px_admin() or user.is_hospital_admin()):
return JsonResponse({"success": False, "error": "Permission denied"}, status=403)
chart_type = request.GET.get("chart_type")
date_range = request.GET.get("date_range", "30d")
hospital_id = request.GET.get("hospital_id")
department_id = request.GET.get("department_id")
staff_ids = request.GET.getlist("staff_ids")
# For PX Admins without explicit hospital_id, use tenant_hospital
if user.is_px_admin() and not hospital_id and request.tenant_hospital:
hospital_id = str(request.tenant_hospital.id)
# Parse custom dates if provided
custom_start = request.GET.get("custom_start")
custom_end = request.GET.get("custom_end")
if custom_start:
from datetime import datetime
custom_start = datetime.fromisoformat(custom_start)
if custom_end:
from datetime import datetime
custom_end = datetime.fromisoformat(custom_end)
try:
if chart_type == "staff_performance":
data = UnifiedAnalyticsService.get_staff_performance_metrics(
user=user,
date_range=date_range,
hospital_id=hospital_id,
department_id=department_id,
staff_ids=staff_ids if staff_ids else None,
custom_start=custom_start,
custom_end=custom_end,
)
else:
data = {"error": f"Unknown chart type: {chart_type}"}
return JsonResponse({"success": True, "data": data})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)}, status=500)
# ============================================================================
# ENHANCED ADMIN EVALUATION VIEWS
# ============================================================================
@login_required
def staff_performance_detail(request, staff_id):
"""
Detailed performance view for a single staff member.
Shows:
- Performance score with breakdown
- Daily workload trends
- Recent complaints and inquiries
- Performance metrics
Access: PX Admin and Hospital Admin only
"""
from django.core.exceptions import PermissionDenied
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
# Only PX Admins and Hospital Admins can access
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can access staff performance details.")
from apps.accounts.models import User
user = request.user
# Get date range
date_range = request.GET.get("date_range", "30d")
try:
staff = User.objects.select_related("hospital", "department").get(id=staff_id)
# Check permissions
if not user.is_px_admin():
if user.hospital and staff.hospital != user.hospital:
messages.error(request, "You don't have permission to view this staff member's performance.")
return redirect("dashboard:admin_evaluation")
# Get detailed performance
performance = UnifiedAnalyticsService.get_staff_detailed_performance(
staff_id=staff_id, user=user, date_range=date_range
)
# Get trends
trends = UnifiedAnalyticsService.get_staff_performance_trends(staff_id=staff_id, user=user, months=6)
context = {
"staff": performance["staff"],
"performance": performance,
"trends": trends,
"date_range": date_range,
}
return render(request, "dashboard/staff_performance_detail.html", context)
except User.DoesNotExist:
messages.error(request, "Staff member not found.")
return redirect("dashboard:admin_evaluation")
except PermissionError:
messages.error(request, "You don't have permission to view this staff member.")
return redirect("dashboard:admin_evaluation")
@login_required
def staff_performance_trends(request, staff_id):
"""
API endpoint to get staff performance trends as JSON.
Access: PX Admin and Hospital Admin only
"""
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
from apps.accounts.models import User
# Only PX Admins and Hospital Admins can access
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
return JsonResponse({"success": False, "error": "Permission denied"}, status=403)
user = request.user
months = int(request.GET.get("months", 6))
try:
trends = UnifiedAnalyticsService.get_staff_performance_trends(staff_id=staff_id, user=user, months=months)
return JsonResponse({"success": True, "trends": trends})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)}, status=400)
@login_required
def department_benchmarks(request):
"""
Department benchmarking view comparing all staff.
Access: PX Admin and Hospital Admin only
"""
from django.core.exceptions import PermissionDenied
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
# Only PX Admins and Hospital Admins can access
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can access department benchmarks.")
user = request.user
# Get filters
department_id = request.GET.get("department_id")
date_range = request.GET.get("date_range", "30d")
# If user is department manager, use their department
if user.is_department_manager() and user.department and not department_id:
department_id = str(user.department.id)
try:
benchmarks = UnifiedAnalyticsService.get_department_benchmarks(
user=user, department_id=department_id, date_range=date_range
)
context = {"benchmarks": benchmarks, "date_range": date_range}
return render(request, "dashboard/department_benchmarks.html", context)
except Exception as e:
messages.error(request, f"Error loading benchmarks: {str(e)}")
return redirect("dashboard:admin_evaluation")
@login_required
def export_staff_performance(request):
"""
Export staff performance report in various formats.
Access: PX Admin and Hospital Admin only
"""
from django.core.exceptions import PermissionDenied
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
import csv
import json
from django.http import HttpResponse
# Only PX Admins and Hospital Admins can access
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can export staff performance.")
user = request.user
if request.method != "POST":
return JsonResponse({"error": "POST required"}, status=405)
try:
data = json.loads(request.body)
staff_ids = data.get("staff_ids", [])
date_range = data.get("date_range", "30d")
format_type = data.get("format", "csv")
# Generate report
report = UnifiedAnalyticsService.export_staff_performance_report(
staff_ids=staff_ids, user=user, date_range=date_range, format_type=format_type
)
if format_type == "csv":
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = (
f'attachment; filename="staff_performance_{timezone.now().strftime("%Y%m%d")}.csv"'
)
if report["data"]:
writer = csv.DictWriter(response, fieldnames=report["data"][0].keys())
writer.writeheader()
writer.writerows(report["data"])
return response
elif format_type == "json":
return JsonResponse(report)
else:
return JsonResponse({"error": f"Unsupported format: {format_type}"}, status=400)
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)
@login_required
def command_center_api(request):
"""
API endpoint for Command Center live data updates.
Returns JSON with all module data for AJAX refresh without page reload.
Enables true real-time updates every 30-60 seconds.
"""
from apps.complaints.models import Complaint, Inquiry
from apps.px_action_center.models import PXAction
from apps.surveys.models import SurveyInstance
from apps.physicians.models import PhysicianMonthlyRating
from apps.observations.models import Observation
user = request.user
now = timezone.now()
last_24h = now - timedelta(hours=24)
last_30d = now - timedelta(days=30)
last_60d = now - timedelta(days=60)
# Build querysets based on user role
if user.is_px_admin():
hospital = request.tenant_hospital
complaints_qs = Complaint.objects.filter(hospital=hospital) if hospital else Complaint.objects.none()
inquiries_qs = Inquiry.objects.filter(hospital=hospital) if hospital else Inquiry.objects.none()
actions_qs = PXAction.objects.filter(hospital=hospital) if hospital else PXAction.objects.none()
surveys_qs = SurveyInstance.objects.all()
observations_qs = Observation.objects.filter(hospital=hospital) if hospital else Observation.objects.none()
elif user.is_hospital_admin() and user.hospital:
complaints_qs = Complaint.objects.filter(hospital=user.hospital)
inquiries_qs = Inquiry.objects.filter(hospital=user.hospital)
actions_qs = PXAction.objects.filter(hospital=user.hospital)
surveys_qs = SurveyInstance.objects.filter(survey_template__hospital=user.hospital)
observations_qs = Observation.objects.filter(hospital=user.hospital)
elif user.is_department_manager() and user.department:
complaints_qs = Complaint.objects.filter(department=user.department)
inquiries_qs = Inquiry.objects.filter(department=user.department)
actions_qs = PXAction.objects.filter(department=user.department)
surveys_qs = SurveyInstance.objects.filter(journey_instance__department=user.department)
observations_qs = Observation.objects.filter(assigned_department=user.department)
elif user.is_director():
directed_depts = user.get_directed_departments()
if directed_depts.exists():
complaints_qs = Complaint.objects.filter(department__in=directed_depts)
inquiries_qs = Inquiry.objects.filter(department__in=directed_depts)
actions_qs = PXAction.objects.filter(department__in=directed_depts)
surveys_qs = SurveyInstance.objects.filter(journey_instance__department__in=directed_depts)
observations_qs = Observation.objects.filter(assigned_department__in=directed_depts)
else:
complaints_qs = Complaint.objects.none()
inquiries_qs = Inquiry.objects.none()
actions_qs = PXAction.objects.none()
surveys_qs = SurveyInstance.objects.none()
observations_qs = Observation.objects.none()
else:
complaints_qs = Complaint.objects.none()
inquiries_qs = Inquiry.objects.none()
actions_qs = PXAction.objects.none()
surveys_qs = SurveyInstance.objects.none()
observations_qs = Observation.objects.none()
# Calculate all module data
# Complaints
complaints_current = complaints_qs.filter(created_at__gte=last_30d).count()
complaints_previous = complaints_qs.filter(created_at__gte=last_60d, created_at__lt=last_30d).count()
complaints_variance = (
round(((complaints_current - complaints_previous) / complaints_previous) * 100, 1)
if complaints_previous > 0
else 0
)
# Surveys
surveys_completed_30d = surveys_qs.filter(completed_at__gte=last_30d)
total_surveys_30d = surveys_completed_30d.count()
positive_count = surveys_completed_30d.filter(is_negative=False).count()
negative_count = surveys_completed_30d.filter(is_negative=True).count()
nps_score = round(((positive_count - negative_count) / total_surveys_30d) * 100) if total_surveys_30d > 0 else 0
avg_satisfaction = (
surveys_completed_30d.filter(total_score__isnull=False).aggregate(Avg("total_score"))["total_score__avg"] or 0
)
surveys_sent_30d = surveys_qs.filter(sent_at__gte=last_30d).count()
response_rate = round((total_surveys_30d / surveys_sent_30d) * 100, 1) if surveys_sent_30d > 0 else 0
# Actions
actions_open = actions_qs.filter(status="open").count()
actions_in_progress = actions_qs.filter(status="in_progress").count()
actions_pending_approval = actions_qs.filter(status="pending_approval").count()
actions_closed_30d = actions_qs.filter(status="closed", closed_at__gte=last_30d).count()
# Red alerts
red_alerts = []
critical_complaints = complaints_qs.filter(severity="critical", status__in=["open", "in_progress"]).count()
if critical_complaints > 0:
red_alerts.append({"type": "critical_complaints", "value": critical_complaints})
overdue_complaints = complaints_qs.filter(is_overdue=True, status__in=["open", "in_progress"]).count()
if overdue_complaints > 0:
red_alerts.append({"type": "overdue_complaints", "value": overdue_complaints})
escalated_actions = actions_qs.filter(escalation_level__gt=0, status__in=["open", "in_progress"]).count()
if escalated_actions > 0:
red_alerts.append({"type": "escalated_actions", "value": escalated_actions})
negative_surveys_24h = surveys_qs.filter(is_negative=True, completed_at__gte=last_24h).count()
if negative_surveys_24h > 0:
red_alerts.append({"type": "negative_surveys", "value": negative_surveys_24h})
return JsonResponse(
{
"success": True,
"timestamp": now.isoformat(),
"last_updated": now.strftime("%Y-%m-%d %H:%M:%S"),
"red_alerts": {"has_alerts": len(red_alerts) > 0, "count": len(red_alerts), "items": red_alerts},
"modules": {
"complaints": {
"total_active": complaints_qs.filter(status__in=["open", "in_progress"]).count(),
"variance": complaints_variance,
"variance_direction": "up"
if complaints_variance > 0
else "down"
if complaints_variance < 0
else "neutral",
"overdue": complaints_qs.filter(is_overdue=True, status__in=["open", "in_progress"]).count(),
"critical": complaints_qs.filter(severity="critical", status__in=["open", "in_progress"]).count(),
"by_severity": {
"critical": complaints_qs.filter(
severity="critical", status__in=["open", "in_progress"]
).count(),
"high": complaints_qs.filter(severity="high", status__in=["open", "in_progress"]).count(),
"medium": complaints_qs.filter(severity="medium", status__in=["open", "in_progress"]).count(),
"low": complaints_qs.filter(severity="low", status__in=["open", "in_progress"]).count(),
},
},
"surveys": {
"nps_score": nps_score,
"avg_satisfaction": round(avg_satisfaction, 1),
"response_rate": response_rate,
"total_completed": total_surveys_30d,
"negative_24h": surveys_qs.filter(is_negative=True, completed_at__gte=last_24h).count(),
},
"actions": {
"open": actions_open,
"in_progress": actions_in_progress,
"pending_approval": actions_pending_approval,
"closed_30d": actions_closed_30d,
"overdue": actions_qs.filter(is_overdue=True, status__in=["open", "in_progress"]).count(),
"escalated": actions_qs.filter(escalation_level__gt=0, status__in=["open", "in_progress"]).count(),
},
"inquiries": {
"open": inquiries_qs.filter(status="open").count(),
"in_progress": inquiries_qs.filter(status="in_progress").count(),
"total_active": inquiries_qs.filter(status__in=["open", "in_progress"]).count(),
"new_24h": inquiries_qs.filter(created_at__gte=last_24h).count(),
},
"observations": {
"new": observations_qs.filter(status="new").count(),
"in_progress": observations_qs.filter(status="in_progress").count(),
"total_active": observations_qs.filter(status__in=["new", "in_progress"]).count(),
"critical": observations_qs.filter(
severity="critical", status__in=["new", "triaged", "assigned", "in_progress"]
).count(),
},
},
}
)
@login_required
def performance_analytics_api(request):
"""
API endpoint for various performance analytics.
Access: PX Admin and Hospital Admin only
"""
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
# Only PX Admins and Hospital Admins can access
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
return JsonResponse({"success": False, "error": "Permission denied"}, status=403)
user = request.user
chart_type = request.GET.get("chart_type")
try:
if chart_type == "staff_trends":
staff_id = request.GET.get("staff_id")
months = int(request.GET.get("months", 6))
data = UnifiedAnalyticsService.get_staff_performance_trends(staff_id=staff_id, user=user, months=months)
elif chart_type == "department_benchmarks":
department_id = request.GET.get("department_id")
date_range = request.GET.get("date_range", "30d")
data = UnifiedAnalyticsService.get_department_benchmarks(
user=user, department_id=department_id, date_range=date_range
)
else:
return JsonResponse({"error": f"Unknown chart type: {chart_type}"}, status=400)
return JsonResponse({"success": True, "data": data})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)}, status=500)
@login_required
def employee_evaluation(request):
"""
Employee Evaluation Dashboard - PAD Department Weekly Dashboard.
Shows comprehensive performance metrics for all staff members side-by-side.
Based on the employee_evaluation.md specification.
Access: PX Admin and Hospital Admin only
"""
from django.core.exceptions import PermissionDenied
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
from apps.accounts.models import User
from apps.organizations.models import Hospital, Department
user = request.user
# Only PX Admins and Hospital Admins can access
if not (user.is_px_admin() or user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can access the Employee Evaluation dashboard.")
# Get date range filter - default to last 7 days (weekly)
date_range = request.GET.get("date_range", "7d")
custom_start = request.GET.get("custom_start")
custom_end = request.GET.get("custom_end")
# Parse custom dates if provided
if custom_start:
from datetime import datetime
custom_start = datetime.fromisoformat(custom_start)
if custom_end:
from datetime import datetime
custom_end = datetime.fromisoformat(custom_end)
# Get hospital and department filters
hospital_id = request.GET.get("hospital_id")
department_id = request.GET.get("department_id")
# Get selected staff IDs for comparison
selected_staff_ids = request.GET.getlist("staff_ids")
status_filter = request.GET.get("status", "active")
# Get available hospitals (for PX Admins)
if user.is_px_admin():
hospitals = Hospital.objects.filter(status="active")
elif user.is_hospital_admin() and user.hospital:
hospitals = Hospital.objects.filter(id=user.hospital.id)
hospital_id = hospital_id or user.hospital.id # Default to user's hospital
else:
hospitals = Hospital.objects.none()
# Get available departments based on hospital filter
if hospital_id:
departments = Department.objects.filter(hospital_id=hospital_id, status="active")
elif user.hospital:
departments = Department.objects.filter(hospital=user.hospital, status="active")
else:
departments = Department.objects.none()
# Get employee evaluation metrics
evaluation_data = UnifiedAnalyticsService.get_employee_evaluation_metrics(
user=user,
date_range=date_range,
hospital_id=hospital_id,
department_id=department_id,
staff_ids=selected_staff_ids if selected_staff_ids else None,
custom_start=custom_start,
custom_end=custom_end,
)
# Get all staff for the dropdown
staff_queryset = User.objects.filter(groups__name="PX Employee")
if user.is_px_admin():
if hospital_id:
staff_queryset = staff_queryset.filter(hospital_id=hospital_id)
elif request.tenant_hospital:
staff_queryset = staff_queryset.filter(hospital=request.tenant_hospital)
hospital_id = hospital_id or request.tenant_hospital.id
elif user.is_hospital_admin() and user.hospital:
staff_queryset = staff_queryset.filter(hospital=user.hospital)
hospital_id = hospital_id or user.hospital.id
if department_id:
staff_queryset = staff_queryset.filter(department_id=department_id)
if status_filter == "active":
staff_queryset = staff_queryset.filter(is_active=True)
elif status_filter == "inactive":
staff_queryset = staff_queryset.filter(is_active=False)
# Only staff with assigned complaints or inquiries
staff_queryset = (
staff_queryset.filter(Q(assigned_complaints__isnull=False) | Q(assigned_inquiries__isnull=False))
.distinct()
.select_related("hospital", "department")
)
context = {
"departments": departments,
"staff_list": staff_queryset,
"selected_hospital_id": hospital_id,
"selected_department_id": department_id,
"selected_staff_ids": selected_staff_ids,
"date_range": date_range,
"custom_start": custom_start,
"custom_end": custom_end,
"evaluation_data": evaluation_data,
"status_filter": status_filter,
}
return render(request, "dashboard/employee_evaluation.html", context)
@login_required
def employee_evaluation_charts(request):
"""
Employee Evaluation Charts page — comparison table + visual charts.
Access: PX Admin and Hospital Admin only
"""
from django.core.exceptions import PermissionDenied
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
from apps.accounts.models import User
from apps.organizations.models import Hospital, Department
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can access this page.")
date_range = request.GET.get("date_range", "7d")
custom_start = request.GET.get("custom_start")
custom_end = request.GET.get("custom_end")
if custom_start:
from datetime import datetime
custom_start = datetime.fromisoformat(custom_start)
if custom_end:
from datetime import datetime
custom_end = datetime.fromisoformat(custom_end)
hospital_id = request.GET.get("hospital_id")
department_id = request.GET.get("department_id")
selected_staff_ids = request.GET.getlist("staff_ids")
status_filter = request.GET.get("status", "active")
if user.is_px_admin():
hospitals = Hospital.objects.filter(status="active")
elif user.is_hospital_admin() and user.hospital:
hospitals = Hospital.objects.filter(id=user.hospital.id)
hospital_id = hospital_id or user.hospital.id
else:
hospitals = Hospital.objects.none()
if hospital_id:
departments = Department.objects.filter(hospital_id=hospital_id, status="active")
elif user.hospital:
departments = Department.objects.filter(hospital=user.hospital, status="active")
else:
departments = Department.objects.none()
evaluation_data = UnifiedAnalyticsService.get_employee_evaluation_metrics(
user=user,
date_range=date_range,
hospital_id=hospital_id,
department_id=department_id,
staff_ids=selected_staff_ids if selected_staff_ids else None,
custom_start=custom_start,
custom_end=custom_end,
)
staff_queryset = User.objects.filter(groups__name="PX Employee")
if user.is_px_admin():
if hospital_id:
staff_queryset = staff_queryset.filter(hospital_id=hospital_id)
elif request.tenant_hospital:
staff_queryset = staff_queryset.filter(hospital=request.tenant_hospital)
hospital_id = hospital_id or request.tenant_hospital.id
elif user.is_hospital_admin() and user.hospital:
staff_queryset = staff_queryset.filter(hospital=user.hospital)
hospital_id = hospital_id or user.hospital.id
if department_id:
staff_queryset = staff_queryset.filter(department_id=department_id)
if status_filter == "active":
staff_queryset = staff_queryset.filter(is_active=True)
elif status_filter == "inactive":
staff_queryset = staff_queryset.filter(is_active=False)
staff_queryset = (
staff_queryset.filter(Q(assigned_complaints__isnull=False) | Q(assigned_inquiries__isnull=False))
.distinct()
.select_related("hospital", "department")
)
context = {
"departments": departments,
"staff_list": staff_queryset,
"selected_hospital_id": hospital_id,
"selected_department_id": department_id,
"selected_staff_ids": selected_staff_ids,
"date_range": date_range,
"custom_start": custom_start,
"custom_end": custom_end,
"evaluation_data": evaluation_data,
"hospitals": hospitals,
"status_filter": status_filter,
}
return render(request, "dashboard/employee_evaluation_charts.html", context)
@login_required
def employee_evaluation_data(request):
"""
API endpoint to get employee evaluation data for charts and tables.
Access: PX Admin and Hospital Admin only
"""
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
if request.method != "GET":
return JsonResponse({"success": False, "error": "GET required"}, status=405)
user = request.user
# Only PX Admins and Hospital Admins can access
if not (user.is_px_admin() or user.is_hospital_admin()):
return JsonResponse({"success": False, "error": "Permission denied"}, status=403)
date_range = request.GET.get("date_range", "7d")
hospital_id = request.GET.get("hospital_id")
department_id = request.GET.get("department_id")
staff_ids = request.GET.getlist("staff_ids")
# For PX Admins without explicit hospital_id, use tenant_hospital
if user.is_px_admin() and not hospital_id and request.tenant_hospital:
hospital_id = str(request.tenant_hospital.id)
# Parse custom dates if provided
custom_start = request.GET.get("custom_start")
custom_end = request.GET.get("custom_end")
if custom_start:
from datetime import datetime
custom_start = datetime.fromisoformat(custom_start)
if custom_end:
from datetime import datetime
custom_end = datetime.fromisoformat(custom_end)
try:
# Get employee evaluation metrics
evaluation_data = UnifiedAnalyticsService.get_employee_evaluation_metrics(
user=user,
date_range=date_range,
hospital_id=hospital_id,
department_id=department_id,
staff_ids=staff_ids if staff_ids else None,
custom_start=custom_start,
custom_end=custom_end,
)
return JsonResponse({"success": True, "data": evaluation_data})
except Exception as e:
return JsonResponse({"success": False, "error": str(e)}, status=500)
return JsonResponse({"success": False, "error": str(e)}, status=500)
@login_required
def complaint_request_list(request):
"""
Step 0 — Complaint Request List view.
Shows all complaint requests (filled, not filled, on hold)
with filters for month, staff, status. Includes export button.
"""
from django.core.exceptions import PermissionDenied
from apps.dashboard.models import ComplaintRequest
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can access.")
qs = ComplaintRequest.objects.select_related("staff", "hospital", "complained_department", "complaint")
if user.is_hospital_admin() and user.hospital:
qs = qs.filter(hospital=user.hospital)
elif user.is_px_admin() and request.tenant_hospital:
qs = qs.filter(hospital=request.tenant_hospital)
month = request.GET.get("month")
year = request.GET.get("year")
staff_id = request.GET.get("staff_id")
status = request.GET.get("status")
if year and month:
qs = qs.filter(request_date__year=int(year), request_date__month=int(month))
elif year:
qs = qs.filter(request_date__year=int(year))
if staff_id:
qs = qs.filter(staff_id=staff_id)
if status == "filled":
qs = qs.filter(filled=True)
elif status == "not_filled":
qs = qs.filter(not_filled=True)
elif status == "on_hold":
qs = qs.filter(on_hold=True)
elif status == "barcode":
qs = qs.filter(from_barcode=True)
qs = qs.order_by("-request_date", "-created_at")
paginator = Paginator(qs, 50)
page_number = request.GET.get("page", 1)
page_obj = paginator.get_page(page_number)
from apps.accounts.models import User
staff_members = User.objects.filter(complaint_requests_sent__isnull=False)
# Filter by hospital based on user role
if user.is_hospital_admin() and user.hospital:
staff_members = staff_members.filter(hospital=user.hospital)
elif user.is_px_admin() and request.tenant_hospital:
staff_members = staff_members.filter(hospital=request.tenant_hospital)
staff_members = staff_members.distinct().order_by("first_name", "last_name")
context = {
"page_obj": page_obj,
"staff_members": staff_members,
"selected_year": year,
"selected_month": month,
"selected_staff": staff_id,
"selected_status": status,
"years_range": range(2024, timezone.now().year + 1),
"months_range": range(1, 13),
}
return render(request, "dashboard/complaint_request_list.html", context)
@login_required
def complaint_request_export(request):
"""
Step 0 — Export complaint requests to Excel.
"""
from django.core.exceptions import PermissionDenied
from apps.dashboard.models import ComplaintRequest
from apps.complaints.utils import export_requests_report
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can export.")
qs = ComplaintRequest.objects.select_related("staff", "hospital", "complained_department", "complaint")
if user.is_hospital_admin() and user.hospital:
qs = qs.filter(hospital=user.hospital)
elif user.is_px_admin() and request.tenant_hospital:
qs = qs.filter(hospital=request.tenant_hospital)
year = request.GET.get("year")
month = request.GET.get("month")
if year and month:
qs = qs.filter(request_date__year=int(year), request_date__month=int(month))
elif year:
qs = qs.filter(request_date__year=int(year))
return export_requests_report(
qs,
year=int(year) if year else None,
month=int(month) if month else None,
)
@login_required
def census_report(request):
import json
from django.core.exceptions import PermissionDenied
from apps.dashboard.services.census import CensusService
from apps.organizations.models import Hospital
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee() or user.is_px_management()):
raise PermissionDenied("Only PX Admins, Hospital Admins, and PX Employee and PX Management can access.")
hospital_id = request.GET.get("hospital")
year = request.GET.get("year")
if user.is_hospital_admin() and user.hospital:
hospital_id = hospital_id or str(user.hospital.id)
elif user.is_px_admin() and request.tenant_hospital:
hospital_id = hospital_id or str(request.tenant_hospital.id)
if not hospital_id:
first = Hospital.objects.first()
if first:
hospital_id = str(first.id)
if not hospital_id:
return render(request, "dashboard/census_report.html", {"no_data": True})
year = int(year) if year else timezone.now().year
service = CensusService(hospital_id=hospital_id, year=year)
monthly_counts = service.get_monthly_counts(year)
quarterly_data = service.get_quarterly_data(year)
year_totals = service.get_year_totals(year)
available_years = service.get_available_years()
chart_data = service.get_chart_data(years=[year - 2, year - 1, year])
context = {
"hospitals": Hospital.objects.all(),
"selected_hospital": hospital_id,
"selected_year": year,
"available_years": available_years,
"monthly_counts": monthly_counts,
"quarterly_data": quarterly_data,
"year_totals": year_totals,
"chart_data_json": json.dumps(chart_data),
}
return render(request, "dashboard/census_report.html", context)
@login_required
def inquiry_report(request):
import json
from django.core.exceptions import PermissionDenied
from apps.dashboard.services.inquiry_report import InquiryReportService
from apps.organizations.models import Hospital
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee() or user.is_px_management()):
raise PermissionDenied("Only PX Admins, Hospital Admins, and PX Employee and PX Management can access.")
hospital_id = request.GET.get("hospital")
year = request.GET.get("year")
month = request.GET.get("month")
if user.is_hospital_admin() and user.hospital:
hospital_id = hospital_id or str(user.hospital.id)
elif user.is_px_admin() and request.tenant_hospital:
hospital_id = hospital_id or str(request.tenant_hospital.id)
if not hospital_id:
first = Hospital.objects.first()
if first:
hospital_id = str(first.id)
if not hospital_id:
return render(request, "dashboard/inquiry_report.html", {"no_data": True})
year = int(year) if year else timezone.now().year
month = int(month) if month else timezone.now().month
service = InquiryReportService(hospital_id=hospital_id, year=year, month=month)
available_months = service.get_available_months()
incoming_summary = service.get_summary(is_outgoing=False)
outgoing_summary = service.get_summary(is_outgoing=True)
chart_data = service.get_chart_data()
employee_breakdown = service.get_employee_breakdown()
department_breakdown = service.get_department_breakdown()
fmt = lambda n: f"{n:,}"
for bd in employee_breakdown + department_breakdown:
bd["total_fmt"] = fmt(bd["total"])
context = {
"hospitals": Hospital.objects.all(),
"selected_hospital": hospital_id,
"selected_year": year,
"selected_month": month,
"available_months": available_months,
"incoming": incoming_summary,
"outgoing": outgoing_summary,
"incoming_total_fmt": fmt(incoming_summary["total"]),
"outgoing_total_fmt": fmt(outgoing_summary["total"]),
"chart_data_json": json.dumps(chart_data),
"employee_breakdown": employee_breakdown,
"department_breakdown": department_breakdown,
}
return render(request, "dashboard/inquiry_report.html", context)
@login_required
def census_report_export(request):
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from apps.dashboard.services.census import CensusService
from apps.dashboard.services.census_export import generate_census_excel
from apps.organizations.models import Hospital
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can access.")
hospital_id = request.GET.get("hospital")
year = request.GET.get("year")
month = request.GET.get("month")
if user.is_hospital_admin() and user.hospital:
hospital_id = hospital_id or str(user.hospital.id)
elif user.is_px_admin() and request.tenant_hospital:
hospital_id = hospital_id or str(request.tenant_hospital.id)
if not hospital_id:
return HttpResponse("Hospital parameter required", status=400)
year = int(year) if year else timezone.now().year
month = int(month) if month else timezone.now().month
service = CensusService(hospital_id=hospital_id, year=year, month=month)
buf = generate_census_excel(service)
month_label = f"{year}-{month:02d}"
response = HttpResponse(
buf.getvalue(),
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
response["Content-Disposition"] = f'attachment; filename="census_{month_label}.xlsx"'
return response
@login_required
def observation_report(request):
import json
from datetime import date
from django.core.exceptions import PermissionDenied
from apps.dashboard.services.observation_report import ObservationReportService
from apps.organizations.models import Hospital
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee() or user.is_px_management()):
raise PermissionDenied("Only PX Admins, Hospital Admins, and PX Employee and PX Management can access.")
hospital_id = request.GET.get("hospital")
date_from_str = request.GET.get("date_from")
date_to_str = request.GET.get("date_to")
if user.is_hospital_admin() and user.hospital:
hospital_id = hospital_id or str(user.hospital.id)
elif user.is_px_admin() and request.tenant_hospital:
hospital_id = hospital_id or str(request.tenant_hospital.id)
if not hospital_id:
first = Hospital.objects.first()
if first:
hospital_id = str(first.id)
if not hospital_id:
return render(request, "dashboard/observation_report.html", {"no_data": True})
today = timezone.now().date()
if date_from_str:
date_from = date.fromisoformat(date_from_str)
else:
date_from = today.replace(day=1)
if date_to_str:
date_to = date.fromisoformat(date_to_str)
else:
date_to = today
service = ObservationReportService(hospital_id=hospital_id, date_from=date_from, date_to=date_to)
summary = service.get_summary()
chart_data = service.get_chart_data()
employee_breakdown = service.get_employee_breakdown()
department_breakdown = service.get_department_breakdown()
category_breakdown = service.get_category_breakdown()
fmt = lambda n: f"{n:,}"
for bd in employee_breakdown + department_breakdown:
bd["total_fmt"] = fmt(bd["total"])
context = {
"hospitals": Hospital.objects.all(),
"selected_hospital": hospital_id,
"date_from": date_from.isoformat(),
"date_to": date_to.isoformat(),
"summary": summary,
"total_fmt": fmt(summary["total"]),
"chart_data_json": json.dumps(chart_data),
"employee_breakdown": employee_breakdown,
"department_breakdown": department_breakdown,
"category_breakdown": category_breakdown,
}
return render(request, "dashboard/observation_report.html", context)
@login_required
def observation_report_export(request):
from datetime import date
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from apps.dashboard.services.observation_report import ObservationReportService
from apps.dashboard.services.observation_report_export import generate_observation_excel
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can access.")
hospital_id = request.GET.get("hospital")
date_from_str = request.GET.get("date_from")
date_to_str = request.GET.get("date_to")
if user.is_hospital_admin() and user.hospital:
hospital_id = hospital_id or str(user.hospital.id)
elif user.is_px_admin() and request.tenant_hospital:
hospital_id = hospital_id or str(request.tenant_hospital.id)
if not hospital_id:
return HttpResponse("Hospital parameter required", status=400)
today = timezone.now().date()
if date_from_str:
date_from = date.fromisoformat(date_from_str)
else:
date_from = today.replace(day=1)
if date_to_str:
date_to = date.fromisoformat(date_to_str)
else:
date_to = today
service = ObservationReportService(hospital_id=hospital_id, date_from=date_from, date_to=date_to)
buf = generate_observation_excel(service)
date_label = f"{date_from.isoformat()}_to_{date_to.isoformat()}"
response = HttpResponse(
buf.getvalue(),
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
response["Content-Disposition"] = f'attachment; filename="observations_{date_label}.xlsx"'
return response
@login_required
def complaint_monthly_report(request):
import json
from django.core.exceptions import PermissionDenied
from apps.dashboard.services.complaint_monthly_service import ComplaintMonthlyService
from apps.organizations.models import Hospital
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee() or user.is_px_management()):
raise PermissionDenied("Only PX Admins, Hospital Admins, and PX Employee and PX Management can access.")
hospital_id = request.GET.get("hospital")
year = request.GET.get("year")
month = request.GET.get("month")
if user.is_hospital_admin() and user.hospital:
hospital_id = hospital_id or str(user.hospital.id)
elif user.is_px_admin() and request.tenant_hospital:
hospital_id = hospital_id or str(request.tenant_hospital.id)
if not hospital_id:
first = Hospital.objects.first()
if first:
hospital_id = str(first.id)
if not hospital_id:
return render(request, "dashboard/complaint_monthly_report.html", {"no_data": True})
year = int(year) if year else timezone.now().year
month = int(month) if month else timezone.now().month
service = ComplaintMonthlyService(hospital_id=hospital_id, year=year, month=month)
available_months = service.get_available_months()
summary = service.get_summary()
chart_data = service.get_chart_data()
stage_times = service.get_avg_stage_times()
fmt = lambda n: f"{n:,}"
context = {
"hospitals": Hospital.objects.all(),
"selected_hospital": hospital_id,
"selected_year": year,
"selected_month": month,
"available_months": available_months,
"summary": summary,
"total_fmt": fmt(summary["total"]),
"chart_data_json": json.dumps(chart_data),
"stage_times": stage_times,
}
return render(request, "dashboard/complaint_monthly_report.html", context)
@login_required
def complaint_monthly_export(request):
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from apps.dashboard.services.complaint_monthly_service import ComplaintMonthlyService
from apps.dashboard.services.complaint_monthly_export import generate_complaint_monthly_excel
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can access.")
hospital_id = request.GET.get("hospital")
year = int(request.GET.get("year", timezone.now().year))
month = int(request.GET.get("month", timezone.now().month))
if user.is_hospital_admin() and user.hospital:
hospital_id = hospital_id or str(user.hospital.id)
elif user.is_px_admin() and request.tenant_hospital:
hospital_id = hospital_id or str(request.tenant_hospital.id)
if not hospital_id:
return HttpResponse("Hospital parameter required", status=400)
service = ComplaintMonthlyService(hospital_id=hospital_id, year=year, month=month)
buf = generate_complaint_monthly_excel(service)
month_label = f"{year}-{month:02d}"
response = HttpResponse(
buf.getvalue(),
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
response["Content-Disposition"] = f'attachment; filename="complaints_monthly_{month_label}.xlsx"'
return response
def _build_kpi_blocks(kpi_data, satisfaction_data, moh_kpi_data, active_months):
blocks = [
{
"label": "KPI #1 - Resolution to Patient Complaints",
"numerator_label": "Total Number of Closed Complaints",
"denominator_label": "Total Number of Complaints",
"result_label": "Result (%)",
"numerator_values": [kpi_data["months"][m]["closed"] for m in active_months],
"denominator_values": [kpi_data["months"][m]["total"] for m in active_months],
"result_values": [kpi_data["months"][m]["resolution_rate"] for m in active_months],
"total_num": kpi_data["totals"]["closed"],
"total_den": kpi_data["totals"]["total"],
"total_res": kpi_data["totals"]["resolution_rate"],
},
{
"label": "KPI #2 - 72-Hour Complaint Resolution Rate",
"numerator_label": "Number of Complaints Resolved Within 72 Hours",
"denominator_label": "Total Number of Complaints",
"result_label": "Result (%)",
"numerator_values": [kpi_data["months"][m]["resolved_72h"] for m in active_months],
"denominator_values": [kpi_data["months"][m]["total"] for m in active_months],
"result_values": [kpi_data["months"][m]["resolved_72h_rate"] for m in active_months],
"total_num": kpi_data["totals"]["resolved_72h"],
"total_den": kpi_data["totals"]["total"],
"total_res": kpi_data["totals"]["resolved_72h_rate"],
},
{
"label": "KPI #3 - Satisfaction Rate",
"numerator_label": "Number of Satisfied Responses",
"denominator_label": "Total Number of Patient Surveyed (Responses)",
"result_label": "Result (%)",
"numerator_values": [satisfaction_data["months"][m]["satisfied"] for m in active_months],
"denominator_values": [satisfaction_data["months"][m]["surveyed"] for m in active_months],
"result_values": [satisfaction_data["months"][m]["rate"] for m in active_months],
"total_num": satisfaction_data["totals"]["satisfied"],
"total_den": satisfaction_data["totals"]["surveyed"],
"total_res": satisfaction_data["totals"]["rate"],
},
{
"label": "Response Rate - Calls",
"numerator_label": "Total Responses",
"denominator_label": "Total Complaints",
"result_label": "Result (%)",
"numerator_values": [satisfaction_data["call_months"][m]["responses"] for m in active_months],
"denominator_values": [satisfaction_data["call_months"][m]["total"] for m in active_months],
"result_values": [satisfaction_data["call_months"][m]["rate"] for m in active_months],
"total_num": satisfaction_data["call_totals"]["responses"],
"total_den": satisfaction_data["call_totals"]["total"],
"total_res": satisfaction_data["call_totals"]["rate"],
},
{
"label": "Response Rate - Survey",
"numerator_label": "Total Responses",
"denominator_label": "Total Complaints",
"result_label": "Result (%)",
"numerator_values": [satisfaction_data["survey_months"][m]["responses"] for m in active_months],
"denominator_values": [satisfaction_data["survey_months"][m]["total"] for m in active_months],
"result_values": [satisfaction_data["survey_months"][m]["rate"] for m in active_months],
"total_num": satisfaction_data["survey_totals"]["responses"],
"total_den": satisfaction_data["survey_totals"]["total"],
"total_res": satisfaction_data["survey_totals"]["rate"],
},
{
"label": "MOH KPI",
"numerator_label": "Complaints which received a (satisfied) scoring on its resolution",
"denominator_label": "Total number of complaints received",
"result_label": "Result (%)",
"numerator_values": [moh_kpi_data["moh"]["months"][m]["satisfied_scoring"] for m in active_months],
"denominator_values": [moh_kpi_data["moh"]["months"][m]["total"] for m in active_months],
"result_values": [moh_kpi_data["moh"]["months"][m]["rate"] for m in active_months],
"total_num": moh_kpi_data["moh"]["totals"]["satisfied_scoring"],
"total_den": moh_kpi_data["moh"]["totals"]["total"],
"total_res": moh_kpi_data["moh"]["totals"]["rate"],
},
{
"label": "MOH KPI - Real Numbers",
"numerator_label": "Complaints which received a (satisfied) scoring on its resolution",
"denominator_label": "Total number of complaints received",
"result_label": "Result (%)",
"numerator_values": [moh_kpi_data["real"]["months"][m]["satisfied_scoring"] for m in active_months],
"denominator_values": [moh_kpi_data["real"]["months"][m]["total"] for m in active_months],
"result_values": [moh_kpi_data["real"]["months"][m]["rate"] for m in active_months],
"total_num": moh_kpi_data["real"]["totals"]["satisfied_scoring"],
"total_den": moh_kpi_data["real"]["totals"]["total"],
"total_res": moh_kpi_data["real"]["totals"]["rate"],
},
{
"label": "KPI #4 - 48-Hour Response Rate",
"numerator_label": "Total Responses Within 48 Hours",
"denominator_label": "Total Number of Complaints",
"result_label": "Result (%)",
"numerator_values": [kpi_data["months"][m]["resolved_48h"] for m in active_months],
"denominator_values": [kpi_data["months"][m]["total"] for m in active_months],
"result_values": [kpi_data["months"][m]["resolved_48h_rate"] for m in active_months],
"total_num": kpi_data["totals"]["resolved_48h"],
"total_den": kpi_data["totals"]["total"],
"total_res": kpi_data["totals"]["resolved_48h_rate"],
},
]
return blocks
def _build_source_subrows(source_breakdown, keys, labels, active_months):
rows = []
for key, label in zip(keys, labels):
total = sum(source_breakdown[m][key] for m in active_months)
if total == 0:
continue
monthly = [{"count": source_breakdown[m][key]} for m in active_months]
rows.append({"label": label, "total": total, "monthly": monthly})
return rows
def _build_location_ratio_tables(location_ratios, active_months):
import calendar
tables = []
for area, title, comp_col, pat_col in [
("ip", "In-Patient Complaints vs Patients", "In-Patient Complaints", "Total In-Patient"),
("op", "Out-Patient Complaints vs Patients", "Out-Patient Complaint", "Total Out-Patient"),
("er", "ER Complaints vs Patients", "ER Complaints", "Total ER Patients"),
]:
rows = []
for m in active_months:
d = location_ratios["months"][m]
rows.append({
"month": calendar.month_abbr[m],
"complaints": d[f"{area}_complaints"],
"patients": d[f"{area}_patients"],
"ratio": d[f"{area}_ratio"] * 100,
"is_total": False,
})
t = location_ratios["totals"][area]
rows.append({
"month": "TOTAL",
"complaints": t["complaints"],
"patients": t["patients"],
"ratio": t["ratio"] * 100,
"is_total": True,
})
tables.append({
"title": title, "complaint_col": comp_col, "patient_col": pat_col,
"rows": rows,
})
return tables
@login_required
def complaint_quarterly_report(request):
import json
from django.core.exceptions import PermissionDenied
from apps.dashboard.services.complaint_quarterly_service import ComplaintQuarterlyService
from apps.organizations.models import Hospital
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee() or user.is_px_management()):
raise PermissionDenied("Only PX Admins, Hospital Admins, and PX Employee and PX Management can access.")
hospital_id = request.GET.get("hospital")
start_date_str = request.GET.get("start_date")
end_date_str = request.GET.get("end_date")
if user.is_hospital_admin() and user.hospital:
hospital_id = hospital_id or str(user.hospital.id)
elif user.is_px_admin() and request.tenant_hospital:
hospital_id = hospital_id or str(request.tenant_hospital.id)
if not hospital_id:
first = Hospital.objects.first()
if first:
hospital_id = str(first.id)
if not hospital_id:
return render(request, "dashboard/complaint_quarterly_report.html", {"no_data": True})
from datetime import date as _date
current_year = timezone.now().year
if start_date_str and end_date_str:
start_date = _date.fromisoformat(start_date_str)
end_date = _date.fromisoformat(end_date_str)
else:
start_date = _date(current_year, 1, 1)
end_date = _date(current_year, 12, 31)
start_date_str = start_date.isoformat()
end_date_str = end_date.isoformat()
service = ComplaintQuarterlyService(hospital_id=hospital_id, start_date=start_date, end_date=end_date)
active_months = service.active_months
context = {
"hospitals": Hospital.objects.all(),
"selected_hospital": hospital_id,
"selected_year": start_date.year,
"start_date": start_date_str,
"end_date": end_date_str,
"available_years": service.get_available_years(),
"kpi_data": service.get_kpi_data(),
"satisfaction_data": service.get_satisfaction_data(),
"moh_kpi_data": service.get_moh_kpi_data(),
"source_breakdown": service.get_source_breakdown(),
"source_totals": service.get_source_totals(),
"location_breakdown": service.get_location_breakdown(),
"dept_type_breakdown": service.get_dept_type_breakdown(),
"dept_type_monthly": service.get_dept_type_monthly_table(),
"location_ratios": service.get_location_ratios(),
"quarterly_volumes": service.get_quarterly_volumes(),
"monthly_summary": service.get_monthly_summary(),
"escalated": service.get_escalated_breakdown(),
"response_rates": service.get_response_rates(),
"chart_data_json": json.dumps(service.get_chart_data()),
}
kpi_data = context["kpi_data"]
satisfaction_data = context["satisfaction_data"]
moh_kpi_data = context["moh_kpi_data"]
source_breakdown = context["source_breakdown"]
source_totals = context["source_totals"]
location_breakdown = context["location_breakdown"]
dept_type_breakdown = context["dept_type_breakdown"]
dept_type_monthly = context["dept_type_monthly"]
location_ratios = context["location_ratios"]
monthly_summary = context["monthly_summary"]
escalated = context["escalated"]
response_rates = context["response_rates"]
import calendar as _cal
context["month_labels"] = [_cal.month_abbr[m] for m in active_months]
context["kpi_blocks"] = _build_kpi_blocks(kpi_data, satisfaction_data, moh_kpi_data, active_months)
context["source_sub_external"] = _build_source_subrows(source_breakdown, ["moh", "chi", "insurance"], ["MOH", "CHI", "Insurance Company"], active_months)
context["source_sub_internal"] = _build_source_subrows(source_breakdown, ["patients", "relatives"], ["Patients", "Patient's relatives"], active_months)
context["source_internal_monthly"] = [source_breakdown[m]["internal"] for m in active_months]
context["source_total_monthly"] = [source_breakdown[m]["total"] for m in active_months]
context["source_total_rows"] = [
{"label": "Internal Complaints", "count": source_totals["internal"]["count"], "pct_label": "% Internal", "pct": source_totals["internal"]["pct"]},
{"label": "External Complaints", "count": source_totals["external"]["count"], "pct_label": "% External", "pct": source_totals["external"]["pct"]},
]
if source_totals["moh"]["count"] > 0:
context["source_total_rows"].append({"label": "MOH", "count": source_totals["moh"]["count"], "pct_label": "% MOH", "pct": source_totals["moh"]["pct"]})
if source_totals["chi"]["count"] > 0:
context["source_total_rows"].append({"label": "CHI", "count": source_totals["chi"]["count"], "pct_label": "% CHI", "pct": source_totals["chi"]["pct"]})
if source_totals["insurance"]["count"] > 0:
context["source_total_rows"].append({"label": "Insurance Company", "count": source_totals["insurance"]["count"], "pct_label": "% Insurance Comp.", "pct": source_totals["insurance"]["pct"]})
total_all = sum(location_breakdown[m]["total"] for m in active_months)
context["location_rows"] = []
for label, key in [("In-Patient", "ip"), ("Out-Patient", "op"), ("ER", "er")]:
t = sum(location_breakdown[m][key] for m in active_months)
context["location_rows"].append({
"label": label, "total": t, "pct": round(t / total_all, 3) if total_all else 0,
"monthly": [location_breakdown[m][key] for m in active_months], "is_total": False,
})
context["location_rows"].append({
"label": "Total", "total": total_all, "pct": 1,
"monthly": [location_breakdown[m]["total"] for m in active_months], "is_total": True,
})
context["dept_type_rows"] = []
for label, key in [("Medical", "medical"), ("Admin", "admin"), ("Nursing", "nursing"), ("Support Services", "support")]:
t = sum(dept_type_breakdown[m][key] for m in active_months)
context["dept_type_rows"].append({
"label": label, "total": t, "pct": round(t / total_all, 3) if total_all else 0,
"monthly": [dept_type_breakdown[m][key] for m in active_months], "is_total": False,
})
context["dept_type_rows"].append({
"label": "Total", "total": total_all, "pct": 1,
"monthly": [dept_type_breakdown[m]["total"] for m in active_months], "is_total": True,
})
context["dept_monthly_rows"] = []
for mr in dept_type_monthly["months"]:
context["dept_monthly_rows"].append({
"month": mr["month"], "medical": mr["medical"], "admin": mr["admin"],
"nursing": mr["nursing"], "support": mr["support"], "total": mr["total"],
"is_total": False, "is_pct": False,
})
context["dept_monthly_rows"].append({
"month": "TOTAL",
"medical": dept_type_monthly["totals"]["medical"], "admin": dept_type_monthly["totals"]["admin"],
"nursing": dept_type_monthly["totals"]["nursing"], "support": dept_type_monthly["totals"]["support"],
"total": dept_type_monthly["totals"]["total"],
"is_total": True, "is_pct": False,
})
context["dept_monthly_rows"].append({
"month": "% From total",
"medical": dept_type_monthly["percentages"]["medical"], "admin": dept_type_monthly["percentages"]["admin"],
"nursing": dept_type_monthly["percentages"]["nursing"], "support": dept_type_monthly["percentages"]["support"],
"total": dept_type_monthly["percentages"]["total"],
"is_total": False, "is_pct": True,
})
context["location_ratio_tables"] = _build_location_ratio_tables(location_ratios, active_months)
context["monthly_summary_json"] = json.dumps(monthly_summary)
context["dept_type_rows_json"] = json.dumps(context["dept_type_rows"])
per_dept = service.get_per_department_breakdown()
cat_configs = [
("Medical", "medical"),
("Non-Medical", "non_medical"),
("Nursing", "nursing"),
("Support Services", "support_services"),
]
escalated_by_cat = escalated["by_category"]
escalated_table = []
for cat_label, cat_key in cat_configs:
depts = escalated_by_cat.get(cat_key, {})
tc = sum(d["total"] for d in per_dept["categories"].get(cat_key, []))
esc_total = sum(depts.values())
sorted_depts = sorted(depts.items(), key=lambda x: -x[1])
escalated_table.append({
"label": cat_label,
"sub_depts": [{"name": n, "count": c} for n, c in sorted_depts],
"total_escalated": esc_total,
"total_complaints": tc,
"escalation_rate": round(esc_total / tc, 3) if tc else 0,
})
context["escalated_table"] = escalated_table
context["escalated_json"] = json.dumps({
"by_source": escalated.get("by_source", {}),
"total_escalated": escalated.get("total_escalated", 0),
"total_complaints": escalated.get("total_complaints", 0),
"escalation_rate": escalated.get("escalation_rate", 0),
"by_category_rates": {e["label"]: e["escalation_rate"] for e in escalated_table},
"by_category_counts": {e["label"]: e["total_escalated"] for e in escalated_table},
})
context["per_dept"] = per_dept
context["per_dept_json"] = json.dumps({
"categories": {k: v for k, v in per_dept["categories"].items()},
"source_totals": per_dept["source_totals"],
"avg_response_days": per_dept.get("avg_response_days", {}),
})
dept_category_tables = []
for cat_label, cat_key in cat_configs:
depts = per_dept["categories"].get(cat_key, [])
totals = {
"moh": sum(d["moh"] for d in depts),
"chi": sum(d["chi"] for d in depts),
"insurance": sum(d["insurance"] for d in depts),
"internal": sum(d["internal"] for d in depts),
"total": sum(d["total"] for d in depts),
"escalated": sum(d["escalated"] for d in depts),
}
dept_category_tables.append({"label": cat_label, "depts": depts, "totals": totals})
context["dept_category_tables"] = dept_category_tables
st = per_dept["source_totals"]
context["per_dept_source_rows"] = [
{"label": "MOH", "count": st["moh"], "is_total": False},
{"label": "CHI", "count": st["chi"], "is_total": False},
{"label": "Internal", "count": st["internal"], "is_total": False},
{"label": "Insurance Co.", "count": st["insurance"], "is_total": False},
{"label": "Total", "count": st["total"], "is_total": True},
]
context["response_rates_json"] = json.dumps(response_rates)
context["response_rate_source_list"] = [
{"label": "Internal Complaints", "total": response_rates["internal"]["total"], "individual": response_rates["internal"].get("individual", [])},
{"label": "MOH Complaints", "total": response_rates["moh"]["total"], "individual": response_rates["moh"].get("individual", [])},
{"label": "CHI Complaints", "total": response_rates["chi"]["total"], "individual": response_rates["chi"].get("individual", [])},
]
return render(request, "dashboard/complaint_quarterly_report.html", context)
@login_required
def complaint_quarterly_export(request):
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from apps.dashboard.services.complaint_quarterly_service import ComplaintQuarterlyService
from apps.dashboard.services.complaint_quarterly_export import generate_complaint_quarterly_excel
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin()):
raise PermissionDenied("Only PX Admins and Hospital Admins can access.")
hospital_id = request.GET.get("hospital")
start_date_str = request.GET.get("start_date")
end_date_str = request.GET.get("end_date")
if user.is_hospital_admin() and user.hospital:
hospital_id = hospital_id or str(user.hospital.id)
elif user.is_px_admin() and request.tenant_hospital:
hospital_id = hospital_id or str(request.tenant_hospital.id)
if not hospital_id:
return HttpResponse("Hospital parameter required", status=400)
from datetime import date as _date
current_year = timezone.now().year
if start_date_str and end_date_str:
start_date = _date.fromisoformat(start_date_str)
end_date = _date.fromisoformat(end_date_str)
else:
start_date = _date(current_year, 1, 1)
end_date = _date(current_year, 12, 31)
service = ComplaintQuarterlyService(hospital_id=hospital_id, start_date=start_date, end_date=end_date)
buf = generate_complaint_quarterly_excel(service)
response = HttpResponse(
buf.getvalue(),
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
date_label = f"{start_date.strftime('%Y%m%d')}-{end_date.strftime('%Y%m%d')}"
response["Content-Disposition"] = f'attachment; filename="complaints_{date_label}.xlsx"'
return response
@login_required
def comments_report(request):
import json
from django.core.exceptions import PermissionDenied
from apps.dashboard.services.comments_report_service import CommentsReportService
from apps.organizations.models import Hospital
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee() or user.is_px_management()):
raise PermissionDenied("Only PX Admins, Hospital Admins, and PX Employee and PX Management can access.")
hospital_id = request.GET.get("hospital")
period = request.GET.get("period", "monthly")
year = request.GET.get("year")
quarter = request.GET.get("quarter")
if user.is_hospital_admin() and user.hospital:
hospital_id = hospital_id or str(user.hospital.id)
elif user.is_px_admin() and request.tenant_hospital:
hospital_id = hospital_id or str(request.tenant_hospital.id)
if not hospital_id:
first = Hospital.objects.first()
if first:
hospital_id = str(first.id)
if not hospital_id:
return render(request, "dashboard/comments_report.html", {"no_data": True})
year = int(year) if year else timezone.now().year
quarter = int(quarter) if quarter else ((timezone.now().month - 1) // 3 + 1)
service = CommentsReportService(hospital_id=hospital_id, year=year, quarter=quarter)
available_periods = service.get_available_periods()
summary = service.get_summary()
by_category = service.get_category_breakdown()
by_sentiment = service.get_sentiment_breakdown()
by_subcategory = service.get_subcategory_breakdown()
action_plans = service.get_action_plans()
chart_data = service.get_chart_data()
fmt = lambda n: f"{n:,}"
context = {
"hospitals": Hospital.objects.all(),
"selected_hospital": hospital_id,
"selected_year": year,
"selected_quarter": quarter,
"available_periods": available_periods,
"summary": summary,
"by_category": by_category,
"by_sentiment": by_sentiment,
"by_subcategory": by_subcategory,
"action_plans": action_plans,
"chart_data_json": json.dumps(chart_data),
}
return render(request, "dashboard/comments_report.html", context)
@login_required
def standards_dashboard(request):
from django.core.exceptions import PermissionDenied
from apps.dashboard.services.standards_report_service import StandardsReportService
from apps.standards.models import StandardSource
from apps.organizations.models import Hospital
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee() or user.is_px_management()):
raise PermissionDenied("Only PX Admins, Hospital Admins, and PX Employee and PX Management can access.")
hospital_id = request.GET.get("hospital")
source_code = request.GET.get("source", "")
if user.is_hospital_admin() and user.hospital:
hospital_id = hospital_id or str(user.hospital.id)
elif user.is_px_admin() and request.tenant_hospital:
hospital_id = hospital_id or str(request.tenant_hospital.id)
if not hospital_id:
first = Hospital.objects.first()
if first:
hospital_id = str(first.id)
if not hospital_id:
return render(request, "dashboard/standards_dashboard.html", {"no_data": True})
service = StandardsReportService(hospital_id=hospital_id, source_code=source_code or None)
summary = service.get_summary()
if summary["total"] == 0:
return render(request, "dashboard/standards_dashboard.html", {
"no_data": True,
"hospitals": Hospital.objects.all(),
"selected_hospital": hospital_id,
"sources": StandardSource.objects.filter(is_active=True),
"selected_source": source_code,
})
category_breakdown = service.get_category_breakdown()
corrective_actions = service.get_corrective_actions()
standards_table = service.get_standards_table()
score_gauge = service.get_score_gauge_data()
import json
context = {
"hospitals": Hospital.objects.all(),
"selected_hospital": hospital_id,
"sources": StandardSource.objects.filter(is_active=True),
"selected_source": source_code,
"summary": summary,
"category_breakdown": category_breakdown,
"corrective_actions": corrective_actions,
"standards_table": standards_table,
"score_gauge": score_gauge,
"chart_data_json": service.get_all_chart_data(),
}
return render(request, "dashboard/standards_dashboard.html", context)
@login_required
def my_performance(request):
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
from apps.organizations.models import Staff
user = request.user
staff_member = Staff.objects.filter(user=user).select_related("department", "hospital").first()
if not staff_member:
messages.error(request, _("No staff profile found for your account."))
return redirect("dashboard:my_dashboard")
date_range = request.GET.get("date_range", "30d")
custom_start = request.GET.get("custom_start")
custom_end = request.GET.get("custom_end")
if custom_start:
from datetime import datetime
custom_start = datetime.fromisoformat(custom_start)
if custom_end:
from datetime import datetime
custom_end = datetime.fromisoformat(custom_end)
start_date, end_date = UnifiedAnalyticsService._get_date_range(date_range, custom_start, custom_end)
performance = UnifiedAnalyticsService.get_staff_detailed_performance(
staff_id=str(staff_member.user_id),
user=user,
date_range=date_range,
custom_start=custom_start,
custom_end=custom_end,
)
trends = UnifiedAnalyticsService.get_staff_performance_trends(
staff_id=str(staff_member.user_id), user=user, months=6
)
if staff_member.department:
benchmarks = UnifiedAnalyticsService.get_department_benchmarks(
user=user,
department_id=str(staff_member.department.id),
date_range=date_range,
)
else:
benchmarks = None
context = {
"staff_member": staff_member,
"performance": performance,
"trends": trends,
"benchmarks": benchmarks,
"date_range": date_range,
"start_date": start_date,
"end_date": end_date,
}
return render(request, "dashboard/my_performance.html", context)