432 lines
17 KiB
Python
432 lines
17 KiB
Python
"""
|
|
Complaints serializers
|
|
"""
|
|
from rest_framework import serializers
|
|
|
|
from .models import (
|
|
Complaint,
|
|
ComplaintAttachment,
|
|
ComplaintMeeting,
|
|
ComplaintPRInteraction,
|
|
ComplaintUpdate,
|
|
Inquiry,
|
|
ComplaintExplanation
|
|
)
|
|
|
|
|
|
class ComplaintAttachmentSerializer(serializers.ModelSerializer):
|
|
"""Complaint attachment serializer"""
|
|
uploaded_by_name = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = ComplaintAttachment
|
|
fields = [
|
|
'id', 'complaint', 'file', 'filename', 'file_type', 'file_size',
|
|
'uploaded_by', 'uploaded_by_name', 'description',
|
|
'created_at', 'updated_at'
|
|
]
|
|
read_only_fields = ['id', 'file_size', 'created_at', 'updated_at']
|
|
|
|
def get_uploaded_by_name(self, obj):
|
|
"""Get uploader name"""
|
|
if obj.uploaded_by:
|
|
return obj.uploaded_by.get_full_name()
|
|
return None
|
|
|
|
|
|
class ComplaintUpdateSerializer(serializers.ModelSerializer):
|
|
"""Complaint update serializer"""
|
|
created_by_name = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = ComplaintUpdate
|
|
fields = [
|
|
'id', 'complaint', 'update_type', 'message',
|
|
'created_by', 'created_by_name',
|
|
'old_status', 'new_status', 'metadata',
|
|
'created_at', 'updated_at'
|
|
]
|
|
read_only_fields = ['id', 'created_at', 'updated_at']
|
|
|
|
def get_created_by_name(self, obj):
|
|
"""Get creator name"""
|
|
if obj.created_by:
|
|
return obj.created_by.get_full_name()
|
|
return None
|
|
|
|
|
|
class ComplaintPRInteractionSerializer(serializers.ModelSerializer):
|
|
"""PR Interaction serializer"""
|
|
complaint_title = serializers.CharField(source='complaint.title', read_only=True)
|
|
pr_staff_name = serializers.SerializerMethodField()
|
|
created_by_name = serializers.SerializerMethodField()
|
|
contact_method_display = serializers.CharField(source='get_contact_method_display', read_only=True)
|
|
|
|
class Meta:
|
|
model = ComplaintPRInteraction
|
|
fields = [
|
|
'id', 'complaint', 'complaint_title',
|
|
'contact_date', 'contact_method', 'contact_method_display',
|
|
'pr_staff', 'pr_staff_name',
|
|
'statement_text', 'procedure_explained', 'notes',
|
|
'created_by', 'created_by_name',
|
|
'created_at', 'updated_at'
|
|
]
|
|
read_only_fields = ['id', 'created_at', 'updated_at']
|
|
|
|
def get_pr_staff_name(self, obj):
|
|
"""Get PR staff name"""
|
|
if obj.pr_staff:
|
|
return obj.pr_staff.get_full_name()
|
|
return None
|
|
|
|
def get_created_by_name(self, obj):
|
|
"""Get creator name"""
|
|
if obj.created_by:
|
|
return obj.created_by.get_full_name()
|
|
return None
|
|
|
|
|
|
class ComplaintMeetingSerializer(serializers.ModelSerializer):
|
|
"""Complaint Meeting serializer"""
|
|
complaint_title = serializers.CharField(source='complaint.title', read_only=True)
|
|
created_by_name = serializers.SerializerMethodField()
|
|
meeting_type_display = serializers.CharField(source='get_meeting_type_display', read_only=True)
|
|
|
|
class Meta:
|
|
model = ComplaintMeeting
|
|
fields = [
|
|
'id', 'complaint', 'complaint_title',
|
|
'meeting_date', 'meeting_type', 'meeting_type_display',
|
|
'outcome', 'notes',
|
|
'created_by', 'created_by_name',
|
|
'created_at', 'updated_at'
|
|
]
|
|
read_only_fields = ['id', 'created_at', 'updated_at']
|
|
|
|
def get_created_by_name(self, obj):
|
|
"""Get creator name"""
|
|
if obj.created_by:
|
|
return obj.created_by.get_full_name()
|
|
return None
|
|
|
|
|
|
class ComplaintSerializer(serializers.ModelSerializer):
|
|
"""Complaint serializer"""
|
|
patient_name = serializers.CharField(source='patient.get_full_name', read_only=True)
|
|
patient_mrn = serializers.CharField(source='patient.mrn', read_only=True)
|
|
hospital_name = serializers.CharField(source='hospital.name', read_only=True)
|
|
department_name = serializers.CharField(source='department.name', read_only=True)
|
|
staff_name = serializers.SerializerMethodField()
|
|
assigned_to_name = serializers.SerializerMethodField()
|
|
created_by_name = serializers.SerializerMethodField()
|
|
source_name = serializers.CharField(source='source.name_en', read_only=True)
|
|
source_code = serializers.CharField(source='source.code', read_only=True)
|
|
complaint_source_type_display = serializers.CharField(source='get_complaint_source_type_display', read_only=True)
|
|
complaint_type_display = serializers.CharField(source='get_complaint_type_display', read_only=True)
|
|
attachments = ComplaintAttachmentSerializer(many=True, read_only=True)
|
|
updates = ComplaintUpdateSerializer(many=True, read_only=True)
|
|
sla_status = serializers.SerializerMethodField()
|
|
|
|
# 4-level taxonomy fields
|
|
domain_details = serializers.SerializerMethodField()
|
|
category_details = serializers.SerializerMethodField()
|
|
subcategory_details = serializers.SerializerMethodField()
|
|
classification_details = serializers.SerializerMethodField()
|
|
taxonomy_path = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Complaint
|
|
fields = [
|
|
'id', 'patient', 'patient_name', 'patient_mrn', 'encounter_id',
|
|
'hospital', 'hospital_name', 'department', 'department_name',
|
|
'staff', 'staff_name',
|
|
'title', 'description',
|
|
# Reference and tracking
|
|
'reference_number',
|
|
# 4-level taxonomy
|
|
'domain', 'domain_details',
|
|
'category', 'category_details',
|
|
'subcategory', 'subcategory_details',
|
|
'subcategory_obj', 'classification', 'classification_obj', 'classification_details',
|
|
'taxonomy_path',
|
|
'priority', 'severity', 'complaint_type', 'complaint_type_display',
|
|
'complaint_source_type', 'complaint_source_type_display',
|
|
'source', 'source_name', 'source_code', 'status',
|
|
'created_by', 'created_by_name',
|
|
'assigned_to', 'assigned_to_name', 'assigned_at',
|
|
'due_at', 'is_overdue', 'sla_status',
|
|
'reminder_sent_at', 'escalated_at',
|
|
'resolution', 'resolved_at', 'resolved_by',
|
|
'closed_at', 'closed_by',
|
|
'resolution_survey', 'resolution_survey_sent_at',
|
|
'attachments', 'updates', 'metadata',
|
|
'created_at', 'updated_at'
|
|
]
|
|
read_only_fields = [
|
|
'id', 'created_by', 'assigned_at', 'is_overdue',
|
|
'reference_number',
|
|
'reminder_sent_at', 'escalated_at',
|
|
'resolved_at', 'closed_at', 'resolution_survey_sent_at',
|
|
'created_at', 'updated_at'
|
|
]
|
|
|
|
def get_domain_details(self, obj):
|
|
"""Get domain details"""
|
|
if obj.domain:
|
|
return {
|
|
'id': str(obj.domain.id),
|
|
'code': obj.domain.code or obj.domain.name_en.upper(),
|
|
'name_en': obj.domain.name_en,
|
|
'name_ar': obj.domain.name_ar
|
|
}
|
|
return None
|
|
|
|
def get_category_details(self, obj):
|
|
"""Get category details"""
|
|
if obj.category:
|
|
return {
|
|
'id': str(obj.category.id),
|
|
'code': obj.category.code or obj.category.name_en.upper(),
|
|
'name_en': obj.category.name_en,
|
|
'name_ar': obj.category.name_ar
|
|
}
|
|
return None
|
|
|
|
def get_subcategory_details(self, obj):
|
|
"""Get subcategory details"""
|
|
if obj.subcategory_obj:
|
|
return {
|
|
'id': str(obj.subcategory_obj.id),
|
|
'code': obj.subcategory_obj.code or obj.subcategory_obj.name_en.upper(),
|
|
'name_en': obj.subcategory_obj.name_en,
|
|
'name_ar': obj.subcategory_obj.name_ar
|
|
}
|
|
return None
|
|
|
|
def get_classification_details(self, obj):
|
|
"""Get classification details"""
|
|
if obj.classification_obj:
|
|
return {
|
|
'id': str(obj.classification_obj.id),
|
|
'code': obj.classification_obj.code,
|
|
'name_en': obj.classification_obj.name_en,
|
|
'name_ar': obj.classification_obj.name_ar
|
|
}
|
|
return None
|
|
|
|
def get_taxonomy_path(self, obj):
|
|
"""Get full taxonomy path as a string"""
|
|
parts = []
|
|
if obj.domain:
|
|
parts.append(obj.domain.name_en)
|
|
if obj.category:
|
|
parts.append(obj.category.name_en)
|
|
if obj.subcategory_obj:
|
|
parts.append(obj.subcategory_obj.name_en)
|
|
if obj.classification_obj:
|
|
parts.append(obj.classification_obj.name_en)
|
|
return ' > '.join(parts) if parts else None
|
|
|
|
def create(self, validated_data):
|
|
"""
|
|
Create complaint with auto-setting of department from AI analysis.
|
|
|
|
If metadata contains AI analysis with department name,
|
|
automatically set the department field by querying the database.
|
|
"""
|
|
from apps.organizations.models import Department
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Extract metadata if present
|
|
metadata = validated_data.get('metadata', {})
|
|
|
|
# Check if department is not already set and AI analysis contains department info
|
|
if not validated_data.get('department') and metadata.get('ai_analysis'):
|
|
ai_analysis = metadata['ai_analysis']
|
|
department_name = ai_analysis.get('department')
|
|
|
|
# If AI identified a department, try to match it in the database
|
|
if department_name:
|
|
hospital = validated_data.get('hospital')
|
|
|
|
if hospital:
|
|
try:
|
|
# Try exact match first
|
|
department = Department.objects.filter(
|
|
hospital=hospital,
|
|
status='active',
|
|
name__iexact=department_name
|
|
).first()
|
|
|
|
# If no exact match, try partial match
|
|
if not department:
|
|
department = Department.objects.filter(
|
|
hospital=hospital,
|
|
status='active',
|
|
name__icontains=department_name
|
|
).first()
|
|
|
|
if department:
|
|
validated_data['department'] = department
|
|
logger.info(
|
|
f"Auto-set department '{department.name}' from AI analysis "
|
|
f"for complaint (matched from '{department_name}')"
|
|
)
|
|
else:
|
|
logger.warning(
|
|
f"AI suggested department '{department_name}' but no match found "
|
|
f"in hospital '{hospital.name}'"
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error auto-setting department: {e}")
|
|
|
|
# Create the complaint
|
|
return super().create(validated_data)
|
|
|
|
def get_staff_name(self, obj):
|
|
"""Get staff name"""
|
|
if obj.staff:
|
|
return f"{obj.staff.first_name} {obj.staff.last_name}"
|
|
return None
|
|
|
|
def get_assigned_to_name(self, obj):
|
|
"""Get assigned user name"""
|
|
if obj.assigned_to:
|
|
return obj.assigned_to.get_full_name()
|
|
return None
|
|
|
|
def get_created_by_name(self, obj):
|
|
"""Get creator name"""
|
|
if obj.created_by:
|
|
return obj.created_by.get_full_name()
|
|
return None
|
|
|
|
def get_sla_status(self, obj):
|
|
"""Get SLA status"""
|
|
return obj.sla_status if hasattr(obj, 'sla_status') else 'on_track'
|
|
|
|
|
|
class ComplaintExplanationSerializer(serializers.ModelSerializer):
|
|
"""Complaint explanation serializer"""
|
|
staff_name = serializers.SerializerMethodField()
|
|
requested_by_name = serializers.SerializerMethodField()
|
|
attachment_count = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = ComplaintExplanation
|
|
fields = [
|
|
'id', 'complaint', 'staff', 'staff_name',
|
|
'explanation', 'token', 'is_used',
|
|
'email_sent_at', 'responded_at',
|
|
'submitted_via', 'requested_by', 'requested_by_name',
|
|
'request_message', 'attachment_count',
|
|
'created_at'
|
|
]
|
|
read_only_fields = ['id', 'email_sent_at', 'responded_at', 'created_at']
|
|
|
|
def get_staff_name(self, obj):
|
|
if obj.staff:
|
|
return f"{obj.staff.first_name} {obj.staff.last_name}" if obj.staff.last_name else ""
|
|
return ""
|
|
|
|
def get_requested_by_name(self, obj):
|
|
if obj.requested_by:
|
|
return obj.requested_by.get_full_name()
|
|
return None
|
|
|
|
def get_attachment_count(self, obj):
|
|
return obj.attachments.count()
|
|
|
|
|
|
class ComplaintListSerializer(serializers.ModelSerializer):
|
|
"""Simplified complaint serializer for list views"""
|
|
patient_name = serializers.CharField(source='patient.get_full_name', read_only=True)
|
|
patient_mrn = serializers.CharField(source='patient.mrn', read_only=True)
|
|
hospital_name = serializers.CharField(source='hospital.name', read_only=True)
|
|
department_name = serializers.CharField(source='department.name', read_only=True)
|
|
staff_name = serializers.SerializerMethodField()
|
|
assigned_to_name = serializers.SerializerMethodField()
|
|
source_name = serializers.CharField(source='source.name_en', read_only=True)
|
|
complaint_source_type_display = serializers.CharField(source='get_complaint_source_type_display', read_only=True)
|
|
complaint_type_display = serializers.CharField(source='get_complaint_type_display', read_only=True)
|
|
sla_status = serializers.SerializerMethodField()
|
|
taxonomy_summary = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Complaint
|
|
fields = [
|
|
'id', 'reference_number', 'patient_name', 'patient_mrn', 'encounter_id',
|
|
'hospital_name', 'department_name', 'staff_name',
|
|
'title', 'category', 'subcategory', 'taxonomy_summary',
|
|
'priority', 'severity', 'complaint_type', 'complaint_type_display',
|
|
'complaint_source_type', 'complaint_source_type_display',
|
|
'source_name', 'status',
|
|
'assigned_to_name', 'assigned_at',
|
|
'due_at', 'is_overdue', 'sla_status',
|
|
'resolution', 'resolved_at',
|
|
'closed_at',
|
|
'created_at', 'updated_at'
|
|
]
|
|
|
|
def get_staff_name(self, obj):
|
|
"""Get staff name"""
|
|
if obj.staff:
|
|
return f"{obj.staff.first_name} {obj.staff.last_name}"
|
|
return None
|
|
|
|
def get_assigned_to_name(self, obj):
|
|
"""Get assigned user name"""
|
|
if obj.assigned_to:
|
|
return obj.assigned_to.get_full_name()
|
|
return None
|
|
|
|
def get_sla_status(self, obj):
|
|
"""Get SLA status"""
|
|
return obj.sla_status if hasattr(obj, 'sla_status') else 'on_track'
|
|
|
|
def get_taxonomy_summary(self, obj):
|
|
"""Get brief taxonomy summary"""
|
|
parts = []
|
|
if obj.domain:
|
|
parts.append(obj.domain.name_en)
|
|
if obj.category:
|
|
parts.append(obj.category.name_en)
|
|
return ' > '.join(parts) if parts else None
|
|
|
|
|
|
class InquirySerializer(serializers.ModelSerializer):
|
|
"""Inquiry serializer"""
|
|
patient_name = serializers.CharField(source='patient.get_full_name', read_only=True)
|
|
hospital_name = serializers.CharField(source='hospital.name', read_only=True)
|
|
department_name = serializers.CharField(source='department.name', read_only=True)
|
|
assigned_to_name = serializers.SerializerMethodField()
|
|
created_by_name = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Inquiry
|
|
fields = [
|
|
'id', 'patient', 'patient_name',
|
|
'contact_name', 'contact_phone', 'contact_email',
|
|
'hospital', 'hospital_name', 'department', 'department_name',
|
|
'subject', 'message', 'category', 'source',
|
|
'created_by', 'created_by_name',
|
|
'assigned_to', 'assigned_to_name',
|
|
'response', 'responded_at', 'responded_by',
|
|
'created_at', 'updated_at'
|
|
]
|
|
read_only_fields = ['id', 'created_by', 'responded_at', 'created_at', 'updated_at']
|
|
|
|
def get_assigned_to_name(self, obj):
|
|
"""Get assigned user name"""
|
|
if obj.assigned_to:
|
|
return obj.assigned_to.get_full_name()
|
|
return None
|
|
|
|
def get_created_by_name(self, obj):
|
|
"""Get creator name"""
|
|
if obj.created_by:
|
|
return obj.created_by.get_full_name()
|
|
return None
|