HH/apps/complaints/ui_views.py

2247 lines
76 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, Staff
from apps.px_sources.models import SourceUser, PXSource
from apps.px_sources.models import SourceUser, PXSource
from .models import (
Complaint,
ComplaintAttachment,
ComplaintCategory,
ComplaintStatus,
ComplaintUpdate,
ComplaintExplanation,
Inquiry,
InquiryAttachment,
InquiryUpdate,
)
@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", "staff", "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)
staff_filter = request.GET.get("staff")
if staff_filter:
queryset = queryset.filter(staff_id=staff_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)
"""
from apps.px_sources.models import SourceUser
source_user = SourceUser.objects.filter(user=request.user).first()
base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html'
complaint = get_object_or_404(
Complaint.objects.select_related(
"patient", "hospital", "department", "staff", "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)
# Get departments for the complaint's hospital
hospital_departments = []
if complaint.hospital:
hospital_departments = Department.objects.filter(hospital=complaint.hospital, status="active").order_by("name")
# Check if overdue
complaint.check_overdue()
# Get explanations for this complaint
from .models import ComplaintExplanation
explanations = complaint.explanations.all().select_related("staff").order_by("-created_at")
explanation = explanations.first() if explanations else None
# Get explanation attachments if explanation exists
explanation_attachments = []
if explanation:
explanation_attachments = explanation.attachments.all()
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(),
'hospital_departments': hospital_departments,
'base_layout': base_layout,
'source_user': source_user,
"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(),
"hospital_departments": hospital_departments,
"explanation": explanation,
"explanations": explanations,
"explanation_attachments": explanation_attachments,
'base_layout': base_layout,
'source_user': source_user,
"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(),
"hospital_departments": hospital_departments,
"explanation": explanation,
"explanations": explanations,
"explanation_attachments": explanation_attachments,
}
return render(request, "complaints/complaint_detail.html", context)
@login_required
@require_http_methods(["GET", "POST"])
def complaint_create(request):
"""Create new complaint with AI-powered classification"""
from apps.complaints.forms import ComplaintForm
# Determine base layout based on user type
from apps.px_sources.models import SourceUser
source_user = SourceUser.objects.filter(user=request.user).first()
base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html'
if request.method == 'POST':
# Handle form submission
form = ComplaintForm(request.POST, user=request.user)
if not form.is_valid():
# Debug: print form errors
print("Form validation errors:", form.errors)
messages.error(request, f"Please correct the errors: {form.errors}")
context = {
'form': form,
'base_layout': base_layout,
'source_user': source_user,
}
return render(request, 'complaints/complaint_form.html', context)
try:
# Create complaint with AI defaults
complaint = form.save(commit=False)
# Set AI-determined defaults
complaint.title = 'Complaint' # AI will generate title
# category can be None, AI will determine it
complaint.subcategory = '' # AI will determine
# Set source from logged-in source user
if source_user and source_user.source:
complaint.source = source_user.source
else:
# Fallback: get or create a 'staff' source
from apps.px_sources.models import PXSource
try:
source_obj = PXSource.objects.get(code='staff')
except PXSource.DoesNotExist:
source_obj = PXSource.objects.create(
code='staff',
name='Staff',
description='Complaints submitted by staff members'
)
complaint.source = source_obj
complaint.priority = 'medium' # AI will update
complaint.severity = 'medium' # AI will update
complaint.created_by = request.user
complaint.save()
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)
staff_id = request.POST.get("staff_id", None)
description = request.POST.get("description")
category_id = request.POST.get("category")
subcategory_id = request.POST.get("subcategory", "")
source = request.POST.get("source")
encounter_id = request.POST.get("encounter_id", "")
# Validate required fields
if not all([patient_id, hospital_id, description, category_id, source]):
messages.error(request, "Please fill in all required fields.")
return redirect("complaints:complaint_create")
# Get category and subcategory objects
category = ComplaintCategory.objects.get(id=category_id)
subcategory_obj = None
if subcategory_id:
subcategory_obj = ComplaintCategory.objects.get(id=subcategory_id)
# Create complaint with AI defaults
complaint = Complaint.objects.create(
patient_id=patient_id,
hospital_id=hospital_id,
department_id=department_id if department_id else None,
staff_id=staff_id if staff_id else None,
title="Complaint", # AI will generate title
description=description,
category=category,
subcategory=subcategory_obj.code if subcategory_obj else "",
priority="medium", # AI will update
severity="medium", # AI will update
source=source,
encounter_id=encounter_id,
)
# Create initial update
ComplaintUpdate.objects.create(
complaint=complaint,
update_type="note",
message="Complaint created. AI analysis running in background.",
created_by=request.user,
)
# Trigger AI analysis in background using Celery
from apps.complaints.tasks import analyze_complaint_with_ai
analyze_complaint_with_ai.delay(str(complaint.id))
# Log audit
AuditService.log_event(
event_type="complaint_created",
description=f"Complaint created: {complaint.title}",
user=request.user,
content_object=complaint,
metadata={
'severity': complaint.severity,
"category": category.name_en,
"severity": complaint.severity,
"patient_mrn": complaint.patient.mrn if complaint.patient else None,
"ai_analysis_pending": True,
},
)
messages.success(
request,
f"Complaint #{complaint.id} created successfully. AI is analyzing and classifying the complaint.",
)
return redirect("complaints:complaint_detail", pk=complaint.id)
except ComplaintCategory.DoesNotExist:
messages.error(request, "Selected category not found.")
return redirect("complaints:complaint_create")
# GET request - show form
# Check for hospital parameter from URL (for pre-selection)
initial_data = {}
hospital_id = request.GET.get('hospital')
if hospital_id:
initial_data['hospital'] = hospital_id
form = ComplaintForm(user=request.user, initial=initial_data)
context = {
'form': form,
'base_layout': base_layout,
'source_user': source_user,
# "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_change_department(request, pk):
"""Change complaint department"""
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 department.")
return redirect("complaints:complaint_detail", pk=pk)
department_id = request.POST.get("department_id")
if not department_id:
messages.error(request, "Please select a department.")
return redirect("complaints:complaint_detail", pk=pk)
try:
department = Department.objects.get(id=department_id)
# Check department belongs to same hospital
if department.hospital != complaint.hospital:
messages.error(request, "Department does not belong to this complaint's hospital.")
return redirect("complaints:complaint_detail", pk=pk)
old_department = complaint.department
complaint.department = department
complaint.save(update_fields=["department"])
# Create update
ComplaintUpdate.objects.create(
complaint=complaint,
update_type="assignment",
message=f"Department changed to {department.name}",
created_by=request.user,
metadata={
"old_department_id": str(old_department.id) if old_department else None,
"new_department_id": str(department.id),
},
)
# Log audit
AuditService.log_event(
event_type="department_change",
description=f"Complaint department changed to {department.name}",
user=request.user,
content_object=complaint,
metadata={
"old_department_id": str(old_department.id) if old_department else None,
"new_department_id": str(department.id),
},
)
messages.success(request, f"Department changed to {department.name}.")
except Department.DoesNotExist:
messages.error(request, "Department not found.")
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", "staff", "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", "staff", "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 with timeline and attachments.
Features:
- Full inquiry details
- Timeline of all updates
- Attachments management
- Workflow actions (assign, status change, add note, respond)
"""
from apps.px_sources.models import SourceUser
source_user = SourceUser.objects.filter(user=request.user).first()
base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html'
inquiry = get_object_or_404(
Inquiry.objects.select_related(
"patient", "hospital", "department", "assigned_to", "responded_by"
).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 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 timeline (updates)
timeline = inquiry.updates.all().order_by("-created_at")
# Get attachments
attachments = inquiry.attachments.all().order_by("-created_at")
# Get assignable users
assignable_users = User.objects.filter(is_active=True)
if inquiry.hospital:
assignable_users = assignable_users.filter(hospital=inquiry.hospital)
# Status choices for the form
status_choices = [
("open", "Open"),
("in_progress", "In Progress"),
("resolved", "Resolved"),
("closed", "Closed"),
]
context = {
'inquiry': inquiry,
'timeline': timeline,
'attachments': attachments,
'assignable_users': assignable_users,
'status_choices': status_choices,
'can_edit': user.is_px_admin() or user.is_hospital_admin(),
'base_layout': base_layout,
'source_user': source_user,
}
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 .forms import InquiryForm
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_assign(request, pk):
"""Assign inquiry to user"""
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 assign inquiries.")
return redirect("complaints:inquiry_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:inquiry_detail", pk=pk)
try:
assignee = User.objects.get(id=user_id)
inquiry.assigned_to = assignee
inquiry.save(update_fields=["assigned_to"])
# Create update
InquiryUpdate.objects.create(
inquiry=inquiry,
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"Inquiry assigned to {assignee.get_full_name()}",
user=request.user,
content_object=inquiry,
)
messages.success(request, f"Inquiry assigned to {assignee.get_full_name()}.")
except User.DoesNotExist:
messages.error(request, "User not found.")
return redirect("complaints:inquiry_detail", pk=pk)
@login_required
@require_http_methods(["POST"])
def inquiry_change_status(request, pk):
"""Change inquiry status"""
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 change inquiry status.")
return redirect("complaints:inquiry_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:inquiry_detail", pk=pk)
old_status = inquiry.status
inquiry.status = new_status
# Handle status-specific logic
if new_status == "resolved" and not inquiry.response:
messages.error(request, "Please add a response before resolving.")
return redirect("complaints:inquiry_detail", pk=pk)
inquiry.save()
# Create update
InquiryUpdate.objects.create(
inquiry=inquiry,
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"Inquiry status changed from {old_status} to {new_status}",
user=request.user,
content_object=inquiry,
metadata={"old_status": old_status, "new_status": new_status},
)
messages.success(request, f"Inquiry status changed to {new_status}.")
return redirect("complaints:inquiry_detail", pk=pk)
@login_required
@require_http_methods(["POST"])
def inquiry_add_note(request, pk):
"""Add note to inquiry"""
from .models import Inquiry
inquiry = get_object_or_404(Inquiry, pk=pk)
note = request.POST.get("note")
if not note:
messages.error(request, "Please enter a note.")
return redirect("complaints:inquiry_detail", pk=pk)
# Create update
InquiryUpdate.objects.create(inquiry=inquiry, update_type="note", message=note, created_by=request.user)
messages.success(request, "Note added successfully.")
return redirect("complaints:inquiry_detail", pk=pk)
@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()
# Create update
InquiryUpdate.objects.create(
inquiry=inquiry, update_type="response", message="Response sent", created_by=request.user
)
# 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)
# ============================================================================
# PUBLIC COMPLAINT FORM (No Authentication Required)
# ============================================================================
def public_complaint_submit(request):
"""
Public complaint submission form (accessible without login).
Handles both GET (show form) and POST (submit complaint).
Key changes for AI-powered classification:
- Simplified form with only 5 required fields: name, email, phone, hospital, description
- AI generates: title, category, subcategory, department, severity, priority
- Patient lookup removed - contact info stored directly
"""
if request.method == "POST":
try:
# Get form data from simplified form
name = request.POST.get("name")
email = request.POST.get("email")
phone = request.POST.get("phone")
hospital_id = request.POST.get("hospital")
category_id = request.POST.get("category")
subcategory_id = request.POST.get("subcategory")
description = request.POST.get("description")
# Validate required fields
errors = []
if not name:
errors.append("Name is required")
if not email:
errors.append("Email is required")
if not phone:
errors.append("Phone is required")
if not hospital_id:
errors.append("Hospital is required")
if not category_id:
errors.append("Category is required")
if not description:
errors.append("Description is required")
if errors:
if request.headers.get("x-requested-with") == "XMLHttpRequest":
return JsonResponse({"success": False, "errors": errors}, status=400)
else:
messages.error(request, "Please fill in all required fields.")
return render(
request,
"complaints/public_complaint_form.html",
{
"hospitals": Hospital.objects.filter(status="active").order_by("name"),
},
)
# Get hospital
hospital = Hospital.objects.get(id=hospital_id)
# Get category and subcategory
from .models import ComplaintCategory
category = ComplaintCategory.objects.get(id=category_id)
subcategory = None
if subcategory_id:
subcategory = ComplaintCategory.objects.get(id=subcategory_id)
# Generate unique reference number: CMP-YYYYMMDD-XXXXX
import uuid
from datetime import datetime
today = datetime.now().strftime("%Y%m%d")
random_suffix = str(uuid.uuid4().int)[:6]
reference_number = f"CMP-{today}-{random_suffix}"
# Create complaint with user-selected category/subcategory
complaint = Complaint.objects.create(
patient=None, # No patient record for public submissions
hospital=hospital,
department=None, # AI will determine this
title="Complaint", # AI will generate title
description=description,
category=category, # category is ForeignKey, assign the instance
subcategory=subcategory.code if subcategory else "", # subcategory is CharField, assign the code
severity="medium", # Default, AI will update
priority="medium", # Default, AI will update
status="open", # Start as open
reference_number=reference_number,
# Contact info from simplified form
contact_name=name,
contact_phone=phone,
contact_email=email,
)
# Create initial update
ComplaintUpdate.objects.create(
complaint=complaint,
update_type="note",
message="Complaint submitted via public form. AI analysis running in background.",
)
# Trigger AI analysis in the background using Celery
from .tasks import analyze_complaint_with_ai
analyze_complaint_with_ai.delay(str(complaint.id))
# If form was submitted via AJAX, return JSON
if request.headers.get("x-requested-with") == "XMLHttpRequest":
return JsonResponse(
{
"success": True,
"reference_number": reference_number,
"message": "Complaint submitted successfully. AI has analyzed and classified your complaint.",
}
)
# Otherwise, redirect to success page
return redirect("complaints:public_complaint_success", reference=reference_number)
except Hospital.DoesNotExist:
error_msg = "Selected hospital not found."
if request.headers.get("x-requested-with") == "XMLHttpRequest":
return JsonResponse({"success": False, "message": error_msg}, status=400)
messages.error(request, error_msg)
return render(
request,
"complaints/public_complaint_form.html",
{
"hospitals": Hospital.objects.filter(status="active").order_by("name"),
},
)
except Exception as e:
import traceback
traceback.print_exc()
# If AJAX, return error JSON
if request.headers.get("x-requested-with") == "XMLHttpRequest":
return JsonResponse({"success": False, "message": str(e)}, status=400)
# Otherwise, show error message
return render(
request,
"complaints/public_complaint_form.html",
{"hospitals": Hospital.objects.filter(status="active").order_by("name"), "error": str(e)},
)
# GET request - show form
return render(
request,
"complaints/public_complaint_form.html",
{
"hospitals": Hospital.objects.filter(status="active").order_by("name"),
},
)
def public_complaint_success(request, reference):
"""
Success page after public complaint submission.
"""
return render(request, "complaints/public_complaint_success.html", {"reference_number": reference})
def api_lookup_patient(request):
"""
AJAX endpoint to look up patient by national ID.
No authentication required for public form.
"""
from apps.organizations.models import Patient
national_id = request.GET.get("national_id")
if not national_id:
return JsonResponse({"found": False, "error": "National ID required"}, status=400)
try:
patient = Patient.objects.get(national_id=national_id, status="active")
return JsonResponse(
{
"found": True,
"mrn": patient.mrn,
"name": patient.get_full_name(),
"phone": patient.phone or "",
"email": patient.email or "",
}
)
except Patient.DoesNotExist:
return JsonResponse({"found": False, "message": "Patient not found"})
def api_load_departments(request):
"""
AJAX endpoint to load departments for a hospital.
No authentication required for public form.
"""
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")
return JsonResponse({"departments": list(departments)})
def api_load_categories(request):
"""
AJAX endpoint to load complaint categories for a hospital.
Shows hospital-specific categories first, then system-wide categories.
Returns both parent categories and their subcategories with parent_id.
No authentication required for public form.
"""
from .models import ComplaintCategory
hospital_id = request.GET.get("hospital_id")
# Build queryset
if hospital_id:
# Return hospital-specific and system-wide categories
# Empty hospitals list = system-wide
categories_queryset = (
ComplaintCategory.objects.filter(Q(hospitals__id=hospital_id) | Q(hospitals__isnull=True), is_active=True)
.distinct()
.order_by("order", "name_en")
)
else:
# Return only system-wide categories (empty hospitals list)
categories_queryset = ComplaintCategory.objects.filter(hospitals__isnull=True, is_active=True).order_by(
"order", "name_en"
)
# Get all categories with parent_id and descriptions
categories = categories_queryset.values(
"id", "name_en", "name_ar", "code", "parent_id", "description_en", "description_ar"
)
return JsonResponse({"categories": list(categories)})
# ============================================================================
# AJAX/API HELPERS (Authentication Required)
# ============================================================================
@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_staff_by_department(request):
"""Get staff for a department (AJAX)"""
department_id = request.GET.get("department_id")
if not department_id:
return JsonResponse({"staff": []})
staff_members = Staff.objects.filter(department_id=department_id, status="active").values(
"id", "first_name", "last_name", "staff_type", "job_title"
)
return JsonResponse({"staff": list(staff_members)})
@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})
# ============================================================================
# SLA CONFIGURATION MANAGEMENT
# ============================================================================
@login_required
def sla_config_list(request):
"""
SLA Configuration list view with filters.
Allows Hospital Admins and PX Admins to manage SLA configurations.
"""
from .models import ComplaintSLAConfig
# 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 manage SLA configurations.")
return redirect("accounts:settings")
# Base queryset
queryset = ComplaintSLAConfig.objects.select_related("hospital").all()
# Apply hospital filter
if not user.is_px_admin() and user.hospital:
queryset = queryset.filter(hospital=user.hospital)
# Apply filters from request
hospital_filter = request.GET.get("hospital")
if hospital_filter:
queryset = queryset.filter(hospital_id=hospital_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)
is_active_filter = request.GET.get("is_active")
if is_active_filter:
queryset = queryset.filter(is_active=(is_active_filter == "true"))
# Ordering
order_by = request.GET.get("order_by", "hospital__name")
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)
from apps.core.models import SeverityChoices, PriorityChoices
context = {
"page_obj": page_obj,
"sla_configs": page_obj.object_list,
"hospitals": hospitals,
"severity_choices": SeverityChoices.choices,
"priority_choices": PriorityChoices.choices,
"filters": request.GET,
}
return render(request, "complaints/sla_config_list.html", context)
@login_required
@require_http_methods(["GET", "POST"])
def sla_config_create(request):
"""
Create new SLA configuration.
"""
from .models import ComplaintSLAConfig
from .forms import SLAConfigForm
# 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 create SLA configurations.")
return redirect("accounts:settings")
if request.method == "POST":
form = SLAConfigForm(request.POST, user=user)
if form.is_valid():
sla_config = form.save()
# Log audit
AuditService.log_event(
event_type="sla_config_created",
description=f"SLA configuration created: {sla_config}",
user=request.user,
content_object=sla_config,
metadata={
"hospital": str(sla_config.hospital),
"severity": sla_config.severity,
"priority": sla_config.priority,
"sla_hours": sla_config.sla_hours,
},
)
messages.success(request, "SLA configuration created successfully.")
return redirect("complaints:sla_config_list")
else:
messages.error(request, "Please correct the errors below.")
else:
form = SLAConfigForm(user=user)
context = {
"form": form,
"title": "Create SLA Configuration",
"action": "Create",
}
return render(request, "complaints/sla_config_form.html", context)
@login_required
@require_http_methods(["GET", "POST"])
def sla_config_edit(request, pk):
"""
Edit existing SLA configuration.
"""
from .models import ComplaintSLAConfig
from .forms import SLAConfigForm
# 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 edit SLA configurations.")
return redirect("accounts:settings")
sla_config = get_object_or_404(ComplaintSLAConfig, pk=pk)
# Check if user can edit this config
if not user.is_px_admin() and sla_config.hospital != user.hospital:
messages.error(request, "You don't have permission to edit this SLA configuration.")
return redirect("complaints:sla_config_list")
if request.method == "POST":
form = SLAConfigForm(request.POST, user=user, instance=sla_config)
if form.is_valid():
sla_config = form.save()
# Log audit
AuditService.log_event(
event_type="sla_config_updated",
description=f"SLA configuration updated: {sla_config}",
user=request.user,
content_object=sla_config,
metadata={
"hospital": str(sla_config.hospital),
"severity": sla_config.severity,
"priority": sla_config.priority,
"sla_hours": sla_config.sla_hours,
},
)
messages.success(request, "SLA configuration updated successfully.")
return redirect("complaints:sla_config_list")
else:
messages.error(request, "Please correct the errors below.")
else:
form = SLAConfigForm(user=user, instance=sla_config)
context = {
"form": form,
"sla_config": sla_config,
"title": "Edit SLA Configuration",
"action": "Update",
}
return render(request, "complaints/sla_config_form.html", context)
@login_required
@require_http_methods(["POST"])
def sla_config_delete(request, pk):
"""
Delete SLA configuration.
"""
from .models import ComplaintSLAConfig
# 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 delete SLA configurations.")
return redirect("accounts:settings")
sla_config = get_object_or_404(ComplaintSLAConfig, pk=pk)
# Check if user can delete this config
if not user.is_px_admin() and sla_config.hospital != user.hospital:
messages.error(request, "You don't have permission to delete this SLA configuration.")
return redirect("complaints:sla_config_list")
sla_config.delete()
# Log audit
AuditService.log_event(
event_type="sla_config_deleted",
description=f"SLA configuration deleted: {sla_config}",
user=request.user,
metadata={
"hospital": str(sla_config.hospital),
"severity": sla_config.severity,
"priority": sla_config.priority,
},
)
messages.success(request, "SLA configuration deleted successfully.")
return redirect("complaints:sla_config_list")
@login_required
def escalation_rule_list(request):
"""
Escalation Rules list view with filters.
Allows Hospital Admins and PX Admins to manage escalation rules.
"""
from .models import EscalationRule
# 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 manage escalation rules.")
return redirect("accounts:settings")
# Base queryset
queryset = EscalationRule.objects.select_related("hospital", "escalate_to_user").all()
# Apply hospital filter
if not user.is_px_admin() and user.hospital:
queryset = queryset.filter(hospital=user.hospital)
# Apply filters from request
hospital_filter = request.GET.get("hospital")
if hospital_filter:
queryset = queryset.filter(hospital_id=hospital_filter)
escalation_level_filter = request.GET.get("escalation_level")
if escalation_level_filter:
queryset = queryset.filter(escalation_level=escalation_level_filter)
is_active_filter = request.GET.get("is_active")
if is_active_filter:
queryset = queryset.filter(is_active=(is_active_filter == "true"))
# Ordering
order_by = request.GET.get("order_by", "hospital__name")
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)
context = {
"page_obj": page_obj,
"escalation_rules": page_obj.object_list,
"hospitals": hospitals,
"filters": request.GET,
}
return render(request, "complaints/escalation_rule_list.html", context)
@login_required
@require_http_methods(["GET", "POST"])
def escalation_rule_create(request):
"""
Create new escalation rule.
"""
from .models import EscalationRule
from .forms import EscalationRuleForm
# 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 create escalation rules.")
return redirect("accounts:settings")
if request.method == "POST":
form = EscalationRuleForm(request.POST, user=user)
if form.is_valid():
escalation_rule = form.save()
# Log audit
AuditService.log_event(
event_type="escalation_rule_created",
description=f"Escalation rule created: {escalation_rule}",
user=request.user,
content_object=escalation_rule,
metadata={
"hospital": str(escalation_rule.hospital),
"name": escalation_rule.name,
"escalation_level": escalation_rule.escalation_level,
},
)
messages.success(request, "Escalation rule created successfully.")
return redirect("complaints:escalation_rule_list")
else:
messages.error(request, "Please correct the errors below.")
else:
form = EscalationRuleForm(user=user)
context = {
"form": form,
"title": "Create Escalation Rule",
"action": "Create",
}
return render(request, "complaints/escalation_rule_form.html", context)
@login_required
@require_http_methods(["GET", "POST"])
def escalation_rule_edit(request, pk):
"""
Edit existing escalation rule.
"""
from .models import EscalationRule
from .forms import EscalationRuleForm
# 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 edit escalation rules.")
return redirect("accounts:settings")
escalation_rule = get_object_or_404(EscalationRule, pk=pk)
# Check if user can edit this rule
if not user.is_px_admin() and escalation_rule.hospital != user.hospital:
messages.error(request, "You don't have permission to edit this escalation rule.")
return redirect("complaints:escalation_rule_list")
if request.method == "POST":
form = EscalationRuleForm(request.POST, user=user, instance=escalation_rule)
if form.is_valid():
escalation_rule = form.save()
# Log audit
AuditService.log_event(
event_type="escalation_rule_updated",
description=f"Escalation rule updated: {escalation_rule}",
user=request.user,
content_object=escalation_rule,
metadata={
"hospital": str(escalation_rule.hospital),
"name": escalation_rule.name,
"escalation_level": escalation_rule.escalation_level,
},
)
messages.success(request, "Escalation rule updated successfully.")
return redirect("complaints:escalation_rule_list")
else:
messages.error(request, "Please correct the errors below.")
else:
form = EscalationRuleForm(user=user, instance=escalation_rule)
context = {
"form": form,
"escalation_rule": escalation_rule,
"title": "Edit Escalation Rule",
"action": "Update",
}
return render(request, "complaints/escalation_rule_form.html", context)
@login_required
@require_http_methods(["POST"])
def escalation_rule_delete(request, pk):
"""
Delete escalation rule.
"""
from .models import EscalationRule
# 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 delete escalation rules.")
return redirect("accounts:settings")
escalation_rule = get_object_or_404(EscalationRule, pk=pk)
# Check if user can delete this rule
if not user.is_px_admin() and escalation_rule.hospital != user.hospital:
messages.error(request, "You don't have permission to delete this escalation rule.")
return redirect("complaints:escalation_rule_list")
escalation_rule.delete()
# Log audit
AuditService.log_event(
event_type="escalation_rule_deleted",
description=f"Escalation rule deleted: {escalation_rule}",
user=request.user,
metadata={
"hospital": str(escalation_rule.hospital),
"name": escalation_rule.name,
},
)
messages.success(request, "Escalation rule deleted successfully.")
return redirect("complaints:escalation_rule_list")
@login_required
def complaint_threshold_list(request):
"""
Complaint Threshold list view with filters.
Allows Hospital Admins and PX Admins to manage complaint thresholds.
"""
from .models import ComplaintThreshold
# 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 manage complaint thresholds.")
return redirect("accounts:settings")
# Base queryset
queryset = ComplaintThreshold.objects.select_related("hospital").all()
# Apply hospital filter
if not user.is_px_admin() and user.hospital:
queryset = queryset.filter(hospital=user.hospital)
# Apply filters from request
hospital_filter = request.GET.get("hospital")
if hospital_filter:
queryset = queryset.filter(hospital_id=hospital_filter)
threshold_type_filter = request.GET.get("threshold_type")
if threshold_type_filter:
queryset = queryset.filter(threshold_type=threshold_type_filter)
is_active_filter = request.GET.get("is_active")
if is_active_filter:
queryset = queryset.filter(is_active=(is_active_filter == "true"))
# Ordering
order_by = request.GET.get("order_by", "hospital__name")
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)
context = {
"page_obj": page_obj,
"thresholds": page_obj.object_list,
"hospitals": hospitals,
"filters": request.GET,
}
return render(request, "complaints/complaint_threshold_list.html", context)
@login_required
@require_http_methods(["GET", "POST"])
def complaint_threshold_create(request):
"""
Create new complaint threshold.
"""
from .models import ComplaintThreshold
from .forms import ComplaintThresholdForm
# 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 create complaint thresholds.")
return redirect("accounts:settings")
if request.method == "POST":
form = ComplaintThresholdForm(request.POST, user=user)
if form.is_valid():
threshold = form.save()
# Log audit
AuditService.log_event(
event_type="complaint_threshold_created",
description=f"Complaint threshold created: {threshold}",
user=request.user,
content_object=threshold,
metadata={
"hospital": str(threshold.hospital),
"threshold_type": threshold.threshold_type,
"threshold_value": threshold.threshold_value,
},
)
messages.success(request, "Complaint threshold created successfully.")
return redirect("complaints:complaint_threshold_list")
else:
messages.error(request, "Please correct the errors below.")
else:
form = ComplaintThresholdForm(user=user)
context = {
"form": form,
"title": "Create Complaint Threshold",
"action": "Create",
}
return render(request, "complaints/complaint_threshold_form.html", context)
@login_required
@require_http_methods(["GET", "POST"])
def complaint_threshold_edit(request, pk):
"""
Edit existing complaint threshold.
"""
from .models import ComplaintThreshold
from .forms import ComplaintThresholdForm
# 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 edit complaint thresholds.")
return redirect("accounts:settings")
threshold = get_object_or_404(ComplaintThreshold, pk=pk)
# Check if user can edit this threshold
if not user.is_px_admin() and threshold.hospital != user.hospital:
messages.error(request, "You don't have permission to edit this complaint threshold.")
return redirect("complaints:complaint_threshold_list")
if request.method == "POST":
form = ComplaintThresholdForm(request.POST, user=user, instance=threshold)
if form.is_valid():
threshold = form.save()
# Log audit
AuditService.log_event(
event_type="complaint_threshold_updated",
description=f"Complaint threshold updated: {threshold}",
user=request.user,
content_object=threshold,
metadata={
"hospital": str(threshold.hospital),
"threshold_type": threshold.threshold_type,
"threshold_value": threshold.threshold_value,
},
)
messages.success(request, "Complaint threshold updated successfully.")
return redirect("complaints:complaint_threshold_list")
else:
messages.error(request, "Please correct the errors below.")
else:
form = ComplaintThresholdForm(user=user, instance=threshold)
context = {
"form": form,
"threshold": threshold,
"title": "Edit Complaint Threshold",
"action": "Update",
}
return render(request, "complaints/complaint_threshold_form.html", context)
@login_required
@require_http_methods(["POST"])
def complaint_threshold_delete(request, pk):
"""
Delete complaint threshold.
"""
from .models import ComplaintThreshold
# 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 delete complaint thresholds.")
return redirect("accounts:settings")
threshold = get_object_or_404(ComplaintThreshold, pk=pk)
# Check if user can delete this threshold
if not user.is_px_admin() and threshold.hospital != user.hospital:
messages.error(request, "You don't have permission to delete this complaint threshold.")
return redirect("complaints:complaint_threshold_list")
threshold.delete()
# Log audit
AuditService.log_event(
event_type="complaint_threshold_deleted",
description=f"Complaint threshold deleted: {threshold}",
user=request.user,
metadata={
"hospital": str(threshold.hospital),
"threshold_type": threshold.threshold_type,
},
)
messages.success(request, "Complaint threshold deleted successfully.")
return redirect("complaints:complaint_threshold_list")