HH/apps/analytics/kpi_views.py
2026-02-22 08:35:53 +03:00

445 lines
14 KiB
Python

"""
KPI Report Views
Views for listing, viewing, and generating KPI reports.
Follows the PX360 UI patterns with Tailwind, Lucide icons, and HTMX.
"""
import json
from datetime import datetime
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST
from apps.organizations.models import Hospital
from .kpi_models import KPIReport, KPIReportStatus, KPIReportType
from .kpi_service import KPICalculationService
@login_required
def kpi_report_list(request):
"""
KPI Report list view
Shows all KPI reports with filtering by:
- Report type
- Hospital
- Year/Month
- Status
"""
user = request.user
# Base queryset
queryset = KPIReport.objects.select_related('hospital', 'generated_by')
# Apply hospital filter based on user role
if not user.is_px_admin():
if user.hospital:
queryset = queryset.filter(hospital=user.hospital)
else:
queryset = KPIReport.objects.none()
# Apply filters from request
report_type = request.GET.get('report_type')
if report_type:
queryset = queryset.filter(report_type=report_type)
hospital_filter = request.GET.get('hospital')
if hospital_filter and user.is_px_admin():
queryset = queryset.filter(hospital_id=hospital_filter)
year = request.GET.get('year')
if year:
queryset = queryset.filter(year=year)
month = request.GET.get('month')
if month:
queryset = queryset.filter(month=month)
status = request.GET.get('status')
if status:
queryset = queryset.filter(status=status)
# Ordering
queryset = queryset.order_by('-year', '-month', 'report_type')
# Calculate statistics
stats = {
'total': queryset.count(),
'completed': queryset.filter(status='completed').count(),
'pending': queryset.filter(status__in=['pending', 'generating']).count(),
'failed': queryset.filter(status='failed').count(),
}
# Pagination
page_size = int(request.GET.get('page_size', 12))
paginator = Paginator(queryset, page_size)
page_number = request.GET.get('page', 1)
page_obj = paginator.get_page(page_number)
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
current_year = datetime.now().year
years = list(range(current_year, current_year - 5, -1))
context = {
'page_obj': page_obj,
'reports': page_obj.object_list,
'filters': request.GET,
'stats': stats,
'hospitals': hospitals,
'years': years,
'months': [
(1, _('January')), (2, _('February')), (3, _('March')),
(4, _('April')), (5, _('May')), (6, _('June')),
(7, _('July')), (8, _('August')), (9, _('September')),
(10, _('October')), (11, _('November')), (12, _('December')),
],
'report_types': KPIReportType.choices,
'statuses': KPIReportStatus.choices,
}
return render(request, 'analytics/kpi_report_list.html', context)
@login_required
def kpi_report_detail(request, report_id):
"""
KPI Report detail view
Shows the full report with:
- Excel-style data table
- Charts (trend and source distribution)
- Department breakdown
- PDF export option
"""
user = request.user
report = get_object_or_404(
KPIReport.objects.select_related('hospital', 'generated_by'),
id=report_id
)
# Check permissions
if not user.is_px_admin() and user.hospital != report.hospital:
messages.error(request, _('You do not have permission to view this report.'))
return redirect('analytics:kpi_report_list')
# Get monthly data (1-12)
monthly_data_qs = report.monthly_data.filter(month__gt=0).order_by('month')
total_data = report.monthly_data.filter(month=0).first()
# Build monthly data array ensuring 12 months
monthly_data_dict = {m.month: m for m in monthly_data_qs}
monthly_data = [monthly_data_dict.get(i) for i in range(1, 13)]
# Get source breakdowns for pie chart
source_breakdowns = report.source_breakdowns.all()
source_chart_data = {
'labels': [s.source_name for s in source_breakdowns] or ['No Data'],
'data': [float(s.percentage) for s in source_breakdowns] or [100],
}
# Get department breakdowns
department_breakdowns = report.department_breakdowns.all()
# Prepare trend chart data - ensure we have 12 values
trend_data_values = []
for m in monthly_data:
if m:
trend_data_values.append(float(m.percentage))
else:
trend_data_values.append(0.0)
trend_chart_data = {
'labels': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
'data': trend_data_values,
'target': float(report.target_percentage) if report.target_percentage else 95.0,
}
context = {
'report': report,
'monthly_data': monthly_data,
'total_data': total_data,
'source_breakdowns': source_breakdowns,
'department_breakdowns': department_breakdowns,
'source_chart_data_json': json.dumps(source_chart_data),
'trend_chart_data_json': json.dumps(trend_chart_data),
}
return render(request, 'analytics/kpi_report_detail.html', context)
@login_required
def kpi_report_generate(request):
"""
KPI Report generation form
Allows manual generation of KPI reports for a specific
month and hospital.
"""
user = request.user
# Get filter options
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin():
if user.hospital:
hospitals = hospitals.filter(id=user.hospital.id)
else:
hospitals = Hospital.objects.none()
current_year = datetime.now().year
years = list(range(current_year, current_year - 3, -1))
context = {
'hospitals': hospitals,
'years': years,
'months': [
(1, _('January')), (2, _('February')), (3, _('March')),
(4, _('April')), (5, _('May')), (6, _('June')),
(7, _('July')), (8, _('August')), (9, _('September')),
(10, _('October')), (11, _('November')), (12, _('December')),
],
'report_types': KPIReportType.choices,
}
return render(request, 'analytics/kpi_report_generate.html', context)
@login_required
@require_POST
def kpi_report_generate_submit(request):
"""
Handle KPI report generation form submission
"""
user = request.user
report_type = request.POST.get('report_type')
hospital_id = request.POST.get('hospital')
year = request.POST.get('year')
month = request.POST.get('month')
# Validation
if not all([report_type, hospital_id, year, month]):
if request.headers.get('HX-Request'):
return render(request, 'analytics/partials/kpi_generate_error.html', {
'error': _('All fields are required.')
})
messages.error(request, _('All fields are required.'))
return redirect('analytics:kpi_report_generate')
# Check permissions
try:
hospital = Hospital.objects.get(id=hospital_id)
except Hospital.DoesNotExist:
if request.headers.get('HX-Request'):
return render(request, 'analytics/partials/kpi_generate_error.html', {
'error': _('Hospital not found.')
})
messages.error(request, _('Hospital not found.'))
return redirect('analytics:kpi_report_generate')
if not user.is_px_admin() and user.hospital != hospital:
if request.headers.get('HX-Request'):
return render(request, 'analytics/partials/kpi_generate_error.html', {
'error': _('You do not have permission to generate reports for this hospital.')
})
messages.error(request, _('You do not have permission to generate reports for this hospital.'))
return redirect('analytics:kpi_report_generate')
try:
year = int(year)
month = int(month)
# Generate the report
report = KPICalculationService.generate_monthly_report(
report_type=report_type,
hospital=hospital,
year=year,
month=month,
generated_by=user
)
success_message = _('KPI Report generated successfully.')
if request.headers.get('HX-Request'):
return render(request, 'analytics/partials/kpi_generate_success.html', {
'report': report,
'message': success_message
})
messages.success(request, success_message)
return redirect('analytics:kpi_report_detail', report_id=report.id)
except Exception as e:
error_message = str(e)
if request.headers.get('HX-Request'):
return render(request, 'analytics/partials/kpi_generate_error.html', {
'error': error_message
})
messages.error(request, error_message)
return redirect('analytics:kpi_report_generate')
@login_required
@require_POST
def kpi_report_regenerate(request, report_id):
"""
Regenerate an existing KPI report
"""
user = request.user
report = get_object_or_404(KPIReport, id=report_id)
# Check permissions
if not user.is_px_admin() and user.hospital != report.hospital:
messages.error(request, _('You do not have permission to regenerate this report.'))
return redirect('analytics:kpi_report_list')
try:
# Regenerate the report
KPICalculationService.generate_monthly_report(
report_type=report.report_type,
hospital=report.hospital,
year=report.year,
month=report.month,
generated_by=user
)
messages.success(request, _('KPI Report regenerated successfully.'))
except Exception as e:
messages.error(request, str(e))
return redirect('analytics:kpi_report_detail', report_id=report.id)
@login_required
def kpi_report_pdf(request, report_id):
"""
Generate PDF version of KPI report
Returns HTML page with print-friendly styling and
html2pdf.js for client-side PDF generation.
"""
user = request.user
report = get_object_or_404(
KPIReport.objects.select_related('hospital', 'generated_by'),
id=report_id
)
# Check permissions
if not user.is_px_admin() and user.hospital != report.hospital:
messages.error(request, _('You do not have permission to view this report.'))
return redirect('analytics:kpi_report_list')
# Get monthly data (1-12)
monthly_data_qs = report.monthly_data.filter(month__gt=0).order_by('month')
total_data = report.monthly_data.filter(month=0).first()
# Build monthly data array ensuring 12 months
monthly_data_dict = {m.month: m for m in monthly_data_qs}
monthly_data = [monthly_data_dict.get(i) for i in range(1, 13)]
# Get source breakdowns for pie chart
source_breakdowns = report.source_breakdowns.all()
source_chart_data = {
'labels': [s.source_name for s in source_breakdowns] or ['No Data'],
'data': [float(s.percentage) for s in source_breakdowns] or [100],
}
# Get department breakdowns
department_breakdowns = report.department_breakdowns.all()
# Prepare trend chart data - ensure we have 12 values
trend_data_values = []
for m in monthly_data:
if m:
trend_data_values.append(float(m.percentage))
else:
trend_data_values.append(0.0)
trend_chart_data = {
'labels': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
'data': trend_data_values,
'target': float(report.target_percentage) if report.target_percentage else 95.0,
}
context = {
'report': report,
'monthly_data': monthly_data,
'total_data': total_data,
'source_breakdowns': source_breakdowns,
'department_breakdowns': department_breakdowns,
'source_chart_data_json': json.dumps(source_chart_data),
'trend_chart_data_json': json.dumps(trend_chart_data),
'is_pdf': True,
}
return render(request, 'analytics/kpi_report_pdf.html', context)
@login_required
def kpi_report_api_data(request, report_id):
"""
API endpoint for KPI report data (for charts)
"""
user = request.user
report = get_object_or_404(KPIReport, id=report_id)
# Check permissions
if not user.is_px_admin() and user.hospital != report.hospital:
return JsonResponse({'error': 'Permission denied'}, status=403)
# Get monthly data
monthly_data = report.monthly_data.filter(month__gt=0).order_by('month')
# Get source breakdowns
source_breakdowns = report.source_breakdowns.all()
data = {
'report': {
'id': str(report.id),
'type': report.report_type,
'type_display': report.get_report_type_display(),
'year': report.year,
'month': report.month,
'kpi_id': report.kpi_id,
'indicator_title': report.indicator_title,
'target_percentage': float(report.target_percentage),
'overall_result': float(report.overall_result),
},
'monthly_data': [
{
'month': m.month,
'month_name': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][m.month - 1],
'numerator': m.numerator,
'denominator': m.denominator,
'percentage': float(m.percentage),
'is_below_target': m.is_below_target,
}
for m in monthly_data
],
'source_breakdown': [
{
'source': s.source_name,
'count': s.complaint_count,
'percentage': float(s.percentage),
}
for s in source_breakdowns
],
}
return JsonResponse(data)