307 lines
9.7 KiB
Python
307 lines
9.7 KiB
Python
"""
|
|
Complaints analytics service
|
|
|
|
Provides analytics and metrics for complaints dashboard integration.
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Optional
|
|
|
|
from django.db.models import Count, Q, Avg, F, ExpressionWrapper, DurationField
|
|
from django.utils import timezone
|
|
|
|
from apps.complaints.models import Complaint, ComplaintStatus
|
|
|
|
|
|
class ComplaintAnalytics:
|
|
"""Service for complaint analytics and metrics"""
|
|
|
|
@staticmethod
|
|
def get_complaint_trends(hospital=None, date_range=30):
|
|
"""
|
|
Get complaint trends over time.
|
|
|
|
Args:
|
|
hospital: Optional hospital to filter by
|
|
date_range: Number of days to analyze (default 30)
|
|
|
|
Returns:
|
|
dict: Trend data with dates and counts
|
|
"""
|
|
end_date = timezone.now()
|
|
start_date = end_date - timedelta(days=date_range)
|
|
|
|
queryset = Complaint.objects.filter(
|
|
created_at__gte=start_date,
|
|
created_at__lte=end_date
|
|
)
|
|
|
|
if hospital:
|
|
queryset = queryset.filter(hospital=hospital)
|
|
|
|
# Group by date
|
|
trends = queryset.extra(
|
|
select={'date': 'DATE(created_at)'}
|
|
).values('date').annotate(
|
|
count=Count('id')
|
|
).order_by('date')
|
|
|
|
return {
|
|
'labels': [item['date'].strftime('%Y-%m-%d') for item in trends],
|
|
'data': [item['count'] for item in trends],
|
|
'total': queryset.count()
|
|
}
|
|
|
|
@staticmethod
|
|
def get_sla_compliance(hospital=None, date_range=30):
|
|
"""
|
|
Calculate SLA compliance metrics.
|
|
|
|
Args:
|
|
hospital: Optional hospital to filter by
|
|
date_range: Number of days to analyze
|
|
|
|
Returns:
|
|
dict: SLA compliance data
|
|
"""
|
|
end_date = timezone.now()
|
|
start_date = end_date - timedelta(days=date_range)
|
|
|
|
queryset = Complaint.objects.filter(
|
|
created_at__gte=start_date,
|
|
created_at__lte=end_date
|
|
)
|
|
|
|
if hospital:
|
|
queryset = queryset.filter(hospital=hospital)
|
|
|
|
total = queryset.count()
|
|
overdue = queryset.filter(is_overdue=True).count()
|
|
on_time = total - overdue
|
|
|
|
compliance_rate = (on_time / total * 100) if total > 0 else 0
|
|
|
|
# Get trend data
|
|
daily_compliance = []
|
|
for i in range(date_range):
|
|
day = start_date + timedelta(days=i)
|
|
day_end = day + timedelta(days=1)
|
|
|
|
day_total = queryset.filter(
|
|
created_at__gte=day,
|
|
created_at__lt=day_end
|
|
).count()
|
|
|
|
day_overdue = queryset.filter(
|
|
created_at__gte=day,
|
|
created_at__lt=day_end,
|
|
is_overdue=True
|
|
).count()
|
|
|
|
day_compliance = ((day_total - day_overdue) / day_total * 100) if day_total > 0 else 100
|
|
|
|
daily_compliance.append({
|
|
'date': day.strftime('%Y-%m-%d'),
|
|
'compliance_rate': round(day_compliance, 2),
|
|
'total': day_total,
|
|
'overdue': day_overdue
|
|
})
|
|
|
|
return {
|
|
'overall_compliance_rate': round(compliance_rate, 2),
|
|
'total_complaints': total,
|
|
'on_time': on_time,
|
|
'overdue': overdue,
|
|
'daily_compliance': daily_compliance
|
|
}
|
|
|
|
@staticmethod
|
|
def get_resolution_rate(hospital=None, date_range=30):
|
|
"""
|
|
Calculate resolution rate metrics.
|
|
|
|
Args:
|
|
hospital: Optional hospital to filter by
|
|
date_range: Number of days to analyze
|
|
|
|
Returns:
|
|
dict: Resolution rate data
|
|
"""
|
|
end_date = timezone.now()
|
|
start_date = end_date - timedelta(days=date_range)
|
|
|
|
queryset = Complaint.objects.filter(
|
|
created_at__gte=start_date,
|
|
created_at__lte=end_date
|
|
)
|
|
|
|
if hospital:
|
|
queryset = queryset.filter(hospital=hospital)
|
|
|
|
total = queryset.count()
|
|
resolved = queryset.filter(
|
|
status__in=[ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED]
|
|
).count()
|
|
|
|
resolution_rate = (resolved / total * 100) if total > 0 else 0
|
|
|
|
# Calculate average resolution time
|
|
resolved_complaints = queryset.filter(
|
|
status__in=[ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED],
|
|
resolved_at__isnull=False
|
|
)
|
|
|
|
avg_resolution_time = None
|
|
if resolved_complaints.exists():
|
|
# Calculate time difference
|
|
time_diffs = []
|
|
for complaint in resolved_complaints:
|
|
if complaint.resolved_at:
|
|
diff = (complaint.resolved_at - complaint.created_at).total_seconds() / 3600 # hours
|
|
time_diffs.append(diff)
|
|
|
|
if time_diffs:
|
|
avg_resolution_time = sum(time_diffs) / len(time_diffs)
|
|
|
|
# Resolution by department
|
|
by_department = queryset.filter(
|
|
status__in=[ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED],
|
|
department__isnull=False
|
|
).values(
|
|
'department__name_en'
|
|
).annotate(
|
|
count=Count('id')
|
|
).order_by('-count')[:10]
|
|
|
|
return {
|
|
'resolution_rate': round(resolution_rate, 2),
|
|
'total_complaints': total,
|
|
'resolved': resolved,
|
|
'pending': total - resolved,
|
|
'avg_resolution_time_hours': round(avg_resolution_time, 2) if avg_resolution_time else None,
|
|
'by_department': list(by_department)
|
|
}
|
|
|
|
@staticmethod
|
|
def get_top_categories(hospital=None, date_range=30, limit=10):
|
|
"""
|
|
Get top complaint categories.
|
|
|
|
Args:
|
|
hospital: Optional hospital to filter by
|
|
date_range: Number of days to analyze
|
|
limit: Number of top categories to return
|
|
|
|
Returns:
|
|
dict: Top categories data
|
|
"""
|
|
end_date = timezone.now()
|
|
start_date = end_date - timedelta(days=date_range)
|
|
|
|
queryset = Complaint.objects.filter(
|
|
created_at__gte=start_date,
|
|
created_at__lte=end_date
|
|
)
|
|
|
|
if hospital:
|
|
queryset = queryset.filter(hospital=hospital)
|
|
|
|
categories = queryset.values('category').annotate(
|
|
count=Count('id')
|
|
).order_by('-count')[:limit]
|
|
|
|
return {
|
|
'categories': [
|
|
{
|
|
'category': item['category'],
|
|
'count': item['count']
|
|
}
|
|
for item in categories
|
|
]
|
|
}
|
|
|
|
@staticmethod
|
|
def get_overdue_complaints(hospital=None, limit=10):
|
|
"""
|
|
Get list of overdue complaints.
|
|
|
|
Args:
|
|
hospital: Optional hospital to filter by
|
|
limit: Number of complaints to return
|
|
|
|
Returns:
|
|
QuerySet: Overdue complaints
|
|
"""
|
|
queryset = Complaint.objects.filter(
|
|
is_overdue=True,
|
|
status__in=[ComplaintStatus.OPEN, ComplaintStatus.IN_PROGRESS]
|
|
).select_related(
|
|
'patient', 'hospital', 'department', 'assigned_to'
|
|
).order_by('due_at')
|
|
|
|
if hospital:
|
|
queryset = queryset.filter(hospital=hospital)
|
|
|
|
return queryset[:limit]
|
|
|
|
@staticmethod
|
|
def get_dashboard_summary(hospital=None):
|
|
"""
|
|
Get comprehensive dashboard summary.
|
|
|
|
Args:
|
|
hospital: Optional hospital to filter by
|
|
|
|
Returns:
|
|
dict: Dashboard summary data
|
|
"""
|
|
queryset = Complaint.objects.all()
|
|
|
|
if hospital:
|
|
queryset = queryset.filter(hospital=hospital)
|
|
|
|
# Current status counts
|
|
status_counts = {
|
|
'total': queryset.count(),
|
|
'open': queryset.filter(status=ComplaintStatus.OPEN).count(),
|
|
'in_progress': queryset.filter(status=ComplaintStatus.IN_PROGRESS).count(),
|
|
'resolved': queryset.filter(status=ComplaintStatus.RESOLVED).count(),
|
|
'closed': queryset.filter(status=ComplaintStatus.CLOSED).count(),
|
|
'overdue': queryset.filter(is_overdue=True).count(),
|
|
}
|
|
|
|
# Severity breakdown
|
|
severity_counts = queryset.values('severity').annotate(
|
|
count=Count('id')
|
|
)
|
|
|
|
# Recent high severity
|
|
recent_high_severity = queryset.filter(
|
|
severity__in=['high', 'critical'],
|
|
created_at__gte=timezone.now() - timedelta(days=7)
|
|
).count()
|
|
|
|
# Trends (last 7 days vs previous 7 days)
|
|
last_7_days = queryset.filter(
|
|
created_at__gte=timezone.now() - timedelta(days=7)
|
|
).count()
|
|
|
|
previous_7_days = queryset.filter(
|
|
created_at__gte=timezone.now() - timedelta(days=14),
|
|
created_at__lt=timezone.now() - timedelta(days=7)
|
|
).count()
|
|
|
|
trend_percentage = 0
|
|
if previous_7_days > 0:
|
|
trend_percentage = ((last_7_days - previous_7_days) / previous_7_days) * 100
|
|
|
|
return {
|
|
'status_counts': status_counts,
|
|
'severity_counts': list(severity_counts),
|
|
'recent_high_severity': recent_high_severity,
|
|
'trend': {
|
|
'last_7_days': last_7_days,
|
|
'previous_7_days': previous_7_days,
|
|
'percentage_change': round(trend_percentage, 2)
|
|
}
|
|
}
|