445 lines
14 KiB
Python
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)
|