777 lines
29 KiB
Python
777 lines
29 KiB
Python
import logging
|
|
|
|
from django.utils import timezone
|
|
|
|
from apps.core.services import AuditService
|
|
from apps.notifications.services import NotificationService
|
|
from apps.organizations.models import Department
|
|
from apps.complaints.models import Complaint, ComplaintExplanation, ComplaintStatus, ComplaintUpdate
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ComplaintServiceError(Exception):
|
|
pass
|
|
|
|
|
|
class ComplaintService:
|
|
@staticmethod
|
|
def get_escalation_target(complaint, staff=None):
|
|
"""
|
|
Resolve an escalation target using a fallback chain.
|
|
|
|
For explanation escalation (staff provided):
|
|
staff.report_to -> staff.department.manager ->
|
|
complaint.department.manager -> hospital admins & PX staff
|
|
|
|
For complaint-level escalation (no staff):
|
|
complaint.department.manager -> hospital admins & PX staff
|
|
|
|
Args:
|
|
complaint: Complaint instance
|
|
staff: Optional Staff instance (the person being escalated from)
|
|
|
|
Returns:
|
|
tuple: (target_user, fallback_path) where target_user is a User
|
|
(or None if no target found) and fallback_path describes
|
|
which step succeeded.
|
|
"""
|
|
from apps.complaints.tasks import get_hospital_admins_and_staff
|
|
|
|
if staff:
|
|
if staff.report_to and staff.report_to.user and staff.report_to.user.is_active:
|
|
return staff.report_to.user, "staff.report_to"
|
|
|
|
staff_dept = getattr(staff, "department", None)
|
|
if staff_dept and staff_dept.manager and staff_dept.manager.is_active:
|
|
return staff_dept.manager, "staff.department.manager"
|
|
|
|
if complaint.department and complaint.department.manager and complaint.department.manager.is_active:
|
|
if not staff or (staff and getattr(staff, "department", None) != complaint.department):
|
|
return complaint.department.manager, "complaint.department.manager"
|
|
|
|
hospital = complaint.hospital
|
|
if hospital:
|
|
fallback = get_hospital_admins_and_staff(hospital).first()
|
|
if fallback:
|
|
return fallback, "hospital_admins_staff"
|
|
|
|
return None, "no_target_found"
|
|
|
|
@staticmethod
|
|
def can_manage(user, complaint):
|
|
if user.is_px_admin():
|
|
return True
|
|
if user.is_hospital_admin() and user.hospital == complaint.hospital:
|
|
return True
|
|
if user.is_px_management() and user.hospital == complaint.hospital:
|
|
return True
|
|
if user.is_px_employee() and user.hospital == complaint.hospital:
|
|
return True
|
|
if user.is_department_manager() and user.department == complaint.department:
|
|
return True
|
|
if complaint.assigned_to and complaint.assigned_to == user:
|
|
return True
|
|
if complaint.involved_departments.filter(id=user.department_id).exists() if user.department_id else False:
|
|
return True
|
|
return False
|
|
|
|
@staticmethod
|
|
def can_activate(user, complaint):
|
|
return (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or user.is_px_management()
|
|
or (user.is_department_manager() and complaint.department == user.department)
|
|
or complaint.hospital == user.hospital
|
|
)
|
|
|
|
@staticmethod
|
|
def activate(complaint, user, request=None):
|
|
if not complaint.is_active_status:
|
|
raise ComplaintServiceError(
|
|
f"Cannot activate complaint with status '{complaint.get_status_display()}'. "
|
|
"Complaint must be Open, In Progress, or Partially Resolved."
|
|
)
|
|
|
|
if not ComplaintService.can_activate(user, complaint):
|
|
raise ComplaintServiceError("You don't have permission to activate this complaint.")
|
|
|
|
if complaint.assigned_to == user:
|
|
raise ComplaintServiceError("This complaint is already assigned to you.")
|
|
|
|
previous_assignee = complaint.assigned_to
|
|
old_status = complaint.status
|
|
|
|
complaint.assigned_to = user
|
|
complaint.assigned_at = timezone.now()
|
|
|
|
if complaint.status == ComplaintStatus.OPEN:
|
|
complaint.status = ComplaintStatus.IN_PROGRESS
|
|
complaint.activated_at = timezone.now()
|
|
complaint.save(update_fields=["assigned_to", "assigned_at", "status", "activated_at"])
|
|
else:
|
|
complaint.save(update_fields=["assigned_to", "assigned_at"])
|
|
|
|
assign_message = f"Complaint activated and assigned to {user.get_full_name()}"
|
|
if previous_assignee:
|
|
assign_message += f" (reassigned from {previous_assignee.get_full_name()})"
|
|
|
|
roles_display = ", ".join(user.get_role_names())
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="assignment",
|
|
message=f"{assign_message} ({roles_display})",
|
|
created_by=user,
|
|
metadata={
|
|
"old_assignee_id": str(previous_assignee.id) if previous_assignee else None,
|
|
"new_assignee_id": str(user.id),
|
|
"assignee_roles": user.get_role_names(),
|
|
"old_status": old_status,
|
|
"new_status": complaint.status,
|
|
"activated_by_current_user": True,
|
|
},
|
|
)
|
|
|
|
metadata = {
|
|
"old_assignee_id": str(previous_assignee.id) if previous_assignee else None,
|
|
"new_assignee_id": str(user.id),
|
|
"old_status": old_status,
|
|
"new_status": complaint.status,
|
|
}
|
|
|
|
if request:
|
|
AuditService.log_from_request(
|
|
event_type="complaint_activated",
|
|
description=f"Complaint activated by {user.get_full_name()}",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
else:
|
|
AuditService.log_event(
|
|
event_type="complaint_activated",
|
|
description=f"Complaint activated by {user.get_full_name()}",
|
|
user=user,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"complaint": complaint,
|
|
"old_assignee": previous_assignee,
|
|
"old_status": old_status,
|
|
}
|
|
|
|
@staticmethod
|
|
def assign(complaint, target_user, assigned_by, request=None):
|
|
if not complaint.is_active_status and complaint.status not in (
|
|
ComplaintStatus.RESOLVED,
|
|
ComplaintStatus.CLOSED,
|
|
ComplaintStatus.CANCELLED,
|
|
):
|
|
raise ComplaintServiceError(
|
|
f"Cannot assign complaint with status '{complaint.get_status_display()}'. "
|
|
"Complaint must be in an active or terminal status."
|
|
)
|
|
|
|
if not (
|
|
assigned_by.is_px_admin()
|
|
or assigned_by.is_hospital_admin()
|
|
or complaint.assigned_to == assigned_by
|
|
):
|
|
raise ComplaintServiceError("You don't have permission to assign complaints.")
|
|
|
|
old_assignee = complaint.assigned_to
|
|
old_status = complaint.status
|
|
|
|
complaint.assigned_to = target_user
|
|
complaint.assigned_at = timezone.now()
|
|
|
|
reopened = False
|
|
if old_status in (ComplaintStatus.RESOLVED, ComplaintStatus.CLOSED, ComplaintStatus.CANCELLED):
|
|
complaint.status = ComplaintStatus.IN_PROGRESS
|
|
complaint.resolved_at = None
|
|
complaint.resolved_by = None
|
|
complaint.closed_at = None
|
|
complaint.closed_by = None
|
|
complaint.reopened_at = timezone.now()
|
|
complaint.reopened_by = assigned_by
|
|
reopened = True
|
|
complaint.save(update_fields=["assigned_to", "assigned_at", "status", "resolved_at", "resolved_by", "closed_at", "closed_by", "reopened_at", "reopened_by"])
|
|
else:
|
|
complaint.save(update_fields=["assigned_to", "assigned_at"])
|
|
|
|
roles_display = ", ".join(target_user.get_role_names())
|
|
|
|
msg = f"Assigned to {target_user.get_full_name()} ({roles_display})"
|
|
if old_assignee:
|
|
msg += f" (reassigned from {old_assignee.get_full_name()})"
|
|
if reopened:
|
|
msg = f"Reopened and {msg}"
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="assignment",
|
|
message=msg,
|
|
created_by=assigned_by,
|
|
metadata={
|
|
"old_assignee_id": str(old_assignee.id) if old_assignee else None,
|
|
"new_assignee_id": str(target_user.id),
|
|
"assignee_roles": target_user.get_role_names(),
|
|
"reopened": reopened,
|
|
"old_status": old_status,
|
|
},
|
|
)
|
|
|
|
metadata = {
|
|
"old_assignee_id": str(old_assignee.id) if old_assignee else None,
|
|
"new_assignee_id": str(target_user.id),
|
|
}
|
|
|
|
if request:
|
|
AuditService.log_from_request(
|
|
event_type="assignment",
|
|
description=f"Complaint assigned to {target_user.get_full_name()} ({roles_display})",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
else:
|
|
AuditService.log_event(
|
|
event_type="assignment",
|
|
description=f"Complaint assigned to {target_user.get_full_name()}",
|
|
user=assigned_by,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"complaint": complaint,
|
|
"old_assignee": old_assignee,
|
|
}
|
|
|
|
# Valid status transitions for lifecycle enforcement
|
|
VALID_STATUS_TRANSITIONS = {
|
|
"open": ["in_progress", "cancelled", "contacted", "contacted_no_response"],
|
|
"in_progress": ["partially_resolved", "resolved", "cancelled", "contacted", "contacted_no_response", "pending_external"],
|
|
"partially_resolved": ["resolved", "in_progress", "cancelled", "pending_external"],
|
|
"resolved": ["closed", "in_progress"],
|
|
"closed": ["in_progress"],
|
|
"cancelled": ["open", "in_progress"],
|
|
"contacted": ["open", "in_progress", "contacted_no_response", "cancelled", "pending_external"],
|
|
"contacted_no_response": ["open", "in_progress", "cancelled", "pending_external"],
|
|
"pending_external": ["resolved", "in_progress", "cancelled", "closed"],
|
|
}
|
|
|
|
@staticmethod
|
|
def reopen(complaint, reopened_by, request=None, *, note=""):
|
|
if complaint.status not in (
|
|
ComplaintStatus.RESOLVED,
|
|
ComplaintStatus.CLOSED,
|
|
ComplaintStatus.CANCELLED,
|
|
):
|
|
raise ComplaintServiceError("Only resolved, closed, or cancelled complaints can be reopened.")
|
|
|
|
old_status = complaint.status
|
|
|
|
new_complaint = Complaint.objects.create(
|
|
patient=complaint.patient,
|
|
contact_name=complaint.contact_name,
|
|
contact_phone=complaint.contact_phone,
|
|
contact_email=complaint.contact_email,
|
|
hospital=complaint.hospital,
|
|
department=complaint.department,
|
|
staff=complaint.staff,
|
|
title=complaint.title,
|
|
description=complaint.description,
|
|
domain=complaint.domain,
|
|
category=complaint.category,
|
|
subcategory=complaint.subcategory,
|
|
classification=complaint.classification,
|
|
subcategory_obj=complaint.subcategory_obj,
|
|
classification_obj=complaint.classification_obj,
|
|
location=complaint.location,
|
|
main_section=complaint.main_section,
|
|
subsection=complaint.subsection,
|
|
complaint_type=complaint.complaint_type,
|
|
complaint_source_type=complaint.complaint_source_type,
|
|
priority=complaint.priority,
|
|
severity=complaint.severity,
|
|
source=complaint.source,
|
|
status=ComplaintStatus.OPEN,
|
|
reopened_from=complaint,
|
|
reopened_at=timezone.now(),
|
|
reopened_by=reopened_by,
|
|
)
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="status_change",
|
|
message=note or f"Complaint reopened as new complaint #{new_complaint.reference_number or new_complaint.id}",
|
|
created_by=reopened_by,
|
|
old_status=old_status,
|
|
new_status=old_status,
|
|
metadata={"reopened_as_new": True, "new_complaint_id": str(new_complaint.id)},
|
|
)
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=new_complaint,
|
|
update_type="status_change",
|
|
message=f"Created as reopen of complaint #{complaint.reference_number or complaint.id}",
|
|
created_by=reopened_by,
|
|
old_status="",
|
|
new_status=ComplaintStatus.OPEN,
|
|
metadata={"reopened_from": str(complaint.id)},
|
|
)
|
|
|
|
metadata = {
|
|
"old_status": old_status,
|
|
"new_complaint_id": str(new_complaint.id),
|
|
"original_complaint_id": str(complaint.id),
|
|
}
|
|
if request:
|
|
AuditService.log_from_request(
|
|
event_type="complaint_reopened",
|
|
description=f"Complaint #{complaint.reference_number or complaint.id} reopened as new complaint by {reopened_by.get_full_name()}",
|
|
request=request,
|
|
content_object=new_complaint,
|
|
metadata=metadata,
|
|
)
|
|
else:
|
|
AuditService.log_event(
|
|
event_type="complaint_reopened",
|
|
description=f"Complaint #{complaint.reference_number or complaint.id} reopened as new complaint by {reopened_by.get_full_name()}",
|
|
user=reopened_by,
|
|
content_object=new_complaint,
|
|
metadata=metadata,
|
|
)
|
|
|
|
return {"success": True, "complaint": new_complaint, "old_status": old_status, "original_complaint": complaint}
|
|
|
|
@staticmethod
|
|
def change_status(
|
|
complaint,
|
|
new_status,
|
|
changed_by,
|
|
request=None,
|
|
*,
|
|
note="",
|
|
resolution="",
|
|
resolution_outcome="",
|
|
resolution_outcome_other="",
|
|
resolution_category="",
|
|
):
|
|
if not (changed_by.is_px_admin() or changed_by.is_hospital_admin()):
|
|
raise ComplaintServiceError("You don't have permission to change complaint status.")
|
|
|
|
if not new_status:
|
|
raise ComplaintServiceError("Please select a status.")
|
|
|
|
old_status = complaint.status
|
|
|
|
# Enforce valid status transitions
|
|
valid_next = ComplaintService.VALID_STATUS_TRANSITIONS.get(old_status, [])
|
|
if new_status not in valid_next and not changed_by.is_px_admin():
|
|
raise ComplaintServiceError(
|
|
f"Invalid status transition from '{old_status}' to '{new_status}'. "
|
|
f"Allowed transitions: {', '.join(valid_next)}"
|
|
)
|
|
complaint.status = new_status
|
|
|
|
if new_status == ComplaintStatus.RESOLVED or new_status == "resolved":
|
|
if complaint.was_pending_external and complaint.pending_external_set_at:
|
|
complaint.resolved_at = complaint.pending_external_set_at
|
|
else:
|
|
complaint.resolved_at = timezone.now()
|
|
complaint.resolved_by = changed_by
|
|
if resolution:
|
|
complaint.resolution = resolution
|
|
complaint.resolution_sent_at = timezone.now()
|
|
if resolution_category:
|
|
complaint.resolution_category = resolution_category
|
|
if resolution_outcome:
|
|
complaint.resolution_outcome = resolution_outcome
|
|
if resolution_outcome == "other" and resolution_outcome_other:
|
|
complaint.resolution_outcome_other = resolution_outcome_other
|
|
|
|
elif new_status == ComplaintStatus.CLOSED or new_status == "closed":
|
|
complaint.closed_at = timezone.now()
|
|
complaint.closed_by = changed_by
|
|
from apps.complaints.tasks import send_complaint_resolution_survey
|
|
|
|
send_complaint_resolution_survey.delay(str(complaint.id))
|
|
|
|
elif new_status == ComplaintStatus.PENDING_EXTERNAL or new_status == "pending_external":
|
|
complaint.pending_external_set_at = timezone.now()
|
|
complaint.was_pending_external = True
|
|
|
|
complaint.save()
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="status_change",
|
|
message=note or f"Status changed from {old_status} to {new_status}",
|
|
created_by=changed_by,
|
|
old_status=old_status,
|
|
new_status=new_status,
|
|
metadata={
|
|
"resolution_text": resolution if resolution else None,
|
|
"resolution_category": resolution_category if resolution_category else None,
|
|
},
|
|
)
|
|
|
|
metadata = {
|
|
"old_status": old_status,
|
|
"new_status": new_status,
|
|
"resolution_category": resolution_category if resolution_category else None,
|
|
}
|
|
|
|
if request:
|
|
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=metadata,
|
|
)
|
|
else:
|
|
AuditService.log_event(
|
|
event_type="status_change",
|
|
description=f"Complaint status changed from {old_status} to {new_status}",
|
|
user=changed_by,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"complaint": complaint,
|
|
"old_status": old_status,
|
|
"new_status": new_status,
|
|
}
|
|
|
|
@staticmethod
|
|
def add_note(complaint, message, created_by, request=None):
|
|
if not complaint.is_active_status:
|
|
raise ComplaintServiceError(
|
|
f"Cannot add notes to complaint with status '{complaint.get_status_display()}'. "
|
|
"Complaint must be Open, In Progress, or Partially Resolved."
|
|
)
|
|
|
|
if not message:
|
|
raise ComplaintServiceError("Please enter a note.")
|
|
|
|
update = ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="note",
|
|
message=message,
|
|
created_by=created_by,
|
|
)
|
|
|
|
metadata = {
|
|
"note_id": str(update.id),
|
|
}
|
|
|
|
if request:
|
|
AuditService.log_from_request(
|
|
event_type="note_added",
|
|
description=f"Note added to complaint",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
else:
|
|
AuditService.log_event(
|
|
event_type="note_added",
|
|
description=f"Note added to complaint",
|
|
user=created_by,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
|
|
return update
|
|
|
|
@staticmethod
|
|
def change_department(complaint, department, changed_by, request=None):
|
|
if not complaint.is_active_status:
|
|
raise ComplaintServiceError(
|
|
f"Cannot change department for complaint with status '{complaint.get_status_display()}'. "
|
|
"Complaint must be Open, In Progress, or Partially Resolved."
|
|
)
|
|
|
|
if not (changed_by.is_px_admin() or changed_by.is_hospital_admin()):
|
|
raise ComplaintServiceError("You don't have permission to change complaint department.")
|
|
|
|
if department.hospital != complaint.hospital:
|
|
raise ComplaintServiceError("Department does not belong to this complaint's hospital.")
|
|
|
|
old_department = complaint.department
|
|
complaint.department = department
|
|
complaint.save(update_fields=["department"])
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="assignment",
|
|
message=f"Department changed to {department.name}",
|
|
created_by=changed_by,
|
|
metadata={
|
|
"old_department_id": str(old_department.id) if old_department else None,
|
|
"new_department_id": str(department.id),
|
|
},
|
|
)
|
|
|
|
metadata = {
|
|
"old_department_id": str(old_department.id) if old_department else None,
|
|
"new_department_id": str(department.id),
|
|
}
|
|
|
|
if request:
|
|
AuditService.log_from_request(
|
|
event_type="department_change",
|
|
description=f"Complaint department changed to {department.name}",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
else:
|
|
AuditService.log_event(
|
|
event_type="department_change",
|
|
description=f"Complaint department changed to {department.name}",
|
|
user=changed_by,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
|
|
return {
|
|
"success": True,
|
|
"complaint": complaint,
|
|
"old_department": old_department,
|
|
}
|
|
|
|
@staticmethod
|
|
def send_to_department(
|
|
complaint,
|
|
department_groups,
|
|
selected_dept_ids,
|
|
request_message,
|
|
requested_by,
|
|
domain,
|
|
request=None,
|
|
):
|
|
import secrets
|
|
|
|
if not complaint.is_active_status:
|
|
raise ComplaintServiceError(
|
|
f"Cannot send complaint to department with status '{complaint.get_status_display()}'. "
|
|
"Complaint must be Open, In Progress, or Partially Resolved."
|
|
)
|
|
|
|
champion_count = 0
|
|
skipped_no_email = 0
|
|
results = []
|
|
|
|
for dept_id, dept_info in department_groups.items():
|
|
if dept_id not in selected_dept_ids:
|
|
continue
|
|
|
|
champion = dept_info.get("champion")
|
|
champion_email = dept_info.get("champion_email")
|
|
|
|
if not champion or not champion_email:
|
|
skipped_no_email += 1
|
|
continue
|
|
|
|
staff_names = [s["staff_name"] for s in dept_info["staff_list"]]
|
|
|
|
champion_token = secrets.token_urlsafe(32)
|
|
|
|
explanation, created = ComplaintExplanation.objects.update_or_create(
|
|
complaint=complaint,
|
|
staff=champion,
|
|
defaults={
|
|
"token": champion_token,
|
|
"is_used": False,
|
|
"requested_by": requested_by,
|
|
"request_message": request_message,
|
|
"email_sent_at": timezone.now(),
|
|
"submitted_via": "email_link",
|
|
},
|
|
)
|
|
|
|
champion_link = f"https://{domain}/complaints/{complaint.id}/explain/{champion_token}/"
|
|
champion_subject = f"Explanation Request - Complaint #{complaint.reference_number}"
|
|
champion_display = dept_info.get("champion_name", str(champion))
|
|
|
|
staff_list_text = "\n".join(f" - {n}" for n in staff_names)
|
|
|
|
champion_email_body = f"""Dear {champion_display},
|
|
|
|
As the department champion for {dept_info['department_name']}, we are requesting your assistance in gathering explanations for a complaint involving staff from your department.
|
|
|
|
INVOLVED STAFF FROM YOUR DEPARTMENT:
|
|
-----------------------------------
|
|
{staff_list_text}
|
|
|
|
COMPLAINT DETAILS:
|
|
----------------
|
|
Reference: {complaint.reference_number}
|
|
Title: {complaint.title}
|
|
Severity: {complaint.get_severity_display()}
|
|
Priority: {complaint.get_priority_display()}
|
|
|
|
{complaint.description or "No description provided."}"""
|
|
|
|
if complaint.patient:
|
|
champion_email_body += f"""
|
|
|
|
PATIENT INFORMATION:
|
|
------------------
|
|
Name: {complaint.patient.get_full_name()}
|
|
MRN: {complaint.patient.mrn or "N/A"}"""
|
|
|
|
if request_message:
|
|
champion_email_body += f"""
|
|
|
|
ADDITIONAL MESSAGE:
|
|
------------------
|
|
{request_message}"""
|
|
|
|
champion_email_body += f"""
|
|
|
|
SUBMIT EXPLANATION:
|
|
------------------
|
|
Please coordinate with the involved staff and submit the explanation:
|
|
{champion_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."""
|
|
|
|
try:
|
|
NotificationService.send_email(
|
|
email=champion_email,
|
|
subject=champion_subject,
|
|
message=champion_email_body,
|
|
related_object=complaint,
|
|
metadata={
|
|
"notification_type": "explanation_request",
|
|
"staff_id": str(champion.id),
|
|
"complaint_id": str(complaint.id),
|
|
"department_id": dept_id,
|
|
},
|
|
)
|
|
champion_count += 1
|
|
results.append(
|
|
{
|
|
"recipient_type": "champion",
|
|
"recipient": champion_display,
|
|
"email": champion_email,
|
|
"department": dept_info["department_name"],
|
|
"explanation_id": str(explanation.id),
|
|
"sent": True,
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to send explanation request to champion {champion.id}: {e}")
|
|
results.append(
|
|
{
|
|
"recipient_type": "champion",
|
|
"recipient": champion_display,
|
|
"email": champion_email,
|
|
"department": dept_info["department_name"],
|
|
"explanation_id": str(explanation.id),
|
|
"sent": False,
|
|
"error": str(e),
|
|
}
|
|
)
|
|
|
|
metadata = {
|
|
"champion_count": champion_count,
|
|
"skipped_no_email": skipped_no_email,
|
|
"selected_dept_ids": selected_dept_ids,
|
|
}
|
|
|
|
if request:
|
|
AuditService.log_from_request(
|
|
event_type="explanation_requested",
|
|
description=f"Explanation requests sent to {champion_count} department champions",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
else:
|
|
AuditService.log_event(
|
|
event_type="explanation_requested",
|
|
description=f"Explanation requests sent to {champion_count} department champions",
|
|
user=requested_by,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
|
|
if champion_count > 0:
|
|
recipients_str = ", ".join([r["recipient"] for r in results if r["sent"]])
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="communication",
|
|
message=f"Explanation request sent to department champions: {recipients_str}",
|
|
created_by=requested_by,
|
|
metadata={
|
|
"champion_count": champion_count,
|
|
"results": results,
|
|
},
|
|
)
|
|
complaint.status = ComplaintStatus.CONTACTED
|
|
complaint.save(update_fields=["status", "updated_at"])
|
|
|
|
return {
|
|
"champion_count": champion_count,
|
|
"skipped_no_email": skipped_no_email,
|
|
"results": results,
|
|
}
|
|
|
|
@staticmethod
|
|
def post_create_hooks(complaint, created_by, request=None):
|
|
from apps.complaints.tasks import analyze_complaint_with_ai, notify_admins_new_complaint
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="note",
|
|
message="Complaint created. AI analysis running in background.",
|
|
created_by=created_by,
|
|
)
|
|
|
|
analyze_complaint_with_ai.delay(str(complaint.id))
|
|
notify_admins_new_complaint.delay(str(complaint.id))
|
|
|
|
metadata = {
|
|
"severity": complaint.severity,
|
|
"patient_name": complaint.patient_name,
|
|
"national_id": complaint.national_id,
|
|
"hospital": complaint.hospital.name if complaint.hospital else None,
|
|
"ai_analysis_pending": True,
|
|
}
|
|
|
|
if request:
|
|
AuditService.log_from_request(
|
|
event_type="complaint_created",
|
|
description=f"Complaint created: {complaint.title}",
|
|
request=request,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|
|
else:
|
|
AuditService.log_event(
|
|
event_type="complaint_created",
|
|
description=f"Complaint created: {complaint.title}",
|
|
user=created_by,
|
|
content_object=complaint,
|
|
metadata=metadata,
|
|
)
|