1042 lines
35 KiB
Python
1042 lines
35 KiB
Python
"""
|
|
Complaints UI views - Server-rendered templates for complaints console
|
|
"""
|
|
from django.contrib import messages
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.core.paginator import Paginator
|
|
from django.db.models import Q, Count, Prefetch
|
|
from django.http import JsonResponse
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils import timezone
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from apps.accounts.models import User
|
|
from apps.core.services import AuditService
|
|
from apps.organizations.models import Department, Hospital, Physician
|
|
|
|
from .models import (
|
|
Complaint,
|
|
ComplaintAttachment,
|
|
ComplaintStatus,
|
|
ComplaintUpdate,
|
|
)
|
|
|
|
|
|
@login_required
|
|
def complaint_list(request):
|
|
"""
|
|
Complaints list view with advanced filters and pagination.
|
|
|
|
Features:
|
|
- Server-side pagination
|
|
- Advanced filters (status, severity, priority, hospital, department, etc.)
|
|
- Search by title, description, patient MRN
|
|
- Bulk actions support
|
|
- Export capability
|
|
"""
|
|
# Base queryset with optimizations
|
|
queryset = Complaint.objects.select_related(
|
|
'patient', 'hospital', 'department', 'physician',
|
|
'assigned_to', 'resolved_by', 'closed_by'
|
|
)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if user.is_px_admin():
|
|
pass # See all
|
|
elif user.is_hospital_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
elif user.is_department_manager() and user.department:
|
|
queryset = queryset.filter(department=user.department)
|
|
elif user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
else:
|
|
queryset = queryset.none()
|
|
|
|
# Apply filters from request
|
|
status_filter = request.GET.get('status')
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
severity_filter = request.GET.get('severity')
|
|
if severity_filter:
|
|
queryset = queryset.filter(severity=severity_filter)
|
|
|
|
priority_filter = request.GET.get('priority')
|
|
if priority_filter:
|
|
queryset = queryset.filter(priority=priority_filter)
|
|
|
|
category_filter = request.GET.get('category')
|
|
if category_filter:
|
|
queryset = queryset.filter(category=category_filter)
|
|
|
|
source_filter = request.GET.get('source')
|
|
if source_filter:
|
|
queryset = queryset.filter(source=source_filter)
|
|
|
|
hospital_filter = request.GET.get('hospital')
|
|
if hospital_filter:
|
|
queryset = queryset.filter(hospital_id=hospital_filter)
|
|
|
|
department_filter = request.GET.get('department')
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
physician_filter = request.GET.get('physician')
|
|
if physician_filter:
|
|
queryset = queryset.filter(physician_id=physician_filter)
|
|
|
|
assigned_to_filter = request.GET.get('assigned_to')
|
|
if assigned_to_filter:
|
|
queryset = queryset.filter(assigned_to_id=assigned_to_filter)
|
|
|
|
overdue_filter = request.GET.get('is_overdue')
|
|
if overdue_filter == 'true':
|
|
queryset = queryset.filter(is_overdue=True)
|
|
|
|
# Search
|
|
search_query = request.GET.get('search')
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(title__icontains=search_query) |
|
|
Q(description__icontains=search_query) |
|
|
Q(patient__mrn__icontains=search_query) |
|
|
Q(patient__first_name__icontains=search_query) |
|
|
Q(patient__last_name__icontains=search_query)
|
|
)
|
|
|
|
# Date range filters
|
|
date_from = request.GET.get('date_from')
|
|
if date_from:
|
|
queryset = queryset.filter(created_at__gte=date_from)
|
|
|
|
date_to = request.GET.get('date_to')
|
|
if date_to:
|
|
queryset = queryset.filter(created_at__lte=date_to)
|
|
|
|
# Ordering
|
|
order_by = request.GET.get('order_by', '-created_at')
|
|
queryset = queryset.order_by(order_by)
|
|
|
|
# Pagination
|
|
page_size = int(request.GET.get('page_size', 25))
|
|
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)
|
|
|
|
departments = Department.objects.filter(status='active')
|
|
if not user.is_px_admin() and user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
|
|
# Get assignable users
|
|
assignable_users = User.objects.filter(is_active=True)
|
|
if user.hospital:
|
|
assignable_users = assignable_users.filter(hospital=user.hospital)
|
|
|
|
# Statistics
|
|
stats = {
|
|
'total': queryset.count(),
|
|
'open': queryset.filter(status=ComplaintStatus.OPEN).count(),
|
|
'in_progress': queryset.filter(status=ComplaintStatus.IN_PROGRESS).count(),
|
|
'overdue': queryset.filter(is_overdue=True).count(),
|
|
}
|
|
|
|
context = {
|
|
'page_obj': page_obj,
|
|
'complaints': page_obj.object_list,
|
|
'stats': stats,
|
|
'hospitals': hospitals,
|
|
'departments': departments,
|
|
'assignable_users': assignable_users,
|
|
'status_choices': ComplaintStatus.choices,
|
|
'filters': request.GET,
|
|
}
|
|
|
|
return render(request, 'complaints/complaint_list.html', context)
|
|
|
|
|
|
@login_required
|
|
def complaint_detail(request, pk):
|
|
"""
|
|
Complaint detail view with timeline, attachments, and actions.
|
|
|
|
Features:
|
|
- Full complaint details
|
|
- Timeline of all updates
|
|
- Attachments management
|
|
- Related surveys and journey
|
|
- Linked PX actions
|
|
- Workflow actions (assign, status change, add note)
|
|
"""
|
|
complaint = get_object_or_404(
|
|
Complaint.objects.select_related(
|
|
'patient', 'hospital', 'department', 'physician',
|
|
'assigned_to', 'resolved_by', 'closed_by', 'resolution_survey'
|
|
).prefetch_related(
|
|
'attachments',
|
|
'updates__created_by'
|
|
),
|
|
pk=pk
|
|
)
|
|
|
|
# Check access
|
|
user = request.user
|
|
if not user.is_px_admin():
|
|
if user.is_hospital_admin() and complaint.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to view this complaint.")
|
|
return redirect('complaints:complaint_list')
|
|
elif user.is_department_manager() and complaint.department != user.department:
|
|
messages.error(request, "You don't have permission to view this complaint.")
|
|
return redirect('complaints:complaint_list')
|
|
elif user.hospital and complaint.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to view this complaint.")
|
|
return redirect('complaints:complaint_list')
|
|
|
|
# Get timeline (updates)
|
|
timeline = complaint.updates.all().order_by('-created_at')
|
|
|
|
# Get attachments
|
|
attachments = complaint.attachments.all().order_by('-created_at')
|
|
|
|
# Get related PX actions (using ContentType since PXAction uses GenericForeignKey)
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from apps.px_action_center.models import PXAction
|
|
complaint_ct = ContentType.objects.get_for_model(Complaint)
|
|
px_actions = PXAction.objects.filter(
|
|
content_type=complaint_ct,
|
|
object_id=complaint.id
|
|
).order_by('-created_at')
|
|
|
|
# Get assignable users
|
|
assignable_users = User.objects.filter(is_active=True)
|
|
if complaint.hospital:
|
|
assignable_users = assignable_users.filter(hospital=complaint.hospital)
|
|
|
|
# Check if overdue
|
|
complaint.check_overdue()
|
|
|
|
context = {
|
|
'complaint': complaint,
|
|
'timeline': timeline,
|
|
'attachments': attachments,
|
|
'px_actions': px_actions,
|
|
'assignable_users': assignable_users,
|
|
'status_choices': ComplaintStatus.choices,
|
|
'can_edit': user.is_px_admin() or user.is_hospital_admin(),
|
|
}
|
|
|
|
return render(request, 'complaints/complaint_detail.html', context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def complaint_create(request):
|
|
"""Create new complaint"""
|
|
if request.method == 'POST':
|
|
# Handle form submission
|
|
try:
|
|
from apps.organizations.models import Patient
|
|
|
|
# Get form data
|
|
patient_id = request.POST.get('patient_id')
|
|
hospital_id = request.POST.get('hospital_id')
|
|
department_id = request.POST.get('department_id', None)
|
|
physician_id = request.POST.get('physician_id', None)
|
|
|
|
title = request.POST.get('title')
|
|
description = request.POST.get('description')
|
|
category = request.POST.get('category')
|
|
subcategory = request.POST.get('subcategory', '')
|
|
priority = request.POST.get('priority')
|
|
severity = request.POST.get('severity')
|
|
source = request.POST.get('source')
|
|
encounter_id = request.POST.get('encounter_id', '')
|
|
|
|
# Validate required fields
|
|
if not all([patient_id, hospital_id, title, description, category, priority, severity, source]):
|
|
messages.error(request, "Please fill in all required fields.")
|
|
return redirect('complaints:complaint_create')
|
|
|
|
# Create complaint
|
|
complaint = Complaint.objects.create(
|
|
patient_id=patient_id,
|
|
hospital_id=hospital_id,
|
|
department_id=department_id if department_id else None,
|
|
physician_id=physician_id if physician_id else None,
|
|
title=title,
|
|
description=description,
|
|
category=category,
|
|
subcategory=subcategory,
|
|
priority=priority,
|
|
severity=severity,
|
|
source=source,
|
|
encounter_id=encounter_id,
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type='complaint_created',
|
|
description=f"Complaint created: {complaint.title}",
|
|
user=request.user,
|
|
content_object=complaint,
|
|
metadata={
|
|
'category': complaint.category,
|
|
'severity': complaint.severity,
|
|
'patient_mrn': complaint.patient.mrn
|
|
}
|
|
)
|
|
|
|
messages.success(request, f"Complaint #{complaint.id} created successfully.")
|
|
return redirect('complaints:complaint_detail', pk=complaint.id)
|
|
|
|
except Exception as e:
|
|
messages.error(request, f"Error creating complaint: {str(e)}")
|
|
return redirect('complaints:complaint_create')
|
|
|
|
# GET request - show form
|
|
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 = {
|
|
'hospitals': hospitals,
|
|
}
|
|
|
|
return render(request, 'complaints/complaint_form.html', context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_assign(request, pk):
|
|
"""Assign complaint to user"""
|
|
complaint = get_object_or_404(Complaint, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to assign complaints.")
|
|
return redirect('complaints:complaint_detail', pk=pk)
|
|
|
|
user_id = request.POST.get('user_id')
|
|
if not user_id:
|
|
messages.error(request, "Please select a user to assign.")
|
|
return redirect('complaints:complaint_detail', pk=pk)
|
|
|
|
try:
|
|
assignee = User.objects.get(id=user_id)
|
|
complaint.assigned_to = assignee
|
|
complaint.assigned_at = timezone.now()
|
|
complaint.save(update_fields=['assigned_to', 'assigned_at'])
|
|
|
|
# Create update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='assignment',
|
|
message=f"Assigned to {assignee.get_full_name()}",
|
|
created_by=request.user
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type='assignment',
|
|
description=f"Complaint assigned to {assignee.get_full_name()}",
|
|
user=request.user,
|
|
content_object=complaint
|
|
)
|
|
|
|
messages.success(request, f"Complaint assigned to {assignee.get_full_name()}.")
|
|
|
|
except User.DoesNotExist:
|
|
messages.error(request, "User not found.")
|
|
|
|
return redirect('complaints:complaint_detail', pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_change_status(request, pk):
|
|
"""Change complaint status"""
|
|
complaint = get_object_or_404(Complaint, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to change complaint status.")
|
|
return redirect('complaints:complaint_detail', pk=pk)
|
|
|
|
new_status = request.POST.get('status')
|
|
note = request.POST.get('note', '')
|
|
|
|
if not new_status:
|
|
messages.error(request, "Please select a status.")
|
|
return redirect('complaints:complaint_detail', pk=pk)
|
|
|
|
old_status = complaint.status
|
|
complaint.status = new_status
|
|
|
|
# Handle status-specific logic
|
|
if new_status == ComplaintStatus.RESOLVED:
|
|
complaint.resolved_at = timezone.now()
|
|
complaint.resolved_by = request.user
|
|
elif new_status == ComplaintStatus.CLOSED:
|
|
complaint.closed_at = timezone.now()
|
|
complaint.closed_by = request.user
|
|
|
|
# Trigger resolution satisfaction survey
|
|
from apps.complaints.tasks import send_complaint_resolution_survey
|
|
send_complaint_resolution_survey.delay(str(complaint.id))
|
|
|
|
complaint.save()
|
|
|
|
# Create update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='status_change',
|
|
message=note or f"Status changed from {old_status} to {new_status}",
|
|
created_by=request.user,
|
|
old_status=old_status,
|
|
new_status=new_status
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type='status_change',
|
|
description=f"Complaint status changed from {old_status} to {new_status}",
|
|
user=request.user,
|
|
content_object=complaint,
|
|
metadata={'old_status': old_status, 'new_status': new_status}
|
|
)
|
|
|
|
messages.success(request, f"Complaint status changed to {new_status}.")
|
|
return redirect('complaints:complaint_detail', pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_add_note(request, pk):
|
|
"""Add note to complaint"""
|
|
complaint = get_object_or_404(Complaint, pk=pk)
|
|
|
|
note = request.POST.get('note')
|
|
if not note:
|
|
messages.error(request, "Please enter a note.")
|
|
return redirect('complaints:complaint_detail', pk=pk)
|
|
|
|
# Create update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='note',
|
|
message=note,
|
|
created_by=request.user
|
|
)
|
|
|
|
messages.success(request, "Note added successfully.")
|
|
return redirect('complaints:complaint_detail', pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_escalate(request, pk):
|
|
"""Escalate complaint"""
|
|
complaint = get_object_or_404(Complaint, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to escalate complaints.")
|
|
return redirect('complaints:complaint_detail', pk=pk)
|
|
|
|
reason = request.POST.get('reason', '')
|
|
|
|
# Mark as escalated
|
|
complaint.escalated_at = timezone.now()
|
|
complaint.save(update_fields=['escalated_at'])
|
|
|
|
# Create update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='escalation',
|
|
message=f"Complaint escalated. Reason: {reason}",
|
|
created_by=request.user
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type='escalation',
|
|
description="Complaint escalated",
|
|
user=request.user,
|
|
content_object=complaint,
|
|
metadata={'reason': reason}
|
|
)
|
|
|
|
messages.success(request, "Complaint escalated successfully.")
|
|
return redirect('complaints:complaint_detail', pk=pk)
|
|
|
|
|
|
@login_required
|
|
def complaint_export_csv(request):
|
|
"""Export complaints to CSV"""
|
|
from apps.complaints.utils import export_complaints_csv
|
|
|
|
# Get filtered queryset (reuse list view logic)
|
|
queryset = Complaint.objects.select_related(
|
|
'patient', 'hospital', 'department', 'physician',
|
|
'assigned_to', 'resolved_by', 'closed_by'
|
|
)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if user.is_px_admin():
|
|
pass
|
|
elif user.is_hospital_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
elif user.is_department_manager() and user.department:
|
|
queryset = queryset.filter(department=user.department)
|
|
elif user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
else:
|
|
queryset = queryset.none()
|
|
|
|
# Apply filters from request
|
|
status_filter = request.GET.get('status')
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
severity_filter = request.GET.get('severity')
|
|
if severity_filter:
|
|
queryset = queryset.filter(severity=severity_filter)
|
|
|
|
priority_filter = request.GET.get('priority')
|
|
if priority_filter:
|
|
queryset = queryset.filter(priority=priority_filter)
|
|
|
|
hospital_filter = request.GET.get('hospital')
|
|
if hospital_filter:
|
|
queryset = queryset.filter(hospital_id=hospital_filter)
|
|
|
|
department_filter = request.GET.get('department')
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
overdue_filter = request.GET.get('is_overdue')
|
|
if overdue_filter == 'true':
|
|
queryset = queryset.filter(is_overdue=True)
|
|
|
|
search_query = request.GET.get('search')
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(title__icontains=search_query) |
|
|
Q(description__icontains=search_query) |
|
|
Q(patient__mrn__icontains=search_query)
|
|
)
|
|
|
|
return export_complaints_csv(queryset, request.GET.dict())
|
|
|
|
|
|
@login_required
|
|
def complaint_export_excel(request):
|
|
"""Export complaints to Excel"""
|
|
from apps.complaints.utils import export_complaints_excel
|
|
|
|
# Get filtered queryset (same as CSV)
|
|
queryset = Complaint.objects.select_related(
|
|
'patient', 'hospital', 'department', 'physician',
|
|
'assigned_to', 'resolved_by', 'closed_by'
|
|
)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if user.is_px_admin():
|
|
pass
|
|
elif user.is_hospital_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
elif user.is_department_manager() and user.department:
|
|
queryset = queryset.filter(department=user.department)
|
|
elif user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
else:
|
|
queryset = queryset.none()
|
|
|
|
# Apply filters from request
|
|
status_filter = request.GET.get('status')
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
severity_filter = request.GET.get('severity')
|
|
if severity_filter:
|
|
queryset = queryset.filter(severity=severity_filter)
|
|
|
|
priority_filter = request.GET.get('priority')
|
|
if priority_filter:
|
|
queryset = queryset.filter(priority=priority_filter)
|
|
|
|
hospital_filter = request.GET.get('hospital')
|
|
if hospital_filter:
|
|
queryset = queryset.filter(hospital_id=hospital_filter)
|
|
|
|
department_filter = request.GET.get('department')
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
overdue_filter = request.GET.get('is_overdue')
|
|
if overdue_filter == 'true':
|
|
queryset = queryset.filter(is_overdue=True)
|
|
|
|
search_query = request.GET.get('search')
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(title__icontains=search_query) |
|
|
Q(description__icontains=search_query) |
|
|
Q(patient__mrn__icontains=search_query)
|
|
)
|
|
|
|
return export_complaints_excel(queryset, request.GET.dict())
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_bulk_assign(request):
|
|
"""Bulk assign complaints"""
|
|
from apps.complaints.utils import bulk_assign_complaints
|
|
import json
|
|
|
|
# Check permission
|
|
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
|
|
return JsonResponse({'success': False, 'error': 'Permission denied'}, status=403)
|
|
|
|
try:
|
|
data = json.loads(request.body)
|
|
complaint_ids = data.get('complaint_ids', [])
|
|
user_id = data.get('user_id')
|
|
|
|
if not complaint_ids or not user_id:
|
|
return JsonResponse({'success': False, 'error': 'Missing required fields'}, status=400)
|
|
|
|
result = bulk_assign_complaints(complaint_ids, user_id, request.user)
|
|
|
|
if result['success']:
|
|
messages.success(request, f"Successfully assigned {result['success_count']} complaints.")
|
|
|
|
return JsonResponse(result)
|
|
|
|
except json.JSONDecodeError:
|
|
return JsonResponse({'success': False, 'error': 'Invalid JSON'}, status=400)
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_bulk_status(request):
|
|
"""Bulk change complaint status"""
|
|
from apps.complaints.utils import bulk_change_status
|
|
import json
|
|
|
|
# Check permission
|
|
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
|
|
return JsonResponse({'success': False, 'error': 'Permission denied'}, status=403)
|
|
|
|
try:
|
|
data = json.loads(request.body)
|
|
complaint_ids = data.get('complaint_ids', [])
|
|
new_status = data.get('status')
|
|
note = data.get('note', '')
|
|
|
|
if not complaint_ids or not new_status:
|
|
return JsonResponse({'success': False, 'error': 'Missing required fields'}, status=400)
|
|
|
|
result = bulk_change_status(complaint_ids, new_status, request.user, note)
|
|
|
|
if result['success']:
|
|
messages.success(request, f"Successfully updated {result['success_count']} complaints.")
|
|
|
|
return JsonResponse(result)
|
|
|
|
except json.JSONDecodeError:
|
|
return JsonResponse({'success': False, 'error': 'Invalid JSON'}, status=400)
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_bulk_escalate(request):
|
|
"""Bulk escalate complaints"""
|
|
from apps.complaints.utils import bulk_escalate_complaints
|
|
import json
|
|
|
|
# Check permission
|
|
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
|
|
return JsonResponse({'success': False, 'error': 'Permission denied'}, status=403)
|
|
|
|
try:
|
|
data = json.loads(request.body)
|
|
complaint_ids = data.get('complaint_ids', [])
|
|
reason = data.get('reason', '')
|
|
|
|
if not complaint_ids:
|
|
return JsonResponse({'success': False, 'error': 'No complaints selected'}, status=400)
|
|
|
|
result = bulk_escalate_complaints(complaint_ids, request.user, reason)
|
|
|
|
if result['success']:
|
|
messages.success(request, f"Successfully escalated {result['success_count']} complaints.")
|
|
|
|
return JsonResponse(result)
|
|
|
|
except json.JSONDecodeError:
|
|
return JsonResponse({'success': False, 'error': 'Invalid JSON'}, status=400)
|
|
except Exception as e:
|
|
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
|
|
|
|
|
# ============================================================================
|
|
# INQUIRIES VIEWS
|
|
# ============================================================================
|
|
|
|
@login_required
|
|
def inquiry_list(request):
|
|
"""
|
|
Inquiries list view with filters and pagination.
|
|
"""
|
|
from .models import Inquiry
|
|
|
|
# Base queryset with optimizations
|
|
queryset = Inquiry.objects.select_related(
|
|
'patient', 'hospital', 'department', 'assigned_to', 'responded_by'
|
|
)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if user.is_px_admin():
|
|
pass # See all
|
|
elif user.is_hospital_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
elif user.is_department_manager() and user.department:
|
|
queryset = queryset.filter(department=user.department)
|
|
elif user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
else:
|
|
queryset = queryset.none()
|
|
|
|
# Apply filters
|
|
status_filter = request.GET.get('status')
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
category_filter = request.GET.get('category')
|
|
if category_filter:
|
|
queryset = queryset.filter(category=category_filter)
|
|
|
|
hospital_filter = request.GET.get('hospital')
|
|
if hospital_filter:
|
|
queryset = queryset.filter(hospital_id=hospital_filter)
|
|
|
|
department_filter = request.GET.get('department')
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
# Search
|
|
search_query = request.GET.get('search')
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(subject__icontains=search_query) |
|
|
Q(message__icontains=search_query) |
|
|
Q(contact_name__icontains=search_query) |
|
|
Q(contact_email__icontains=search_query)
|
|
)
|
|
|
|
# Ordering
|
|
order_by = request.GET.get('order_by', '-created_at')
|
|
queryset = queryset.order_by(order_by)
|
|
|
|
# Pagination
|
|
page_size = int(request.GET.get('page_size', 25))
|
|
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)
|
|
|
|
departments = Department.objects.filter(status='active')
|
|
if not user.is_px_admin() and user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
|
|
# Statistics
|
|
stats = {
|
|
'total': queryset.count(),
|
|
'open': queryset.filter(status='open').count(),
|
|
'in_progress': queryset.filter(status='in_progress').count(),
|
|
'resolved': queryset.filter(status='resolved').count(),
|
|
}
|
|
|
|
context = {
|
|
'page_obj': page_obj,
|
|
'inquiries': page_obj.object_list,
|
|
'stats': stats,
|
|
'hospitals': hospitals,
|
|
'departments': departments,
|
|
'filters': request.GET,
|
|
}
|
|
|
|
return render(request, 'complaints/inquiry_list.html', context)
|
|
|
|
|
|
@login_required
|
|
def inquiry_detail(request, pk):
|
|
"""
|
|
Inquiry detail view.
|
|
"""
|
|
from .models import Inquiry
|
|
|
|
inquiry = get_object_or_404(
|
|
Inquiry.objects.select_related(
|
|
'patient', 'hospital', 'department', 'assigned_to', 'responded_by'
|
|
),
|
|
pk=pk
|
|
)
|
|
|
|
# Check access
|
|
user = request.user
|
|
if not user.is_px_admin():
|
|
if user.is_hospital_admin() and inquiry.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to view this inquiry.")
|
|
return redirect('complaints:inquiry_list')
|
|
elif user.hospital and inquiry.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to view this inquiry.")
|
|
return redirect('complaints:inquiry_list')
|
|
|
|
# Get assignable users
|
|
assignable_users = User.objects.filter(is_active=True)
|
|
if inquiry.hospital:
|
|
assignable_users = assignable_users.filter(hospital=inquiry.hospital)
|
|
|
|
context = {
|
|
'inquiry': inquiry,
|
|
'assignable_users': assignable_users,
|
|
'can_edit': user.is_px_admin() or user.is_hospital_admin(),
|
|
}
|
|
|
|
return render(request, 'complaints/inquiry_detail.html', context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def inquiry_create(request):
|
|
"""Create new inquiry"""
|
|
from .models import Inquiry
|
|
from apps.organizations.models import Patient
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
# Get form data
|
|
patient_id = request.POST.get('patient_id', None)
|
|
hospital_id = request.POST.get('hospital_id')
|
|
department_id = request.POST.get('department_id', None)
|
|
|
|
subject = request.POST.get('subject')
|
|
message = request.POST.get('message')
|
|
category = request.POST.get('category')
|
|
|
|
# Contact info (if no patient)
|
|
contact_name = request.POST.get('contact_name', '')
|
|
contact_phone = request.POST.get('contact_phone', '')
|
|
contact_email = request.POST.get('contact_email', '')
|
|
|
|
# Validate required fields
|
|
if not all([hospital_id, subject, message, category]):
|
|
messages.error(request, "Please fill in all required fields.")
|
|
return redirect('complaints:inquiry_create')
|
|
|
|
# Create inquiry
|
|
inquiry = Inquiry.objects.create(
|
|
patient_id=patient_id if patient_id else None,
|
|
hospital_id=hospital_id,
|
|
department_id=department_id if department_id else None,
|
|
subject=subject,
|
|
message=message,
|
|
category=category,
|
|
contact_name=contact_name,
|
|
contact_phone=contact_phone,
|
|
contact_email=contact_email,
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type='inquiry_created',
|
|
description=f"Inquiry created: {inquiry.subject}",
|
|
user=request.user,
|
|
content_object=inquiry,
|
|
metadata={'category': inquiry.category}
|
|
)
|
|
|
|
messages.success(request, f"Inquiry #{inquiry.id} created successfully.")
|
|
return redirect('complaints:inquiry_detail', pk=inquiry.id)
|
|
|
|
except Exception as e:
|
|
messages.error(request, f"Error creating inquiry: {str(e)}")
|
|
return redirect('complaints:inquiry_create')
|
|
|
|
# GET request - show form
|
|
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 = {
|
|
'hospitals': hospitals,
|
|
}
|
|
|
|
return render(request, 'complaints/inquiry_form.html', context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def inquiry_respond(request, pk):
|
|
"""Respond to inquiry"""
|
|
from .models import Inquiry
|
|
|
|
inquiry = get_object_or_404(Inquiry, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to respond to inquiries.")
|
|
return redirect('complaints:inquiry_detail', pk=pk)
|
|
|
|
response = request.POST.get('response')
|
|
if not response:
|
|
messages.error(request, "Please enter a response.")
|
|
return redirect('complaints:inquiry_detail', pk=pk)
|
|
|
|
inquiry.response = response
|
|
inquiry.responded_at = timezone.now()
|
|
inquiry.responded_by = request.user
|
|
inquiry.status = 'resolved'
|
|
inquiry.save()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type='inquiry_responded',
|
|
description=f"Inquiry responded to: {inquiry.subject}",
|
|
user=request.user,
|
|
content_object=inquiry
|
|
)
|
|
|
|
messages.success(request, "Response sent successfully.")
|
|
return redirect('complaints:inquiry_detail', pk=pk)
|
|
|
|
|
|
# ============================================================================
|
|
# ANALYTICS VIEWS
|
|
# ============================================================================
|
|
|
|
@login_required
|
|
def complaints_analytics(request):
|
|
"""
|
|
Complaints analytics dashboard.
|
|
"""
|
|
from .analytics import ComplaintAnalytics
|
|
|
|
user = request.user
|
|
hospital = None
|
|
|
|
# Apply RBAC
|
|
if not user.is_px_admin() and user.hospital:
|
|
hospital = user.hospital
|
|
|
|
# Get date range from request
|
|
date_range = int(request.GET.get('date_range', 30))
|
|
|
|
# Get analytics data
|
|
dashboard_summary = ComplaintAnalytics.get_dashboard_summary(hospital)
|
|
trends = ComplaintAnalytics.get_complaint_trends(hospital, date_range)
|
|
sla_compliance = ComplaintAnalytics.get_sla_compliance(hospital, date_range)
|
|
resolution_rate = ComplaintAnalytics.get_resolution_rate(hospital, date_range)
|
|
top_categories = ComplaintAnalytics.get_top_categories(hospital, date_range)
|
|
overdue_complaints = ComplaintAnalytics.get_overdue_complaints(hospital)
|
|
|
|
context = {
|
|
'dashboard_summary': dashboard_summary,
|
|
'trends': trends,
|
|
'sla_compliance': sla_compliance,
|
|
'resolution_rate': resolution_rate,
|
|
'top_categories': top_categories,
|
|
'overdue_complaints': overdue_complaints,
|
|
'date_range': date_range,
|
|
}
|
|
|
|
return render(request, 'complaints/analytics.html', context)
|
|
|
|
|
|
# ============================================================================
|
|
# AJAX/API HELPERS
|
|
# ============================================================================
|
|
|
|
@login_required
|
|
def get_departments_by_hospital(request):
|
|
"""Get departments for a hospital (AJAX)"""
|
|
hospital_id = request.GET.get('hospital_id')
|
|
if not hospital_id:
|
|
return JsonResponse({'departments': []})
|
|
|
|
departments = Department.objects.filter(
|
|
hospital_id=hospital_id,
|
|
status='active'
|
|
).values('id', 'name', 'name_ar')
|
|
|
|
return JsonResponse({'departments': list(departments)})
|
|
|
|
|
|
@login_required
|
|
def get_physicians_by_department(request):
|
|
"""Get physicians for a department (AJAX)"""
|
|
department_id = request.GET.get('department_id')
|
|
if not department_id:
|
|
return JsonResponse({'physicians': []})
|
|
|
|
from apps.organizations.models import Physician
|
|
physicians = Physician.objects.filter(
|
|
department_id=department_id,
|
|
status='active'
|
|
).values('id', 'first_name', 'last_name')
|
|
|
|
return JsonResponse({'physicians': list(physicians)})
|
|
|
|
|
|
@login_required
|
|
def search_patients(request):
|
|
"""Search patients by MRN or name (AJAX)"""
|
|
from apps.organizations.models import Patient
|
|
|
|
query = request.GET.get('q', '')
|
|
if len(query) < 2:
|
|
return JsonResponse({'patients': []})
|
|
|
|
patients = Patient.objects.filter(
|
|
Q(mrn__icontains=query) |
|
|
Q(first_name__icontains=query) |
|
|
Q(last_name__icontains=query) |
|
|
Q(national_id__icontains=query)
|
|
)[:10]
|
|
|
|
results = [
|
|
{
|
|
'id': str(p.id),
|
|
'mrn': p.mrn,
|
|
'name': p.get_full_name(),
|
|
'phone': p.phone,
|
|
'email': p.email,
|
|
}
|
|
for p in patients
|
|
]
|
|
|
|
return JsonResponse({'patients': results})
|