437 lines
15 KiB
Python
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()}
|
|
}) |