HH/apps/reports/views.py
2026-03-09 16:10:24 +03:00

437 lines
15 KiB
Python

"""
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
hospitals = Hospital.objects.filter(status='active')
if not user.is_px_admin() and user.hospital:
hospitals = hospitals.filter(id=user.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
if not user.is_px_admin() and user.hospital:
filter_config['hospital'] = str(user.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
)
# Generate summary
summary = ReportBuilderService.generate_summary(data_source, filter_config)
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
queryset = SavedReport.objects.filter(
created_by=user
) | SavedReport.objects.filter(
is_shared=True,
hospital=user.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
if report.created_by != user and not (report.is_shared and report.hospital == user.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 user.hospital:
filter_config['hospital'] = str(user.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
)
# Generate summary
summary = ReportBuilderService.generate_summary(report.data_source, filter_config)
# 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
if report.created_by != user and not (report.is_shared and report.hospital == user.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 user.hospital:
filter_config['hospital'] = str(user.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
)
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()}
})