583 lines
19 KiB
Python
583 lines
19 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
|
|
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()
|
|
|
|
# Get location breakdowns
|
|
location_breakdowns = report.location_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,
|
|
"threshold": float(report.threshold_percentage) if report.threshold_percentage else 90.0,
|
|
}
|
|
|
|
context = {
|
|
"report": report,
|
|
"monthly_data": monthly_data,
|
|
"total_data": total_data,
|
|
"source_breakdowns": source_breakdowns,
|
|
"department_breakdowns": department_breakdowns,
|
|
"location_breakdowns": location_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()
|
|
|
|
# Get location breakdowns
|
|
location_breakdowns = report.location_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,
|
|
"threshold": float(report.threshold_percentage) if report.threshold_percentage else 90.0,
|
|
}
|
|
|
|
context = {
|
|
"report": report,
|
|
"monthly_data": monthly_data,
|
|
"total_data": total_data,
|
|
"source_breakdowns": source_breakdowns,
|
|
"department_breakdowns": department_breakdowns,
|
|
"location_breakdowns": location_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)
|