557 lines
18 KiB
Python
557 lines
18 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",
|
|
"activated_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",
|
|
"ai_brief_en",
|
|
"ai_brief_ar",
|
|
"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
|