""" 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, 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() 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, } 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() # 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, '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 Exception as e: messages.error(request, f"Error creating complaint: {str(e)}") 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, } 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 source='public', # Mark as public submission 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})