""" Report Builder UI Views - Simplified Version Handles the visual report builder interface, saved reports, and exports. No chart functionality. """ import json from django.contrib.auth.decorators import login_required from django.contrib import messages from django.http import JsonResponse, HttpResponse from django.shortcuts import render, redirect, get_object_or_404 from django.views.decorators.http import require_POST, require_GET from django.views.decorators.csrf import ensure_csrf_cookie from django.utils import timezone from django.core.paginator import Paginator from apps.organizations.models import Department, Hospital from .models import SavedReport, GeneratedReport, ReportTemplate, DataSource, ReportFormat from .services import ReportBuilderService, ReportExportService @login_required @ensure_csrf_cookie def report_builder(request): """ Visual report builder interface. Allows creating custom reports with: - Data source selection - Dynamic filters - Column selection - Chart configuration """ user = request.user # Get hospitals for filter hospital = getattr(request, "tenant_hospital", None) or user.hospital hospitals = Hospital.objects.filter(status="active") if not user.is_px_admin() and hospital: hospitals = hospitals.filter(id=hospital.id) # Get saved reports saved_reports = SavedReport.objects.filter(created_by=user).order_by("-created_at")[:10] context = { "hospitals": hospitals, "saved_reports": saved_reports, "data_sources": DataSource.choices, } return render(request, "reports/report_builder.html", context) @login_required def report_preview_api(request): """ API endpoint to preview report data. Returns JSON with: - Report data rows - Summary statistics - Chart data """ if request.method != "POST": return JsonResponse({"error": "POST required"}, status=405) try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({"error": "Invalid JSON"}, status=400) data_source = data.get("data_source", "complaints") filter_config = data.get("filter_config", {}) column_config = data.get("column_config", []) grouping_config = data.get("grouping_config", {}) chart_config = data.get("chart_config", {}) sort_config = data.get("sort_config", []) # Apply user's hospital restriction user = request.user hospital = getattr(request, "tenant_hospital", None) or user.hospital if not user.is_px_admin() and hospital: filter_config["hospital"] = str(hospital.id) # Generate report data report_data = ReportBuilderService.generate_report_data( data_source=data_source, filter_config=filter_config, column_config=column_config, grouping_config=grouping_config, sort_config=sort_config, user=user, ) # Generate summary summary = ReportBuilderService.generate_summary(data_source, filter_config, user=user) return JsonResponse( { "success": True, "data": report_data, "summary": summary, } ) @login_required def save_report(request): """Save a report configuration.""" if request.method != "POST": return JsonResponse({"error": "POST required"}, status=405) try: data = json.loads(request.body) except json.JSONDecodeError: return JsonResponse({"error": "Invalid JSON"}, status=400) report_id = data.get("id") if report_id: # Update existing report report = get_object_or_404(SavedReport, id=report_id, created_by=request.user) report.name = data.get("name", report.name) report.description = data.get("description", report.description) report.data_source = data.get("data_source", report.data_source) report.filter_config = data.get("filter_config", report.filter_config) report.column_config = data.get("column_config", report.column_config) report.grouping_config = data.get("grouping_config", report.grouping_config) report.sort_config = data.get("sort_config", report.sort_config) report.is_shared = data.get("is_shared", report.is_shared) report.save() else: # Create new report report = SavedReport.objects.create( name=data.get("name", "Untitled Report"), description=data.get("description", ""), data_source=data.get("data_source", "complaints"), filter_config=data.get("filter_config", {}), column_config=data.get("column_config", []), grouping_config=data.get("grouping_config", {}), sort_config=data.get("sort_config", []), is_shared=data.get("is_shared", False), created_by=request.user, hospital=request.user.hospital, ) return JsonResponse({"success": True, "report_id": str(report.id), "message": "Report saved successfully"}) @login_required def saved_reports_list(request): """List all saved reports.""" user = request.user # Get user's reports and shared reports hospital = getattr(request, "tenant_hospital", None) or user.hospital queryset = SavedReport.objects.filter(created_by=user) if hospital: queryset = queryset | SavedReport.objects.filter(is_shared=True, hospital=hospital) # Remove duplicates and order queryset = queryset.distinct().order_by("-created_at") # Filter by data source data_source = request.GET.get("data_source") if data_source: queryset = queryset.filter(data_source=data_source) # Search search = request.GET.get("search", "") if search: queryset = queryset.filter(name__icontains=search) # Pagination paginator = Paginator(queryset, 25) page_number = request.GET.get("page", 1) page_obj = paginator.get_page(page_number) context = { "page_obj": page_obj, "reports": page_obj.object_list, "data_sources": DataSource.choices, "search": search, "selected_source": data_source, } return render(request, "reports/saved_reports.html", context) @login_required def report_detail(request, report_id): """View a saved report with live data.""" user = request.user report = get_object_or_404(SavedReport, id=report_id) # Check access hospital = getattr(request, "tenant_hospital", None) or user.hospital if report.created_by != user and not (report.is_shared and report.hospital == hospital): if not user.is_px_admin(): messages.error(request, "You don't have access to this report.") return redirect("reports:saved_reports") # Apply user's hospital restriction filter_config = report.filter_config.copy() if not user.is_px_admin() and hospital: filter_config["hospital"] = str(hospital.id) # Generate report data report_data = ReportBuilderService.generate_report_data( data_source=report.data_source, filter_config=filter_config, column_config=report.column_config, grouping_config=report.grouping_config, sort_config=report.sort_config, user=user, ) # Generate summary summary = ReportBuilderService.generate_summary(report.data_source, filter_config, user=user) # Update last run report.last_run_at = timezone.now() report.last_run_count = len(report_data.get("rows", [])) report.save(update_fields=["last_run_at", "last_run_count"]) context = { "report": report, "data": report_data, "summary": summary, "source_fields": ReportBuilderService.SOURCE_FIELDS.get(report.data_source, {}), } return render(request, "reports/report_detail.html", context) @login_required def delete_report(request, report_id): """Delete a saved report.""" report = get_object_or_404(SavedReport, id=report_id, created_by=request.user) if request.method == "POST": report.delete() messages.success(request, "Report deleted successfully.") return redirect("reports:saved_reports") return render(request, "reports/report_confirm_delete.html", {"report": report}) @login_required def export_report(request, report_id, export_format): """Export a report to Excel, PDF, or CSV.""" user = request.user report = get_object_or_404(SavedReport, id=report_id) # Check access hospital = getattr(request, "tenant_hospital", None) or user.hospital if report.created_by != user and not (report.is_shared and report.hospital == hospital): if not user.is_px_admin(): messages.error(request, "You don't have access to this report.") return redirect("reports:saved_reports") # Apply user's hospital restriction filter_config = report.filter_config.copy() if not user.is_px_admin() and hospital: filter_config["hospital"] = str(hospital.id) # Generate report data report_data = ReportBuilderService.generate_report_data( data_source=report.data_source, filter_config=filter_config, column_config=report.column_config, grouping_config=report.grouping_config, sort_config=report.sort_config, user=user, ) rows = report_data.get("rows", []) columns = report_data.get("columns", []) column_keys = report_data.get("column_keys", columns) # Use keys if available, fallback to labels # Generate filename filename = f"{report.name.replace(' ', '_')}_{timezone.now().strftime('%Y%m%d')}" # Export based on format if export_format == "csv": return ReportExportService.export_to_csv(rows, columns, column_keys, filename) elif export_format == "excel": return ReportExportService.export_to_excel(rows, columns, column_keys, filename) elif export_format == "pdf": return ReportExportService.export_to_pdf(rows, columns, column_keys, report.name, filename) else: messages.error(request, f"Unsupported export format: {export_format}") return redirect("reports:report_detail", report_id=report_id) @login_required def report_templates(request): """List available report templates.""" templates = ReportTemplate.objects.filter(is_active=True).order_by("category", "sort_order", "name") # Group by category categories = {} for template in templates: cat = template.category or "General" if cat not in categories: categories[cat] = [] categories[cat].append(template) context = { "categories": categories, "templates": templates, } return render(request, "reports/report_templates.html", context) @login_required def use_template(request, template_id): """Create a report from a template.""" template = get_object_or_404(ReportTemplate, id=template_id, is_active=True) if request.method == "POST": # Create report from template with overrides overrides = { "name": request.POST.get("name", f"{template.name} - {timezone.now().strftime('%Y-%m-%d')}"), } # Apply any filter overrides from the form for key, value in request.POST.items(): if key.startswith("filter_"): filter_key = key[7:] # Remove 'filter_' prefix if "filter_config" not in overrides: overrides["filter_config"] = template.filter_config.copy() overrides["filter_config"][filter_key] = value report = template.create_report(request.user, overrides) messages.success(request, f"Report created from template: {template.name}") return redirect("reports:report_detail", report_id=report.id) # Get available filter options hospitals = Hospital.objects.filter(status="active") if not request.user.is_px_admin() and request.user.hospital: hospitals = hospitals.filter(id=request.user.hospital.id) context = { "template": template, "hospitals": hospitals, "source_filters": ReportBuilderService.SOURCE_FILTERS.get(template.data_source, []), } return render(request, "reports/use_template.html", context) @login_required def filter_options_api(request): """API endpoint to get filter options for a data source.""" data_source = request.GET.get("data_source", "complaints") options = {} # Status options - use defined choices, not database queries if data_source == "complaints": from apps.complaints.models import Complaint # Get unique status values from model choices options["status"] = ( [choice[0] for choice in Complaint.STATUS_CHOICES] if hasattr(Complaint, "STATUS_CHOICES") else ["open", "in_progress", "resolved", "closed"] ) options["severity"] = ["low", "medium", "high", "critical"] options["priority"] = ["low", "medium", "high", "urgent"] # Get unique source types from model choices or use defaults options["source"] = ["walk_in", "call", "email", "website", "social_media", "app"] elif data_source == "inquiries": from apps.complaints.models import Complaint options["status"] = ( [choice[0] for choice in Complaint.STATUS_CHOICES] if hasattr(Complaint, "STATUS_CHOICES") else ["open", "in_progress", "resolved", "closed"] ) elif data_source == "observations": from apps.observations.models import Observation, ObservationStatus options["status"] = [s.value for s in ObservationStatus] options["severity"] = ["low", "medium", "high", "critical"] elif data_source == "surveys": options["status"] = ["pending", "sent", "completed", "expired"] options["patient_type"] = ["inpatient", "outpatient", "emergency"] options["journey_type"] = ["admission", "discharge", "visit"] elif data_source == "px_actions": options["status"] = ["open", "in_progress", "completed", "closed"] options["priority"] = ["low", "medium", "high", "urgent"] elif data_source == "physicians": options["journey_type"] = ["inpatient", "outpatient", "emergency"] # Hospital options hospitals = Hospital.objects.filter(status="active") if not request.user.is_px_admin() and request.user.hospital: hospitals = hospitals.filter(id=request.user.hospital.id) options["hospitals"] = list(hospitals.values("id", "name")) # Department options (filtered by hospital if provided) hospital_id = request.GET.get("hospital") departments = Department.objects.filter(status="active") if hospital_id: departments = departments.filter(hospital_id=hospital_id) elif not request.user.is_px_admin() and request.user.hospital: departments = departments.filter(hospital=request.user.hospital) options["departments"] = list(departments.values("id", "name")) # Available columns for the data source fields = ReportBuilderService.SOURCE_FIELDS.get(data_source, {}) # Default columns (first 8 fields) default_columns = list(fields.keys())[:8] options["columns"] = [ {"key": key, "label": info["label"], "type": info["type"], "selected": key in default_columns} for key, info in fields.items() ] return JsonResponse(options) @login_required def available_fields_api(request): """API endpoint to get available fields for a data source.""" data_source = request.GET.get("data_source", "complaints") fields = ReportBuilderService.SOURCE_FIELDS.get(data_source, {}) return JsonResponse({"fields": {k: {"label": v["label"], "type": v["type"]} for k, v in fields.items()}})