HH/apps/complaints/views.py
2026-01-06 18:26:17 +03:00

481 lines
16 KiB
Python

"""
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'})