""" Complaints views and viewsets """ from django.utils import timezone from rest_framework import status, viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from apps.core.services import AuditService from .models import Complaint, ComplaintAttachment, ComplaintUpdate, Inquiry from .serializers import ( ComplaintAttachmentSerializer, ComplaintListSerializer, ComplaintSerializer, ComplaintUpdateSerializer, InquirySerializer, ) def map_complaint_category_to_action_category(complaint_category_code): """ Map complaint category code to PX Action category. Provides intelligent mapping from complaint categories to PX Action categories. Returns 'other' as fallback if no match found. """ if not complaint_category_code: return 'other' mapping = { # Clinical issues 'clinical': 'clinical_quality', 'medical': 'clinical_quality', 'diagnosis': 'clinical_quality', 'treatment': 'clinical_quality', 'medication': 'clinical_quality', 'care': 'clinical_quality', # Safety issues 'safety': 'patient_safety', 'risk': 'patient_safety', 'incident': 'patient_safety', 'infection': 'patient_safety', 'harm': 'patient_safety', # Service quality 'service': 'service_quality', 'communication': 'service_quality', 'wait': 'service_quality', 'response': 'service_quality', 'customer_service': 'service_quality', 'timeliness': 'service_quality', 'waiting_time': 'service_quality', # Staff behavior 'staff': 'staff_behavior', 'behavior': 'staff_behavior', 'attitude': 'staff_behavior', 'professionalism': 'staff_behavior', 'rude': 'staff_behavior', 'respect': 'staff_behavior', # Facility 'facility': 'facility', 'environment': 'facility', 'cleanliness': 'facility', 'equipment': 'facility', 'infrastructure': 'facility', 'parking': 'facility', 'accessibility': 'facility', # Process 'process': 'process_improvement', 'administrative': 'process_improvement', 'billing': 'process_improvement', 'procedure': 'process_improvement', 'workflow': 'process_improvement', 'registration': 'process_improvement', 'appointment': 'process_improvement', } # Try exact match first category_lower = complaint_category_code.lower() if category_lower in mapping: return mapping[category_lower] # Try partial match (contains the keyword) for keyword, action_category in mapping.items(): if keyword in category_lower: return action_category # Fallback to 'other' return 'other' class ComplaintViewSet(viewsets.ModelViewSet): """ ViewSet for Complaints with workflow actions. Permissions: - All authenticated users can view complaints - PX Admins and Hospital Admins can create/manage complaints """ queryset = Complaint.objects.all() permission_classes = [IsAuthenticated] filterset_fields = [ 'status', 'severity', 'priority', 'category', 'source', 'hospital', 'department', 'physician', 'assigned_to', 'is_overdue', 'hospital__organization' ] search_fields = ['title', 'description', 'patient__mrn', 'patient__first_name', 'patient__last_name'] ordering_fields = ['created_at', 'due_at', 'severity'] ordering = ['-created_at'] def get_serializer_class(self): """Use simplified serializer for list view""" if self.action == 'list': return ComplaintListSerializer return ComplaintSerializer def get_queryset(self): """Filter complaints based on user role""" queryset = super().get_queryset().select_related( 'patient', 'hospital', 'department', 'physician', 'assigned_to', 'resolved_by', 'closed_by' ).prefetch_related('attachments', 'updates') user = self.request.user # PX Admins see all complaints if user.is_px_admin(): return queryset # Hospital Admins see complaints for their hospital if user.is_hospital_admin() and user.hospital: return queryset.filter(hospital=user.hospital) # Department Managers see complaints for their department if user.is_department_manager() and user.department: return queryset.filter(department=user.department) # Others see complaints for their hospital if user.hospital: return queryset.filter(hospital=user.hospital) return queryset.none() def perform_create(self, serializer): """Log complaint creation and trigger resolution satisfaction survey""" complaint = serializer.save() AuditService.log_from_request( event_type='complaint_created', description=f"Complaint created: {complaint.title}", request=self.request, content_object=complaint, metadata={ 'category': complaint.category, 'severity': complaint.severity, 'patient_mrn': complaint.patient.mrn } ) # TODO: Optionally create PX Action (Phase 6) # from apps.complaints.tasks import create_action_from_complaint # create_action_from_complaint.delay(str(complaint.id)) @action(detail=True, methods=['post']) def assign(self, request, pk=None): """Assign complaint to user""" complaint = self.get_object() user_id = request.data.get('user_id') if not user_id: return Response( {'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST ) from apps.accounts.models import User try: user = User.objects.get(id=user_id) complaint.assigned_to = user 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 {user.get_full_name()}", created_by=request.user ) AuditService.log_from_request( event_type='assignment', description=f"Complaint assigned to {user.get_full_name()}", request=request, content_object=complaint ) return Response({'message': 'Complaint assigned successfully'}) except User.DoesNotExist: return Response( {'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND ) @action(detail=True, methods=['post']) def change_status(self, request, pk=None): """Change complaint status""" complaint = self.get_object() new_status = request.data.get('status') note = request.data.get('note', '') if not new_status: return Response( {'error': 'status is required'}, status=status.HTTP_400_BAD_REQUEST ) old_status = complaint.status complaint.status = new_status # Handle status-specific logic if new_status == 'resolved': complaint.resolved_at = timezone.now() complaint.resolved_by = request.user elif new_status == '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 ) AuditService.log_from_request( event_type='status_change', description=f"Complaint status changed from {old_status} to {new_status}", request=request, content_object=complaint, metadata={'old_status': old_status, 'new_status': new_status} ) return Response({'message': 'Status updated successfully'}) @action(detail=True, methods=['post']) def add_note(self, request, pk=None): """Add note to complaint""" complaint = self.get_object() note = request.data.get('note') if not note: return Response( {'error': 'note is required'}, status=status.HTTP_400_BAD_REQUEST ) # Create update update = ComplaintUpdate.objects.create( complaint=complaint, update_type='note', message=note, created_by=request.user ) serializer = ComplaintUpdateSerializer(update) return Response(serializer.data, status=status.HTTP_201_CREATED) @action(detail=True, methods=['post']) def create_action_from_ai(self, request, pk=None): """Create PX Action from AI-suggested action""" complaint = self.get_object() # Check if complaint has suggested action suggested_action = request.data.get('suggested_action') if not suggested_action and complaint.metadata and 'ai_analysis' in complaint.metadata: suggested_action = complaint.metadata['ai_analysis'].get('suggested_action_en') if not suggested_action: return Response( {'error': 'No suggested action available for this complaint'}, status=status.HTTP_400_BAD_REQUEST ) # Get category (optional - will be auto-mapped from complaint category if not provided) category = request.data.get('category') # If category not provided, auto-map from complaint category if not category: if complaint.category: category = map_complaint_category_to_action_category(complaint.category.code) else: category = 'other' # Validate category choice if manually provided valid_categories = [ 'clinical_quality', 'patient_safety', 'service_quality', 'staff_behavior', 'facility', 'process_improvement', 'other' ] if category not in valid_categories: return Response( {'error': f'Invalid category. Valid options: {", ".join(valid_categories)}'}, status=status.HTTP_400_BAD_REQUEST ) # Get optional assigned_to assigned_to_id = request.data.get('assigned_to') assigned_to = None if assigned_to_id: from apps.accounts.models import User try: assigned_to = User.objects.get(id=assigned_to_id) except User.DoesNotExist: return Response( {'error': 'Assigned user not found'}, status=status.HTTP_404_NOT_FOUND ) # Create PX Action from apps.px_action_center.models import PXAction, PXActionLog from django.contrib.contenttypes.models import ContentType complaint_content_type = ContentType.objects.get_for_model(Complaint) action = PXAction.objects.create( source_type='complaint', content_type=complaint_content_type, object_id=complaint.id, title=f"Action from Complaint: {complaint.title}", description=suggested_action, hospital=complaint.hospital, department=complaint.department, category=category, priority=complaint.priority, severity=complaint.severity, assigned_to=assigned_to, status='open', metadata={ 'source_complaint_id': str(complaint.id), 'source_complaint_title': complaint.title, 'ai_generated': True, 'created_from_ai_suggestion': True } ) # Create action log entry PXActionLog.objects.create( action=action, log_type='note', message=f"Action created from AI-suggested action for complaint: {complaint.title}", created_by=request.user, metadata={ 'complaint_id': str(complaint.id), 'ai_generated': True } ) # Create complaint update ComplaintUpdate.objects.create( complaint=complaint, update_type='note', message=f"PX Action created from AI-suggested action (Action #{action.id})", created_by=request.user, metadata={'action_id': str(action.id)} ) # Log audit AuditService.log_from_request( event_type='action_created_from_ai', description=f"PX Action created from AI-suggested action for complaint: {complaint.title}", request=request, content_object=action, metadata={ 'complaint_id': str(complaint.id), 'category': category, 'ai_generated': True } ) return Response({ 'action_id': str(action.id), 'message': 'Action created successfully from AI-suggested action' }, status=status.HTTP_201_CREATED) class ComplaintAttachmentViewSet(viewsets.ModelViewSet): """ViewSet for Complaint Attachments""" queryset = ComplaintAttachment.objects.all() serializer_class = ComplaintAttachmentSerializer permission_classes = [IsAuthenticated] filterset_fields = ['complaint'] ordering = ['-created_at'] def get_queryset(self): queryset = super().get_queryset().select_related('complaint', 'uploaded_by') user = self.request.user # Filter based on complaint access if user.is_px_admin(): return queryset if user.is_hospital_admin() and user.hospital: return queryset.filter(complaint__hospital=user.hospital) if user.hospital: return queryset.filter(complaint__hospital=user.hospital) return queryset.none() class InquiryViewSet(viewsets.ModelViewSet): """ViewSet for Inquiries""" queryset = Inquiry.objects.all() serializer_class = InquirySerializer permission_classes = [IsAuthenticated] filterset_fields = ['status', 'category', 'hospital', 'department', 'assigned_to', 'hospital__organization'] search_fields = ['subject', 'message', 'contact_name', 'patient__mrn'] ordering_fields = ['created_at'] ordering = ['-created_at'] def get_queryset(self): """Filter inquiries based on user role""" queryset = super().get_queryset().select_related( 'patient', 'hospital', 'department', 'assigned_to', 'responded_by' ) user = self.request.user # PX Admins see all inquiries if user.is_px_admin(): return queryset # Hospital Admins see inquiries for their hospital if user.is_hospital_admin() and user.hospital: return queryset.filter(hospital=user.hospital) # Department Managers see inquiries for their department if user.is_department_manager() and user.department: return queryset.filter(department=user.department) # Others see inquiries for their hospital if user.hospital: return queryset.filter(hospital=user.hospital) return queryset.none() @action(detail=True, methods=['post']) def respond(self, request, pk=None): """Respond to inquiry""" inquiry = self.get_object() response_text = request.data.get('response') if not response_text: return Response( {'error': 'response is required'}, status=status.HTTP_400_BAD_REQUEST ) inquiry.response = response_text inquiry.responded_at = timezone.now() inquiry.responded_by = request.user inquiry.status = 'resolved' inquiry.save() return Response({'message': 'Response submitted successfully'})