""" KPI Report Views Views for listing, viewing, and generating KPI reports. Follows the PX360 UI patterns with Tailwind, Lucide icons, and HTMX. """ import json import logging from datetime import datetime from django.contrib import messages logger = logging.getLogger(__name__) 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 import timezone 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) @login_required def kpi_report_ai_analysis(request, report_id): """ Generate or retrieve AI analysis for a KPI report. GET: Retrieve existing AI analysis POST: Generate new AI analysis """ from django.http import JsonResponse from .kpi_service import KPICalculationService 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: return JsonResponse({'error': 'Permission denied'}, status=403) if request.method == 'GET': # Return existing analysis if report.ai_analysis: return JsonResponse({ 'success': True, 'analysis': report.ai_analysis, 'generated_at': report.ai_analysis_generated_at.isoformat() if report.ai_analysis_generated_at else None }) else: return JsonResponse({ 'success': False, 'message': 'No AI analysis available. Use POST to generate.' }, status=404) elif request.method == 'POST': # Generate new analysis try: analysis = KPICalculationService.generate_ai_analysis(report) if 'error' in analysis: return JsonResponse({ 'success': False, 'error': analysis['error'] }, status=500) return JsonResponse({ 'success': True, 'analysis': analysis, 'generated_at': report.ai_analysis_generated_at.isoformat() if report.ai_analysis_generated_at else None }) except Exception as e: return JsonResponse({ 'success': False, 'error': str(e) }, status=500) return JsonResponse({'error': 'Method not allowed'}, status=405) @login_required def kpi_report_save_analysis(request, report_id): """ Save edited AI analysis for a KPI report. POST: Save edited analysis JSON """ import json from django.http import JsonResponse user = request.user report = get_object_or_404(KPIReport, id=report_id) # Check permissions - only PX admins and hospital admins can edit if not user.is_px_admin() and user.hospital != report.hospital: return JsonResponse({'error': 'Permission denied'}, status=403) if request.method != 'POST': return JsonResponse({'error': 'Method not allowed'}, status=405) try: # Parse the edited analysis from request body body = json.loads(request.body) edited_analysis = body.get('analysis') if not edited_analysis: return JsonResponse({'error': 'No analysis data provided'}, status=400) # Preserve metadata if it exists if report.ai_analysis and '_metadata' in report.ai_analysis: edited_analysis['_metadata'] = report.ai_analysis['_metadata'] edited_analysis['_metadata']['last_edited_at'] = timezone.now().isoformat() edited_analysis['_metadata']['last_edited_by'] = user.get_full_name() or user.email else: edited_analysis['_metadata'] = { 'generated_at': timezone.now().isoformat(), 'report_id': str(report.id), 'report_type': report.report_type, 'hospital': report.hospital.name, 'year': report.year, 'month': report.month, 'last_edited_at': timezone.now().isoformat(), 'last_edited_by': user.get_full_name() or user.email } # Save to report report.ai_analysis = edited_analysis report.save(update_fields=['ai_analysis']) logger.info(f"AI analysis edited for KPI report {report.id} by user {user.id}") return JsonResponse({ 'success': True, 'message': 'Analysis saved successfully' }) except json.JSONDecodeError: return JsonResponse({'error': 'Invalid JSON data'}, status=400) except Exception as e: logger.exception(f"Error saving AI analysis for report {report.id}: {e}") return JsonResponse({'error': str(e)}, status=500)