2882 lines
118 KiB
Python
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)
|