""" 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 .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, } 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 from apps.px_sources.models import SourceUser, PXSource # Determine base layout based on user type 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 = InquiryForm(request.POST, user=request.user) if not form.is_valid(): 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/inquiry_form.html', context) try: # Save inquiry inquiry = form.save(commit=False) # Set source for source users source_user = SourceUser.objects.filter(user=request.user).first() if source_user: inquiry.source = source_user.source inquiry.created_by = request.user inquiry.save() # 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 form = InquiryForm(user=request.user) 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 = { 'form': form, 'base_layout': base_layout, 'source_user': source_user, } 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")