""" 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)