1015 lines
38 KiB
Python
1015 lines
38 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
|
|
from django.utils import timezone
|
|
from django.views.generic import TemplateView
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
|
|
class CommandCenterView(LoginRequiredMixin, TemplateView):
|
|
"""
|
|
PX Command Center Dashboard - Real-time control panel.
|
|
|
|
Shows:
|
|
- Top KPI cards (complaints, actions, surveys, etc.)
|
|
- Charts (trends, satisfaction, leaderboards)
|
|
- Live feed (latest complaints, actions, events)
|
|
- Filters (date range, hospital, department)
|
|
"""
|
|
template_name = 'dashboard/command_center.html'
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
"""Check PX Admin has selected a hospital before processing request"""
|
|
# Only check hospital selection for authenticated users
|
|
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
|
|
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
|
|
|
|
# Date filters
|
|
now = timezone.now()
|
|
last_24h = now - timedelta(hours=24)
|
|
last_7d = now - timedelta(days=7)
|
|
last_30d = now - timedelta(days=30)
|
|
|
|
# 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()
|
|
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()
|
|
elif user.is_hospital_admin() and user.hospital:
|
|
complaints_qs = Complaint.objects.filter(hospital=user.hospital)
|
|
actions_qs = PXAction.objects.filter(hospital=user.hospital)
|
|
surveys_qs = SurveyInstance.objects.filter(survey_template__hospital=user.hospital)
|
|
social_qs = SocialMediaComment.objects.all() # Social media is organization-wide, not hospital-specific
|
|
calls_qs = CallCenterInteraction.objects.filter(hospital=user.hospital)
|
|
elif user.is_department_manager() and user.department:
|
|
complaints_qs = Complaint.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)
|
|
else:
|
|
complaints_qs = Complaint.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()
|
|
|
|
# Top KPI Stats
|
|
context['stats'] = [
|
|
{
|
|
'label': _("Active Complaints"),
|
|
'value': complaints_qs.filter(status__in=['open', 'in_progress']).count(),
|
|
'icon': 'exclamation-triangle',
|
|
'color': 'danger'
|
|
},
|
|
{
|
|
'label': _('Overdue Complaints'),
|
|
'value': complaints_qs.filter(is_overdue=True, status__in=['open', 'in_progress']).count(),
|
|
'icon': 'clock-history',
|
|
'color': 'warning'
|
|
},
|
|
{
|
|
'label': _('Open PX Actions'),
|
|
'value': actions_qs.filter(status__in=['open', 'in_progress']).count(),
|
|
'icon': 'clipboard-check',
|
|
'color': 'primary'
|
|
},
|
|
{
|
|
'label': _('Overdue Actions'),
|
|
'value': actions_qs.filter(is_overdue=True, status__in=['open', 'in_progress']).count(),
|
|
'icon': 'alarm',
|
|
'color': 'danger'
|
|
},
|
|
{
|
|
'label': _('Negative Surveys (24h)'),
|
|
'value': surveys_qs.filter(is_negative=True, completed_at__gte=last_24h).count(),
|
|
'icon': 'emoji-frown',
|
|
'color': 'warning'
|
|
},
|
|
{
|
|
'label': _('Negative Social Mentions'),
|
|
'value': sum(
|
|
1 for comment in social_qs.filter(published_at__gte=last_7d)
|
|
if comment.ai_analysis and
|
|
comment.ai_analysis.get('sentiment', {}).get('classification', {}).get('en') == 'negative'
|
|
),
|
|
'icon': 'chat-dots',
|
|
'color': 'danger'
|
|
},
|
|
{
|
|
'label': _('Low Call Center Ratings'),
|
|
'value': calls_qs.filter(is_low_rating=True, call_started_at__gte=last_7d).count(),
|
|
'icon': 'telephone',
|
|
'color': 'warning'
|
|
},
|
|
{
|
|
'label': _('Avg Survey Score'),
|
|
'value': f"{surveys_qs.filter(completed_at__gte=last_30d).aggregate(Avg('total_score'))['total_score__avg'] or 0:.1f}",
|
|
'icon': 'star',
|
|
'color': 'success'
|
|
},
|
|
]
|
|
|
|
# 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 integration events
|
|
context['latest_events'] = InboundEvent.objects.filter(
|
|
status='processed'
|
|
).select_related().order_by('-processed_at')[:10]
|
|
|
|
# Staff ratings data
|
|
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 (simplified for now)
|
|
import json
|
|
context['chart_data'] = {
|
|
'complaints_trend': json.dumps(self.get_complaints_trend(complaints_qs, last_30d)),
|
|
'survey_satisfaction': self.get_survey_satisfaction(surveys_qs, last_30d),
|
|
}
|
|
|
|
# Add hospital context
|
|
context['current_hospital'] = self.request.tenant_hospital
|
|
context['is_px_admin'] = user.is_px_admin()
|
|
|
|
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_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
|
|
"""
|
|
user = request.user
|
|
|
|
# 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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
|
|
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,
|
|
}
|
|
|
|
return render(request, 'dashboard/my_dashboard.html', context)
|
|
|
|
|
|
def get_dashboard_chart_data(user, start_date=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
|
|
completed_count += Complaint.objects.filter(
|
|
assigned_to=user,
|
|
status='closed',
|
|
closed_at__date=date.date()
|
|
).count()
|
|
completed_count += Inquiry.objects.filter(
|
|
assigned_to=user,
|
|
status='closed',
|
|
updated_at__date=date.date()
|
|
).count()
|
|
completed_count += Observation.objects.filter(
|
|
assigned_to=user,
|
|
status='closed',
|
|
updated_at__date=date.date()
|
|
).count()
|
|
completed_count += PXAction.objects.filter(
|
|
assigned_to=user,
|
|
status='closed',
|
|
closed_at__date=date.date()
|
|
).count()
|
|
completed_count += QIProjectTask.objects.filter(
|
|
assigned_to=user,
|
|
status='closed',
|
|
completed_date=date.date()
|
|
).count()
|
|
completed_count += Feedback.objects.filter(
|
|
assigned_to=user,
|
|
status='closed',
|
|
closed_at__date=date.date()
|
|
).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
|
|
"""
|
|
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
|
|
from apps.accounts.models import User
|
|
from apps.organizations.models import Hospital, Department
|
|
|
|
user = request.user
|
|
|
|
# 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 = {
|
|
'hospitals': hospitals,
|
|
'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.
|
|
"""
|
|
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
|
|
|
|
if request.method != 'GET':
|
|
return JsonResponse({'success': False, 'error': 'GET required'}, status=405)
|
|
|
|
user = request.user
|
|
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
|
|
"""
|
|
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
|
|
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.
|
|
"""
|
|
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
|
|
from apps.accounts.models import User
|
|
|
|
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.
|
|
"""
|
|
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
|
|
|
|
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.
|
|
"""
|
|
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
|
|
import csv
|
|
import json
|
|
from django.http import HttpResponse
|
|
|
|
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 performance_analytics_api(request):
|
|
"""
|
|
API endpoint for various performance analytics.
|
|
"""
|
|
from apps.analytics.services.analytics_service import UnifiedAnalyticsService
|
|
|
|
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)
|