1606 lines
56 KiB
Python
1606 lines
56 KiB
Python
"""
|
|
Complaints views and viewsets
|
|
"""
|
|
from django.db.models import Q
|
|
from django.shortcuts import get_object_or_404
|
|
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', 'staff', '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', 'staff',
|
|
'assigned_to', 'resolved_by', 'closed_by', 'created_by'
|
|
).prefetch_related('attachments', 'updates')
|
|
|
|
user = self.request.user
|
|
|
|
# PX Admins see all complaints
|
|
if user.is_px_admin():
|
|
return queryset
|
|
|
|
# Source Users see ONLY complaints THEY created
|
|
if hasattr(user, 'source_user_profile') and user.source_user_profile.exists():
|
|
return queryset.filter(created_by=user)
|
|
|
|
# Patients see ONLY their own complaints (if they have user accounts)
|
|
# This assumes patients can have user accounts linked via patient.user
|
|
if hasattr(user, 'patient_profile'):
|
|
return queryset.filter(patient__user=user)
|
|
|
|
# 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 get_object(self):
|
|
"""
|
|
Override get_object to allow PX Admins to access complaints
|
|
for specific actions (request_explanation, resend_explanation, send_notification, assignable_admins).
|
|
"""
|
|
queryset = self.filter_queryset(self.get_queryset())
|
|
|
|
# PX Admins can access any complaint for specific actions
|
|
if self.request.user.is_px_admin() and self.action in [
|
|
'request_explanation', 'resend_explanation', 'send_notification', 'assignable_admins'
|
|
]:
|
|
# Bypass queryset filtering and get directly by pk
|
|
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
|
lookup_value = self.kwargs[lookup_url_kwarg]
|
|
return get_object_or_404(Complaint, pk=lookup_value)
|
|
|
|
# Normal behavior for other users/actions
|
|
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
|
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
|
|
obj = get_object_or_404(queryset, **filter_kwargs)
|
|
|
|
# May raise a permission denied
|
|
self.check_object_permissions(self.request, obj)
|
|
return obj
|
|
|
|
def perform_create(self, serializer):
|
|
"""Log complaint creation and trigger resolution satisfaction survey"""
|
|
# Auto-set created_by from request.user
|
|
complaint = serializer.save(created_by=self.request.user)
|
|
|
|
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,
|
|
'created_by': str(complaint.created_by.id) if complaint.created_by else None
|
|
}
|
|
)
|
|
|
|
# Trigger AI analysis (includes PX Action auto-creation if enabled)
|
|
from apps.complaints.tasks import analyze_complaint_with_ai
|
|
analyze_complaint_with_ai.delay(str(complaint.id))
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def assign(self, request, pk=None):
|
|
"""Assign complaint to user (PX Admin or Hospital Admin)"""
|
|
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)
|
|
|
|
# Verify user has appropriate role
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
return Response(
|
|
{'error': 'Only PX Admins and Hospital Admins can be assigned to complaints'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
old_assignee = complaint.assigned_to
|
|
complaint.assigned_to = user
|
|
complaint.assigned_at = timezone.now()
|
|
complaint.save(update_fields=['assigned_to', 'assigned_at'])
|
|
|
|
# Create update
|
|
roles_display = ', '.join(user.get_role_names())
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='assignment',
|
|
message=f"Assigned to {user.get_full_name()} ({roles_display})",
|
|
created_by=request.user,
|
|
metadata={
|
|
'old_assignee_id': str(old_assignee.id) if old_assignee else None,
|
|
'new_assignee_id': str(user.id),
|
|
'assignee_roles': user.get_role_names()
|
|
}
|
|
)
|
|
|
|
AuditService.log_from_request(
|
|
event_type='assignment',
|
|
description=f"Complaint assigned to {user.get_full_name()} ({roles_display})",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata={
|
|
'old_assignee_id': str(old_assignee.id) if old_assignee else None,
|
|
'new_assignee_id': str(user.id)
|
|
}
|
|
)
|
|
|
|
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=['get'])
|
|
def assignable_admins(self, request, pk=None):
|
|
"""
|
|
Get assignable admins (PX Admins and Hospital Admins) for this complaint.
|
|
|
|
Returns list of all PX Admins and Hospital Admins.
|
|
Supports searching by name.
|
|
"""
|
|
complaint = self.get_object()
|
|
|
|
# Check if user has permission to assign admins
|
|
if not request.user.is_px_admin():
|
|
return Response(
|
|
{'error': 'Only PX Admins can assign complaints to admins'},
|
|
status=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
from apps.accounts.models import User
|
|
|
|
# Get search parameter
|
|
search = request.query_params.get('search', '').strip()
|
|
|
|
# Simple query - get all PX Admins and Hospital Admins
|
|
base_query = Q(groups__name='PX Admin') | Q(groups__name='Hospital Admin')
|
|
|
|
queryset = User.objects.filter(
|
|
base_query,
|
|
is_active=True
|
|
).select_related('hospital').prefetch_related('groups').order_by('first_name', 'last_name')
|
|
|
|
# Search by name or email if provided
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(first_name__icontains=search) |
|
|
Q(last_name__icontains=search) |
|
|
Q(email__icontains=search)
|
|
)
|
|
|
|
# Serialize
|
|
admins_list = []
|
|
for user in queryset:
|
|
roles = user.get_role_names()
|
|
role_display = ', '.join(roles)
|
|
|
|
admins_list.append({
|
|
'id': str(user.id),
|
|
'name': user.get_full_name(),
|
|
'email': user.email,
|
|
'roles': roles,
|
|
'role_display': role_display,
|
|
'hospital': user.hospital.name if user.hospital else None,
|
|
'is_px_admin': user.is_px_admin(),
|
|
'is_hospital_admin': user.is_hospital_admin()
|
|
})
|
|
|
|
return Response({
|
|
'complaint_id': str(complaint.id),
|
|
'hospital_id': str(complaint.hospital.id),
|
|
'hospital_name': complaint.hospital.name,
|
|
'current_assignee': {
|
|
'id': str(complaint.assigned_to.id),
|
|
'name': complaint.assigned_to.get_full_name(),
|
|
'email': complaint.assigned_to.email,
|
|
'roles': complaint.assigned_to.get_role_names()
|
|
} if complaint.assigned_to else None,
|
|
'admin_count': len(admins_list),
|
|
'admins': admins_list
|
|
})
|
|
|
|
@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=['get'])
|
|
def staff_suggestions(self, request, pk=None):
|
|
"""
|
|
Get staff matching suggestions for a complaint.
|
|
|
|
Returns potential staff matches from AI analysis,
|
|
allowing PX Admins to review and select correct staff.
|
|
"""
|
|
complaint = self.get_object()
|
|
|
|
# Check if user is PX Admin
|
|
if not request.user.is_px_admin():
|
|
return Response(
|
|
{'error': 'Only PX Admins can access staff suggestions'},
|
|
status=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
# Get AI analysis metadata
|
|
ai_analysis = complaint.metadata.get('ai_analysis', {})
|
|
staff_matches = ai_analysis.get('staff_matches', [])
|
|
extracted_name = ai_analysis.get('extracted_staff_name', '')
|
|
needs_review = ai_analysis.get('needs_staff_review', False)
|
|
matched_staff_id = ai_analysis.get('matched_staff_id')
|
|
|
|
return Response({
|
|
'extracted_name': extracted_name,
|
|
'staff_matches': staff_matches,
|
|
'current_staff_id': matched_staff_id,
|
|
'needs_staff_review': needs_staff_review,
|
|
'staff_match_count': len(staff_matches)
|
|
})
|
|
|
|
@action(detail=True, methods=['get'])
|
|
def hospital_staff(self, request, pk=None):
|
|
"""
|
|
Get all staff from complaint's hospital for manual selection.
|
|
|
|
Allows PX Admins to manually select staff.
|
|
Supports filtering by department.
|
|
"""
|
|
complaint = self.get_object()
|
|
|
|
# Check if user is PX Admin
|
|
if not request.user.is_px_admin():
|
|
return Response(
|
|
{'error': 'Only PX Admins can access hospital staff list'},
|
|
status=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
from apps.organizations.models import Staff
|
|
|
|
# Get query params
|
|
department_id = request.query_params.get('department_id')
|
|
search = request.query_params.get('search', '').strip()
|
|
|
|
# Build query
|
|
queryset = Staff.objects.filter(
|
|
hospital=complaint.hospital,
|
|
status='active'
|
|
).select_related('department')
|
|
|
|
# Filter by department if specified
|
|
if department_id:
|
|
queryset = queryset.filter(department_id=department_id)
|
|
|
|
# Search by name if provided
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(first_name__icontains=search) |
|
|
Q(last_name__icontains=search) |
|
|
Q(first_name_ar__icontains=search) |
|
|
Q(last_name_ar__icontains=search) |
|
|
Q(job_title__icontains=search)
|
|
)
|
|
|
|
# Order by department and name
|
|
queryset = queryset.order_by('department__name', 'first_name', 'last_name')
|
|
|
|
# Serialize
|
|
staff_list = []
|
|
for staff in queryset:
|
|
staff_list.append({
|
|
'id': str(staff.id),
|
|
'name_en': f"{staff.first_name} {staff.last_name}",
|
|
'name_ar': f"{staff.first_name_ar} {staff.last_name_ar}" if staff.first_name_ar and staff.last_name_ar else "",
|
|
'job_title': staff.job_title,
|
|
'specialization': staff.specialization,
|
|
'department': staff.department.name if staff.department else None,
|
|
'department_id': str(staff.department.id) if staff.department else None
|
|
})
|
|
|
|
return Response({
|
|
'hospital_id': str(complaint.hospital.id),
|
|
'hospital_name': complaint.hospital.name,
|
|
'staff_count': len(staff_list),
|
|
'staff': staff_list
|
|
})
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def assign_staff(self, request, pk=None):
|
|
"""
|
|
Manually assign staff to a complaint.
|
|
|
|
Allows PX Admins to assign specific staff member,
|
|
especially when AI matching is ambiguous.
|
|
"""
|
|
complaint = self.get_object()
|
|
|
|
# Check if user is PX Admin
|
|
if not request.user.is_px_admin():
|
|
return Response(
|
|
{'error': 'Only PX Admins can assign staff to complaints'},
|
|
status=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
staff_id = request.data.get('staff_id')
|
|
reason = request.data.get('reason', '')
|
|
|
|
if not staff_id:
|
|
return Response(
|
|
{'error': 'staff_id is required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
from apps.organizations.models import Staff
|
|
try:
|
|
staff = Staff.objects.get(id=staff_id)
|
|
except Staff.DoesNotExist:
|
|
return Response(
|
|
{'error': 'Staff not found'},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
|
|
# Check staff belongs to same hospital
|
|
if staff.hospital != complaint.hospital:
|
|
return Response(
|
|
{'error': 'Staff does not belong to complaint hospital'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Update complaint
|
|
old_staff_id = str(complaint.staff.id) if complaint.staff else None
|
|
complaint.staff = staff
|
|
# Auto-set department from staff
|
|
complaint.department = staff.department
|
|
complaint.save(update_fields=['staff', 'department'])
|
|
|
|
# Update metadata to clear review flag
|
|
if not complaint.metadata:
|
|
complaint.metadata = {}
|
|
if 'ai_analysis' in complaint.metadata:
|
|
complaint.metadata['ai_analysis']['needs_staff_review'] = False
|
|
complaint.metadata['ai_analysis']['staff_manually_assigned'] = True
|
|
complaint.metadata['ai_analysis']['staff_assigned_by'] = str(request.user.id)
|
|
complaint.metadata['ai_analysis']['staff_assigned_at'] = timezone.now().isoformat()
|
|
complaint.metadata['ai_analysis']['staff_assignment_reason'] = reason
|
|
complaint.save(update_fields=['metadata'])
|
|
|
|
# Create update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='assignment',
|
|
message=f"Staff assigned to {staff.first_name} {staff.last_name} ({staff.job_title}). {reason}" if reason else f"Staff assigned to {staff.first_name} {staff.last_name} ({staff.job_title})",
|
|
created_by=request.user,
|
|
metadata={
|
|
'old_staff_id': old_staff_id,
|
|
'new_staff_id': str(staff.id),
|
|
'manual_assignment': True
|
|
}
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_from_request(
|
|
event_type='staff_assigned',
|
|
description=f"Staff {staff.first_name} {staff.last_name} manually assigned to complaint by {request.user.get_full_name()}",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata={
|
|
'old_staff_id': old_staff_id,
|
|
'new_staff_id': str(staff.id),
|
|
'reason': reason
|
|
}
|
|
)
|
|
|
|
return Response({
|
|
'message': 'Staff assigned successfully',
|
|
'staff_id': str(staff.id),
|
|
'staff_name': f"{staff.first_name} {staff.last_name}"
|
|
})
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def change_department(self, request, pk=None):
|
|
"""Change complaint department"""
|
|
complaint = self.get_object()
|
|
department_id = request.data.get('department_id')
|
|
|
|
if not department_id:
|
|
return Response(
|
|
{'error': 'department_id is required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
from apps.organizations.models import Department
|
|
try:
|
|
department = Department.objects.get(id=department_id)
|
|
except Department.DoesNotExist:
|
|
return Response(
|
|
{'error': 'Department not found'},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
|
|
# Check department belongs to same hospital
|
|
if department.hospital != complaint.hospital:
|
|
return Response(
|
|
{'error': 'Department does not belong to complaint hospital'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Update complaint
|
|
old_department_id = str(complaint.department.id) if complaint.department else None
|
|
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': old_department_id,
|
|
'new_department_id': str(department.id)
|
|
}
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_from_request(
|
|
event_type='department_change',
|
|
description=f"Complaint department changed to {department.name}",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata={
|
|
'old_department_id': old_department_id,
|
|
'new_department_id': str(department.id)
|
|
}
|
|
)
|
|
|
|
return Response({
|
|
'message': 'Department changed successfully',
|
|
'department_id': str(department.id),
|
|
'department_name': department.name
|
|
})
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def create_action_from_ai(self, request, pk=None):
|
|
"""Create PX Action using AI service to generate action details from complaint"""
|
|
complaint = self.get_object()
|
|
|
|
# Use AI service to generate action data
|
|
from apps.core.ai_service import AIService
|
|
|
|
try:
|
|
action_data = AIService.create_px_action_from_complaint(complaint)
|
|
except Exception as e:
|
|
return Response(
|
|
{'error': f'Failed to generate action data: {str(e)}'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
# Get optional assigned_to from request (AI doesn't assign by default)
|
|
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=action_data['title'],
|
|
description=action_data['description'],
|
|
hospital=complaint.hospital,
|
|
department=complaint.department,
|
|
category=action_data['category'],
|
|
priority=action_data['priority'],
|
|
severity=action_data['severity'],
|
|
assigned_to=assigned_to,
|
|
status='open',
|
|
metadata={
|
|
'source_complaint_id': str(complaint.id),
|
|
'source_complaint_title': complaint.title,
|
|
'ai_generated': True,
|
|
'ai_reasoning': action_data.get('reasoning', ''),
|
|
'created_from_ai_suggestion': True
|
|
}
|
|
)
|
|
|
|
# Create action log entry
|
|
PXActionLog.objects.create(
|
|
action=action,
|
|
log_type='note',
|
|
message=f"Action generated by AI for complaint: {complaint.title}",
|
|
created_by=request.user,
|
|
metadata={
|
|
'complaint_id': str(complaint.id),
|
|
'ai_generated': True,
|
|
'category': action_data['category'],
|
|
'priority': action_data['priority'],
|
|
'severity': action_data['severity']
|
|
}
|
|
)
|
|
|
|
# Create complaint update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='note',
|
|
message=f"PX Action created from AI-generated suggestion (Action #{action.id}) - {action_data['category']}",
|
|
created_by=request.user,
|
|
metadata={'action_id': str(action.id), 'category': action_data['category']}
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_from_request(
|
|
event_type='action_created_from_ai',
|
|
description=f"PX Action created from AI analysis for complaint: {complaint.title}",
|
|
request=request,
|
|
content_object=action,
|
|
metadata={
|
|
'complaint_id': str(complaint.id),
|
|
'category': action_data['category'],
|
|
'priority': action_data['priority'],
|
|
'severity': action_data['severity'],
|
|
'ai_reasoning': action_data.get('reasoning', '')
|
|
}
|
|
)
|
|
|
|
return Response({
|
|
'action_id': str(action.id),
|
|
'message': 'Action created successfully from AI analysis',
|
|
'action_data': {
|
|
'title': action_data['title'],
|
|
'category': action_data['category'],
|
|
'priority': action_data['priority'],
|
|
'severity': action_data['severity']
|
|
}
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def send_notification(self, request, pk=None):
|
|
"""
|
|
Send email notification to staff member or department head.
|
|
|
|
Sends complaint notification with AI-generated summary (editable by user).
|
|
Logs the operation to NotificationLog and ComplaintUpdate.
|
|
|
|
Recipient Priority:
|
|
1. Staff with user account
|
|
2. Staff with email field
|
|
3. Department manager
|
|
"""
|
|
complaint = self.get_object()
|
|
|
|
# Get email message (required)
|
|
email_message = request.data.get('email_message', '').strip()
|
|
if not email_message:
|
|
return Response(
|
|
{'error': 'email_message is required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Get additional message (optional)
|
|
additional_message = request.data.get('additional_message', '').strip()
|
|
|
|
# Determine recipient with priority logic
|
|
recipient = None
|
|
recipient_display = None
|
|
recipient_type = None
|
|
recipient_email = None
|
|
|
|
# Priority 1: Staff member with user account
|
|
if complaint.staff and complaint.staff.user:
|
|
recipient = complaint.staff.user
|
|
recipient_display = str(complaint.staff)
|
|
recipient_type = 'Staff Member (User Account)'
|
|
recipient_email = recipient.email
|
|
|
|
# Priority 2: Staff member with email field (no user account)
|
|
elif complaint.staff and complaint.staff.email:
|
|
recipient_display = str(complaint.staff)
|
|
recipient_type = 'Staff Member (Email)'
|
|
recipient_email = complaint.staff.email
|
|
|
|
# Priority 3: Department head
|
|
elif complaint.department and complaint.department.manager:
|
|
recipient = complaint.department.manager
|
|
recipient_display = recipient.get_full_name()
|
|
recipient_type = 'Department Head'
|
|
recipient_email = recipient.email
|
|
|
|
# Check if we found a recipient with email
|
|
if not recipient_email:
|
|
return Response(
|
|
{'error': 'No valid recipient found. Complaint must have staff with email, or a department manager with email.'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Construct email content
|
|
subject = f"Complaint Notification - #{complaint.id}"
|
|
|
|
# Build email body
|
|
email_body = f"""
|
|
Dear {recipient_display},
|
|
|
|
You have been assigned to review the following complaint:
|
|
|
|
COMPLAINT DETAILS:
|
|
----------------
|
|
ID: #{complaint.id}
|
|
Title: {complaint.title}
|
|
Severity: {complaint.get_severity_display()}
|
|
Priority: {complaint.get_priority_display()}
|
|
Status: {complaint.get_status_display()}
|
|
|
|
SUMMARY:
|
|
--------
|
|
{email_message}
|
|
|
|
"""
|
|
|
|
# Add patient info if available
|
|
if complaint.patient:
|
|
email_body += f"""
|
|
PATIENT INFORMATION:
|
|
------------------
|
|
Name: {complaint.patient.get_full_name()}
|
|
MRN: {complaint.patient.mrn}
|
|
"""
|
|
|
|
# Add additional message if provided
|
|
if additional_message:
|
|
email_body += f"""
|
|
|
|
ADDITIONAL MESSAGE:
|
|
------------------
|
|
{additional_message}
|
|
"""
|
|
|
|
# Add link to complaint
|
|
from django.contrib.sites.shortcuts import get_current_site
|
|
site = get_current_site(request)
|
|
complaint_url = f"https://{site.domain}/complaints/{complaint.id}/"
|
|
|
|
email_body += f"""
|
|
|
|
To view the full complaint details, please visit:
|
|
{complaint_url}
|
|
|
|
Thank you for your attention to this matter.
|
|
|
|
---
|
|
This is an automated message from PX360 Complaint Management System.
|
|
"""
|
|
|
|
# Send email using NotificationService
|
|
from apps.notifications.services import NotificationService
|
|
|
|
try:
|
|
notification_log = NotificationService.send_email(
|
|
email=recipient_email,
|
|
subject=subject,
|
|
message=email_body,
|
|
related_object=complaint,
|
|
metadata={
|
|
'notification_type': 'complaint_notification',
|
|
'recipient_type': recipient_type,
|
|
'recipient_id': str(recipient.id) if recipient else None,
|
|
'sender_id': str(request.user.id),
|
|
'has_additional_message': bool(additional_message)
|
|
}
|
|
)
|
|
except Exception as e:
|
|
return Response(
|
|
{'error': f'Failed to send email: {str(e)}'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
# Create ComplaintUpdate entry
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='communication',
|
|
message=f"Email notification sent to {recipient_type}: {recipient_display}",
|
|
created_by=request.user,
|
|
metadata={
|
|
'recipient_type': recipient_type,
|
|
'recipient_id': str(recipient.id) if recipient else None,
|
|
'notification_log_id': str(notification_log.id) if notification_log else None
|
|
}
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_from_request(
|
|
event_type='notification_sent',
|
|
description=f"Email notification sent to {recipient_type}: {recipient_display}",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata={
|
|
'recipient_type': recipient_type,
|
|
'recipient_id': str(recipient.id) if recipient else None,
|
|
'recipient_email': recipient_email
|
|
}
|
|
)
|
|
|
|
return Response({
|
|
'success': True,
|
|
'message': 'Email notification sent successfully',
|
|
'recipient': recipient_display,
|
|
'recipient_type': recipient_type,
|
|
'recipient_email': recipient_email
|
|
})
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def request_explanation(self, request, pk=None):
|
|
"""
|
|
Request explanation from staff/recipient.
|
|
|
|
Generates a unique token and sends email with secure link.
|
|
Token can only be used once.
|
|
"""
|
|
complaint = self.get_object()
|
|
|
|
# Check if complaint has staff to request explanation from
|
|
if not complaint.staff:
|
|
return Response(
|
|
{'error': 'Complaint has no staff assigned to request explanation from'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Check if explanation already exists for this staff
|
|
from .models import ComplaintExplanation
|
|
existing_explanation = ComplaintExplanation.objects.filter(
|
|
complaint=complaint,
|
|
staff=complaint.staff
|
|
).first()
|
|
|
|
if existing_explanation and existing_explanation.is_used:
|
|
return Response(
|
|
{'error': 'This staff member has already submitted an explanation'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Get optional message
|
|
request_message = request.data.get('request_message', '').strip()
|
|
|
|
# Generate unique token
|
|
import secrets
|
|
token = secrets.token_urlsafe(32)
|
|
|
|
# Create or update explanation record
|
|
if existing_explanation:
|
|
explanation = existing_explanation
|
|
explanation.token = token
|
|
explanation.is_used = False
|
|
explanation.requested_by = request.user
|
|
explanation.request_message = request_message
|
|
explanation.email_sent_at = timezone.now()
|
|
explanation.save()
|
|
else:
|
|
explanation = ComplaintExplanation.objects.create(
|
|
complaint=complaint,
|
|
staff=complaint.staff,
|
|
token=token,
|
|
is_used=False,
|
|
submitted_via='email_link',
|
|
requested_by=request.user,
|
|
request_message=request_message,
|
|
email_sent_at=timezone.now()
|
|
)
|
|
|
|
# Send email with explanation link
|
|
from django.contrib.sites.shortcuts import get_current_site
|
|
from apps.notifications.services import NotificationService
|
|
|
|
site = get_current_site(request)
|
|
explanation_link = f"https://{site.domain}/complaints/{complaint.id}/explain/{token}/"
|
|
|
|
# Determine recipient email
|
|
if complaint.staff.user and complaint.staff.user.email:
|
|
recipient_email = complaint.staff.user.email
|
|
recipient_display = str(complaint.staff)
|
|
elif complaint.staff.email:
|
|
recipient_email = complaint.staff.email
|
|
recipient_display = str(complaint.staff)
|
|
else:
|
|
return Response(
|
|
{'error': 'Staff member has no email address'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Build email subject
|
|
subject = f"Explanation Request - Complaint #{complaint.id}"
|
|
|
|
# Build email body
|
|
email_body = f"""
|
|
Dear {recipient_display},
|
|
|
|
We have received a complaint that requires your explanation.
|
|
|
|
COMPLAINT DETAILS:
|
|
----------------
|
|
Reference: #{complaint.id}
|
|
Title: {complaint.title}
|
|
Severity: {complaint.get_severity_display()}
|
|
Priority: {complaint.get_priority_display()}
|
|
|
|
{complaint.description}
|
|
|
|
"""
|
|
|
|
# Add patient info if available
|
|
if complaint.patient:
|
|
email_body += f"""
|
|
PATIENT INFORMATION:
|
|
------------------
|
|
Name: {complaint.patient.get_full_name()}
|
|
MRN: {complaint.patient.mrn}
|
|
"""
|
|
|
|
# Add request message if provided
|
|
if request_message:
|
|
email_body += f"""
|
|
|
|
ADDITIONAL MESSAGE:
|
|
------------------
|
|
{request_message}
|
|
"""
|
|
|
|
email_body += f"""
|
|
|
|
SUBMIT YOUR EXPLANATION:
|
|
------------------------
|
|
Your perspective is important. Please submit your explanation about this complaint:
|
|
{explanation_link}
|
|
|
|
Note: This link can only be used once. After submission, it will expire.
|
|
|
|
If you have any questions, please contact the PX team.
|
|
|
|
---
|
|
This is an automated message from PX360 Complaint Management System.
|
|
"""
|
|
|
|
# Send email
|
|
try:
|
|
notification_log = NotificationService.send_email(
|
|
email=recipient_email,
|
|
subject=subject,
|
|
message=email_body,
|
|
related_object=complaint,
|
|
metadata={
|
|
'notification_type': 'explanation_request',
|
|
'staff_id': str(complaint.staff.id),
|
|
'explanation_id': str(explanation.id),
|
|
'requested_by_id': str(request.user.id),
|
|
'has_request_message': bool(request_message)
|
|
}
|
|
)
|
|
except Exception as e:
|
|
return Response(
|
|
{'error': f'Failed to send email: {str(e)}'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
# Create ComplaintUpdate entry
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='communication',
|
|
message=f"Explanation request sent to {recipient_display}",
|
|
created_by=request.user,
|
|
metadata={
|
|
'explanation_id': str(explanation.id),
|
|
'staff_id': str(complaint.staff.id),
|
|
'notification_log_id': str(notification_log.id) if notification_log else None
|
|
}
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_from_request(
|
|
event_type='explanation_requested',
|
|
description=f"Explanation request sent to {recipient_display}",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata={
|
|
'explanation_id': str(explanation.id),
|
|
'staff_id': str(complaint.staff.id),
|
|
'request_message': request_message
|
|
}
|
|
)
|
|
|
|
return Response({
|
|
'success': True,
|
|
'message': 'Explanation request sent successfully',
|
|
'explanation_id': str(explanation.id),
|
|
'recipient': recipient_display,
|
|
'explanation_link': explanation_link
|
|
})
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def resend_explanation(self, request, pk=None):
|
|
"""
|
|
Resend explanation request email to staff member.
|
|
|
|
Regenerates the token with a new value and resends the email.
|
|
Only allows resending if explanation has not been submitted yet.
|
|
"""
|
|
complaint = self.get_object()
|
|
|
|
# Check if complaint has staff assigned
|
|
if not complaint.staff:
|
|
return Response(
|
|
{'error': 'No staff assigned to this complaint'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Check if explanation exists for this staff
|
|
from .models import ComplaintExplanation
|
|
try:
|
|
explanation = ComplaintExplanation.objects.filter(
|
|
complaint=complaint,
|
|
staff=complaint.staff
|
|
).latest('created_at')
|
|
except ComplaintExplanation.DoesNotExist:
|
|
return Response(
|
|
{'error': 'No explanation found for this complaint and staff'},
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
|
|
# Check if already submitted (can only resend if not submitted)
|
|
if explanation.is_used:
|
|
return Response(
|
|
{'error': 'Explanation already submitted, cannot resend. Create a new explanation request.'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Generate new token
|
|
import secrets
|
|
new_token = secrets.token_urlsafe(32)
|
|
explanation.token = new_token
|
|
explanation.email_sent_at = timezone.now()
|
|
explanation.save()
|
|
|
|
# Determine recipient email
|
|
if complaint.staff.user and complaint.staff.user.email:
|
|
recipient_email = complaint.staff.user.email
|
|
recipient_display = str(complaint.staff)
|
|
elif complaint.staff.email:
|
|
recipient_email = complaint.staff.email
|
|
recipient_display = str(complaint.staff)
|
|
else:
|
|
return Response(
|
|
{'error': 'Staff member has no email address'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Send email with new link (reuse existing email logic)
|
|
from django.contrib.sites.shortcuts import get_current_site
|
|
from apps.notifications.services import NotificationService
|
|
|
|
site = get_current_site(request)
|
|
explanation_link = f"https://{site.domain}/complaints/{complaint.id}/explain/{new_token}/"
|
|
|
|
# Build email subject
|
|
subject = f"Explanation Request (Resent) - Complaint #{complaint.id}"
|
|
|
|
# Build email body
|
|
email_body = f"""
|
|
Dear {recipient_display},
|
|
|
|
We have resent the explanation request for the following complaint:
|
|
|
|
COMPLAINT DETAILS:
|
|
----------------
|
|
Reference: #{complaint.id}
|
|
Title: {complaint.title}
|
|
Severity: {complaint.get_severity_display()}
|
|
Priority: {complaint.get_priority_display()}
|
|
Status: {complaint.get_status_display()}
|
|
|
|
{complaint.description}
|
|
"""
|
|
|
|
# Add patient info if available
|
|
if complaint.patient:
|
|
email_body += f"""
|
|
PATIENT INFORMATION:
|
|
------------------
|
|
Name: {complaint.patient.get_full_name()}
|
|
MRN: {complaint.patient.mrn}
|
|
"""
|
|
|
|
email_body += f"""
|
|
|
|
SUBMIT YOUR EXPLANATION:
|
|
------------------------
|
|
Your perspective is important. Please submit your explanation about this complaint:
|
|
{explanation_link}
|
|
|
|
Note: This link can only be used once. After submission, it will expire.
|
|
|
|
If you have any questions, please contact PX team.
|
|
|
|
---
|
|
This is an automated message from PX360 Complaint Management System.
|
|
"""
|
|
|
|
# Send email
|
|
try:
|
|
notification_log = NotificationService.send_email(
|
|
email=recipient_email,
|
|
subject=subject,
|
|
message=email_body,
|
|
related_object=complaint,
|
|
metadata={
|
|
'notification_type': 'explanation_request_resent',
|
|
'staff_id': str(complaint.staff.id),
|
|
'explanation_id': str(explanation.id),
|
|
'requested_by_id': str(request.user.id),
|
|
'resent': True
|
|
}
|
|
)
|
|
except Exception as e:
|
|
return Response(
|
|
{'error': f'Failed to send email: {str(e)}'},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
# Create ComplaintUpdate entry
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='communication',
|
|
message=f"Explanation request resent to {recipient_display}",
|
|
created_by=request.user,
|
|
metadata={
|
|
'explanation_id': str(explanation.id),
|
|
'staff_id': str(complaint.staff.id),
|
|
'notification_log_id': str(notification_log.id) if notification_log else None,
|
|
'resent': True
|
|
}
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_from_request(
|
|
event_type='explanation_resent',
|
|
description=f"Explanation request resent to {recipient_display}",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata={
|
|
'explanation_id': str(explanation.id),
|
|
'staff_id': str(complaint.staff.id)
|
|
}
|
|
)
|
|
|
|
return Response({
|
|
'success': True,
|
|
'message': 'Explanation request resent successfully',
|
|
'explanation_id': str(explanation.id),
|
|
'recipient': recipient_display,
|
|
'new_token': new_token,
|
|
'explanation_link': explanation_link
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
|
|
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', 'source', 'hospital', 'department', 'assigned_to', 'hospital__organization']
|
|
search_fields = ['subject', 'message', 'contact_name', 'patient__mrn']
|
|
ordering_fields = ['created_at']
|
|
ordering = ['-created_at']
|
|
|
|
def perform_create(self, serializer):
|
|
"""Auto-set created_by from request.user"""
|
|
inquiry = serializer.save(created_by=self.request.user)
|
|
|
|
AuditService.log_from_request(
|
|
event_type='inquiry_created',
|
|
description=f"Inquiry created: {inquiry.subject}",
|
|
request=self.request,
|
|
content_object=inquiry,
|
|
metadata={
|
|
'created_by': str(inquiry.created_by.id) if inquiry.created_by else None
|
|
}
|
|
)
|
|
|
|
def get_queryset(self):
|
|
"""Filter inquiries based on user role"""
|
|
queryset = super().get_queryset().select_related(
|
|
'patient', 'hospital', 'department', 'assigned_to', 'responded_by', 'created_by'
|
|
)
|
|
|
|
user = self.request.user
|
|
|
|
# PX Admins see all inquiries
|
|
if user.is_px_admin():
|
|
return queryset
|
|
|
|
# Source Users see ONLY inquiries THEY created
|
|
if hasattr(user, 'source_user_profile') and user.source_user_profile.exists():
|
|
return queryset.filter(created_by=user)
|
|
|
|
# Patients see ONLY their own inquiries (if they have user accounts)
|
|
if hasattr(user, 'patient_profile'):
|
|
return queryset.filter(patient__user=user)
|
|
|
|
# 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'})
|
|
|
|
|
|
# Public views (no authentication required)
|
|
from django.shortcuts import render, redirect, get_object_or_404
|
|
from django.http import JsonResponse
|
|
|
|
|
|
def complaint_explanation_form(request, complaint_id, token):
|
|
"""
|
|
Public-facing form for staff to submit explanation.
|
|
|
|
This view does NOT require authentication.
|
|
Validates token and checks if it's still valid (not used).
|
|
"""
|
|
from .models import ComplaintExplanation, ExplanationAttachment
|
|
from apps.notifications.services import NotificationService
|
|
from django.contrib.sites.shortcuts import get_current_site
|
|
|
|
# Get complaint
|
|
complaint = get_object_or_404(Complaint, id=complaint_id)
|
|
|
|
# Validate token
|
|
explanation = get_object_or_404(ComplaintExplanation, complaint=complaint, token=token)
|
|
|
|
# Check if token is already used
|
|
if explanation.is_used:
|
|
return render(request, 'complaints/explanation_already_submitted.html', {
|
|
'complaint': complaint,
|
|
'explanation': explanation
|
|
})
|
|
|
|
if request.method == 'POST':
|
|
# Handle form submission
|
|
explanation_text = request.POST.get('explanation', '').strip()
|
|
|
|
if not explanation_text:
|
|
return render(request, 'complaints/explanation_form.html', {
|
|
'complaint': complaint,
|
|
'explanation': explanation,
|
|
'error': 'Please provide your explanation.'
|
|
})
|
|
|
|
# Save explanation
|
|
explanation.explanation = explanation_text
|
|
explanation.is_used = True
|
|
explanation.responded_at = timezone.now()
|
|
explanation.save()
|
|
|
|
# Handle file attachments
|
|
files = request.FILES.getlist('attachments')
|
|
for uploaded_file in files:
|
|
ExplanationAttachment.objects.create(
|
|
explanation=explanation,
|
|
file=uploaded_file,
|
|
filename=uploaded_file.name,
|
|
file_type=uploaded_file.content_type,
|
|
file_size=uploaded_file.size
|
|
)
|
|
|
|
# Notify complaint assignee
|
|
if complaint.assigned_to and complaint.assigned_to.email:
|
|
site = get_current_site(request)
|
|
complaint_url = f"https://{site.domain}/complaints/{complaint.id}/"
|
|
|
|
subject = f"New Explanation Received - Complaint #{complaint.id}"
|
|
|
|
email_body = f"""
|
|
Dear {complaint.assigned_to.get_full_name()},
|
|
|
|
A new explanation has been submitted for the following complaint:
|
|
|
|
COMPLAINT DETAILS:
|
|
----------------
|
|
Reference: #{complaint.id}
|
|
Title: {complaint.title}
|
|
Severity: {complaint.get_severity_display()}
|
|
|
|
EXPLANATION SUBMITTED BY:
|
|
------------------------
|
|
{explanation.staff}
|
|
|
|
EXPLANATION:
|
|
-----------
|
|
{explanation.explanation}
|
|
|
|
"""
|
|
if files:
|
|
email_body += f"""
|
|
ATTACHMENTS:
|
|
------------
|
|
{len(files)} file(s) attached
|
|
"""
|
|
|
|
email_body += f"""
|
|
|
|
To view the complaint and explanation, please visit:
|
|
{complaint_url}
|
|
|
|
---
|
|
This is an automated message from PX360 Complaint Management System.
|
|
"""
|
|
|
|
try:
|
|
NotificationService.send_email(
|
|
email=complaint.assigned_to.email,
|
|
subject=subject,
|
|
message=email_body,
|
|
related_object=complaint,
|
|
metadata={
|
|
'notification_type': 'explanation_submitted',
|
|
'explanation_id': str(explanation.id),
|
|
'staff_id': str(explanation.staff.id) if explanation.staff else None
|
|
}
|
|
)
|
|
except Exception as e:
|
|
# Log error but don't fail the submission
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f"Failed to send notification email: {e}")
|
|
|
|
# Create complaint update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='communication',
|
|
message=f"Explanation submitted by {explanation.staff}",
|
|
metadata={
|
|
'explanation_id': str(explanation.id),
|
|
'staff_id': str(explanation.staff.id) if explanation.staff else None
|
|
}
|
|
)
|
|
|
|
# Redirect to success page
|
|
return render(request, 'complaints/explanation_success.html', {
|
|
'complaint': complaint,
|
|
'explanation': explanation,
|
|
'attachment_count': len(files)
|
|
})
|
|
|
|
# GET request - display form
|
|
return render(request, 'complaints/explanation_form.html', {
|
|
'complaint': complaint,
|
|
'explanation': explanation
|
|
})
|
|
|
|
|
|
from django.http import HttpResponse
|
|
|
|
|
|
def generate_complaint_pdf(request, complaint_id):
|
|
"""
|
|
Generate PDF for a complaint using WeasyPrint.
|
|
|
|
Creates a professionally styled PDF document with all complaint details
|
|
including AI analysis, staff assignment, and resolution information.
|
|
"""
|
|
# Get complaint
|
|
complaint = get_object_or_404(Complaint, id=complaint_id)
|
|
|
|
# Check permissions
|
|
user = request.user
|
|
if not user.is_authenticated:
|
|
return HttpResponse('Unauthorized', status=401)
|
|
|
|
# Check if user can view this complaint
|
|
can_view = False
|
|
if user.is_px_admin():
|
|
can_view = True
|
|
elif user.is_hospital_admin() and user.hospital == complaint.hospital:
|
|
can_view = True
|
|
elif user.is_department_manager() and user.department == complaint.department:
|
|
can_view = True
|
|
elif user.hospital == complaint.hospital:
|
|
can_view = True
|
|
|
|
if not can_view:
|
|
return HttpResponse('Forbidden', status=403)
|
|
|
|
# Render HTML template
|
|
from django.template.loader import render_to_string
|
|
html_string = render_to_string('complaints/complaint_pdf.html', {
|
|
'complaint': complaint,
|
|
})
|
|
|
|
# Generate PDF using WeasyPrint
|
|
try:
|
|
from weasyprint import HTML
|
|
pdf_file = HTML(string=html_string).write_pdf()
|
|
|
|
# Create response
|
|
response = HttpResponse(pdf_file, content_type='application/pdf')
|
|
filename = f"complaint_{complaint.id.strftime('%Y%m%d_%H%M%S')}.pdf"
|
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
|
|
|
# Log audit
|
|
AuditService.log_from_request(
|
|
event_type='pdf_generated',
|
|
description=f"PDF generated for complaint: {complaint.title}",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata={'complaint_id': str(complaint.id)}
|
|
)
|
|
|
|
return response
|
|
except ImportError:
|
|
return HttpResponse('WeasyPrint is not installed. Please install it to generate PDFs.', status=500)
|
|
except Exception as e:
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
logger.error(f"Error generating PDF for complaint {complaint.id}: {e}")
|
|
return HttpResponse(f'Error generating PDF: {str(e)}', status=500)
|