1734 lines
70 KiB
Python
1734 lines
70 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"""
|
|
# Redirect Source Users to their dashboard
|
|
if request.user.is_authenticated and request.user.is_source_user():
|
|
return redirect("px_sources:source_user_dashboard")
|
|
|
|
# Check PX Admin has selected a hospital before processing request
|
|
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.social.models import SocialMediaComment
|
|
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.all() # Surveys can be viewed across hospitals
|
|
social_qs = SocialMediaComment.objects.all() # Social media is organization-wide, not hospital-specific
|
|
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)
|
|
social_qs = SocialMediaComment.objects.all() # Social media is organization-wide, not hospital-specific
|
|
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_stage_instance__department=user.department)
|
|
social_qs = SocialMediaComment.objects.all() # Social media is organization-wide, not department-specific
|
|
calls_qs = CallCenterInteraction.objects.filter(department=user.department)
|
|
observations_qs = Observation.objects.filter(assigned_department=user.department)
|
|
else:
|
|
complaints_qs = Complaint.objects.none()
|
|
inquiries_qs = Inquiry.objects.none()
|
|
actions_qs = PXAction.objects.none()
|
|
surveys_qs = SurveyInstance.objects.none()
|
|
social_qs = SocialMediaComment.objects.all() # Show all social media comments
|
|
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)
|
|
|
|
# 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)
|
|
|
|
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, # For hospital filter display
|
|
}
|
|
|
|
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")
|
|
|
|
# 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.all()
|
|
|
|
if user.is_px_admin() and hospital_id:
|
|
staff_queryset = staff_queryset.filter(hospital_id=hospital_id)
|
|
elif not user.is_px_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)
|
|
|
|
# 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,
|
|
}
|
|
|
|
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")
|
|
|
|
# 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_stage_instance__department=user.department)
|
|
observations_qs = Observation.objects.filter(assigned_department=user.department)
|
|
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")
|
|
|
|
# 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.all()
|
|
|
|
if user.is_px_admin() and hospital_id:
|
|
staff_queryset = staff_queryset.filter(hospital_id=hospital_id)
|
|
elif not user.is_px_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)
|
|
|
|
# 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,
|
|
}
|
|
|
|
return render(request, "dashboard/employee_evaluation.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")
|
|
|
|
# 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).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,
|
|
)
|