3575 lines
127 KiB
Python
3575 lines
127 KiB
Python
"""
|
|
Complaints UI views - Server-rendered templates for complaints console
|
|
"""
|
|
|
|
import logging
|
|
|
|
from django.contrib import messages
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.core.paginator import Paginator
|
|
from django.db.models import Q, Count, Prefetch
|
|
from django.http import JsonResponse
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from apps.accounts.models import User
|
|
from apps.core.services import AuditService
|
|
from apps.organizations.models import Department, Hospital, Staff
|
|
from apps.px_sources.models import SourceUser, PXSource
|
|
from apps.px_sources.models import SourceUser, PXSource
|
|
|
|
from .models import (
|
|
Complaint,
|
|
ComplaintAttachment,
|
|
ComplaintCategory,
|
|
ComplaintExplanation,
|
|
ComplaintSourceType,
|
|
ComplaintStatus,
|
|
ComplaintType,
|
|
ComplaintUpdate,
|
|
Inquiry,
|
|
InquiryAttachment,
|
|
InquiryUpdate,
|
|
ComplaintAdverseAction,
|
|
ComplaintAdverseActionAttachment,
|
|
)
|
|
from .forms import (
|
|
ComplaintInvolvedDepartmentForm,
|
|
ComplaintInvolvedStaffForm,
|
|
DepartmentResponseForm,
|
|
StaffExplanationForm,
|
|
)
|
|
|
|
# Set up logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def can_manage_complaint(user, complaint):
|
|
"""
|
|
Check if user can manage a complaint.
|
|
|
|
Returns True if:
|
|
- User is PX Admin
|
|
- User is Hospital Admin for complaint's hospital
|
|
- User is Department Manager for complaint's department
|
|
- User is assigned to the complaint
|
|
- User is assigned to one of the involved departments
|
|
"""
|
|
if user.is_px_admin():
|
|
return True
|
|
|
|
if user.is_hospital_admin() and user.hospital == complaint.hospital:
|
|
return True
|
|
|
|
if user.is_department_manager() and user.department == complaint.department:
|
|
return True
|
|
|
|
if complaint.assigned_to == user:
|
|
return True
|
|
|
|
# Check if user is assigned to any involved department
|
|
if hasattr(complaint, 'involved_departments'):
|
|
for involved in complaint.involved_departments.all():
|
|
if involved.assigned_to == user:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
@login_required
|
|
def complaint_list(request):
|
|
"""
|
|
Complaints list view with advanced filters and pagination.
|
|
|
|
Features:
|
|
- Server-side pagination
|
|
- Advanced filters (status, severity, priority, hospital, department, etc.)
|
|
- Search by title, description, patient MRN
|
|
- Bulk actions support
|
|
- Export capability
|
|
"""
|
|
# Base queryset with optimizations
|
|
queryset = Complaint.objects.select_related(
|
|
"patient", "hospital", "department", "staff", "assigned_to", "resolved_by", "closed_by",
|
|
"source", "created_by", "domain", "category", "resolution_survey"
|
|
)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if user.is_px_admin():
|
|
pass # See all
|
|
elif user.is_hospital_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
elif user.is_department_manager() and user.department:
|
|
queryset = queryset.filter(department=user.department)
|
|
elif user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
else:
|
|
queryset = queryset.none()
|
|
|
|
# Apply filters from request
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
# Complaint Type filter (complaint vs appreciation)
|
|
complaint_type_filter = request.GET.get("complaint_type")
|
|
if complaint_type_filter:
|
|
queryset = queryset.filter(complaint_type=complaint_type_filter)
|
|
|
|
# Source Type filter (internal vs external)
|
|
source_type_filter = request.GET.get("complaint_source_type")
|
|
if source_type_filter:
|
|
queryset = queryset.filter(complaint_source_type=source_type_filter)
|
|
|
|
# PX Source filter
|
|
px_source_filter = request.GET.get("px_source")
|
|
if px_source_filter:
|
|
queryset = queryset.filter(source_id=px_source_filter)
|
|
|
|
# Domain filter (taxonomy)
|
|
domain_filter = request.GET.get("domain")
|
|
if domain_filter:
|
|
queryset = queryset.filter(domain_id=domain_filter)
|
|
|
|
# Resolution survey filter
|
|
has_survey_filter = request.GET.get("has_survey")
|
|
if has_survey_filter == "yes":
|
|
queryset = queryset.filter(resolution_survey__isnull=False)
|
|
elif has_survey_filter == "no":
|
|
queryset = queryset.filter(resolution_survey__isnull=True)
|
|
|
|
severity_filter = request.GET.get("severity")
|
|
if severity_filter:
|
|
queryset = queryset.filter(severity=severity_filter)
|
|
|
|
priority_filter = request.GET.get("priority")
|
|
if priority_filter:
|
|
queryset = queryset.filter(priority=priority_filter)
|
|
|
|
category_filter = request.GET.get("category")
|
|
if category_filter:
|
|
queryset = queryset.filter(category=category_filter)
|
|
|
|
source_filter = request.GET.get("source")
|
|
if source_filter:
|
|
queryset = queryset.filter(source=source_filter)
|
|
|
|
hospital_filter = request.GET.get("hospital")
|
|
if hospital_filter:
|
|
queryset = queryset.filter(hospital_id=hospital_filter)
|
|
|
|
department_filter = request.GET.get("department")
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
staff_filter = request.GET.get("staff")
|
|
if staff_filter:
|
|
queryset = queryset.filter(staff_id=staff_filter)
|
|
|
|
assigned_to_filter = request.GET.get("assigned_to")
|
|
if assigned_to_filter:
|
|
queryset = queryset.filter(assigned_to_id=assigned_to_filter)
|
|
|
|
overdue_filter = request.GET.get("is_overdue")
|
|
if overdue_filter == "true":
|
|
queryset = queryset.filter(is_overdue=True)
|
|
|
|
# Search
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(title__icontains=search_query)
|
|
| Q(description__icontains=search_query)
|
|
| Q(reference_number__icontains=search_query)
|
|
| Q(patient__mrn__icontains=search_query)
|
|
| Q(patient__first_name__icontains=search_query)
|
|
| Q(patient__last_name__icontains=search_query)
|
|
)
|
|
|
|
# Date range filters (created date)
|
|
date_from = request.GET.get("date_from")
|
|
if date_from:
|
|
queryset = queryset.filter(created_at__gte=date_from)
|
|
|
|
date_to = request.GET.get("date_to")
|
|
if date_to:
|
|
queryset = queryset.filter(created_at__lte=date_to)
|
|
|
|
# Incident date range filters
|
|
incident_from = request.GET.get("incident_from")
|
|
if incident_from:
|
|
queryset = queryset.filter(incident_date__gte=incident_from)
|
|
|
|
incident_to = request.GET.get("incident_to")
|
|
if incident_to:
|
|
queryset = queryset.filter(incident_date__lte=incident_to)
|
|
|
|
# Ordering
|
|
order_by = request.GET.get("order_by", "-created_at")
|
|
queryset = queryset.order_by(order_by)
|
|
|
|
# Pagination
|
|
page_size = int(request.GET.get("page_size", 25))
|
|
paginator = Paginator(queryset, page_size)
|
|
page_number = request.GET.get("page", 1)
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
# Get filter options
|
|
hospitals = Hospital.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
hospitals = hospitals.filter(id=user.hospital.id)
|
|
|
|
departments = Department.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
|
|
# Get assignable users
|
|
assignable_users = User.objects.filter(is_active=True)
|
|
if user.hospital:
|
|
assignable_users = assignable_users.filter(hospital=user.hospital)
|
|
|
|
# Statistics - more comprehensive
|
|
total_count = queryset.count()
|
|
resolved_count = queryset.filter(status=ComplaintStatus.RESOLVED).count()
|
|
pending_count = queryset.filter(status__in=[ComplaintStatus.OPEN, ComplaintStatus.IN_PROGRESS]).count()
|
|
|
|
base_stats = {
|
|
"total": total_count,
|
|
"open": queryset.filter(status=ComplaintStatus.OPEN).count(),
|
|
"in_progress": queryset.filter(status=ComplaintStatus.IN_PROGRESS).count(),
|
|
"resolved": resolved_count,
|
|
"pending": pending_count,
|
|
"resolved_percentage": (resolved_count / total_count * 100) if total_count > 0 else 0,
|
|
"overdue": queryset.filter(is_overdue=True).count(),
|
|
"complaints": queryset.filter(complaint_type=ComplaintType.COMPLAINT).count(),
|
|
"appreciations": queryset.filter(complaint_type=ComplaintType.APPRECIATION).count(),
|
|
"from_px_sources": queryset.filter(source__isnull=False).count(),
|
|
"internal": queryset.filter(complaint_source_type=ComplaintSourceType.INTERNAL).count(),
|
|
}
|
|
|
|
# Get filter options
|
|
from apps.px_sources.models import PXSource
|
|
from apps.complaints.models import ComplaintCategory
|
|
|
|
px_sources = PXSource.objects.filter(is_active=True)
|
|
|
|
# Get domains for taxonomy filter
|
|
domains = ComplaintCategory.objects.filter(
|
|
level=ComplaintCategory.LevelChoices.DOMAIN,
|
|
is_active=True
|
|
)
|
|
|
|
context = {
|
|
"complaints": page_obj,
|
|
"stats": base_stats,
|
|
"hospitals": hospitals,
|
|
"departments": departments,
|
|
"assignable_users": assignable_users,
|
|
"px_sources": px_sources,
|
|
"domains": domains,
|
|
"status_choices": ComplaintStatus.choices,
|
|
"complaint_type_choices": ComplaintType.choices,
|
|
"source_type_choices": ComplaintSourceType.choices,
|
|
"filters": request.GET,
|
|
}
|
|
|
|
return render(request, "complaints/complaint_list.html", context)
|
|
|
|
|
|
@login_required
|
|
def complaint_detail(request, pk):
|
|
"""
|
|
Complaint detail view with timeline, attachments, and actions.
|
|
|
|
Features:
|
|
- Full complaint details
|
|
- Timeline of all updates
|
|
- Attachments management
|
|
- Related surveys and journey
|
|
- Linked PX actions
|
|
- Workflow actions (assign, status change, add note)
|
|
|
|
PERFORMANCE OPTIMIZATIONS:
|
|
- Added missing select_related fields to main query
|
|
- Annotated counts to avoid N+1 queries in template
|
|
- Used prefetched data instead of re-querying
|
|
- Optimized escalation targets query
|
|
- Prefetched explanations with attachments
|
|
"""
|
|
from apps.px_sources.models import SourceUser
|
|
source_user = SourceUser.objects.filter(user=request.user).first()
|
|
base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html'
|
|
|
|
# OPTIMIZED: Added missing select_related fields and annotated counts
|
|
complaint_queryset = Complaint.objects.select_related(
|
|
"patient", "hospital", "department", "staff", "assigned_to", "resolved_by", "closed_by", "resolution_survey",
|
|
"source", "created_by", "domain", "category",
|
|
# ADD: Missing foreign keys that are accessed in template
|
|
"subcategory_obj", "classification_obj", "location", "main_section", "subsection"
|
|
).prefetch_related(
|
|
"attachments",
|
|
"updates__created_by",
|
|
"involved_departments__department",
|
|
"involved_departments__assigned_to",
|
|
"involved_staff__staff__department",
|
|
# ADD: Prefetch explanations with their attachments
|
|
Prefetch(
|
|
"explanations",
|
|
queryset=ComplaintExplanation.objects.select_related("staff").prefetch_related("attachments").order_by("-created_at")
|
|
),
|
|
# ADD: Prefetch adverse actions with related data
|
|
Prefetch(
|
|
"adverse_actions",
|
|
queryset=ComplaintAdverseAction.objects.select_related('reported_by').prefetch_related('involved_staff')
|
|
)
|
|
).annotate(
|
|
# ADD: Annotate counts to avoid N+1 queries in template
|
|
updates_count=Count("updates", distinct=True),
|
|
attachments_count=Count("attachments", distinct=True),
|
|
involved_departments_count=Count("involved_departments", distinct=True),
|
|
involved_staff_count=Count("involved_staff", distinct=True),
|
|
explanations_count=Count("explanations", distinct=True),
|
|
adverse_actions_count=Count("adverse_actions", distinct=True),
|
|
)
|
|
|
|
complaint = get_object_or_404(complaint_queryset, pk=pk)
|
|
|
|
# Check access
|
|
user = request.user
|
|
if not user.is_px_admin():
|
|
if user.is_hospital_admin() and complaint.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to view this complaint.")
|
|
return redirect("complaints:complaint_list")
|
|
elif user.is_department_manager() and complaint.department != user.department:
|
|
messages.error(request, "You don't have permission to view this complaint.")
|
|
return redirect("complaints:complaint_list")
|
|
elif user.hospital and complaint.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to view this complaint.")
|
|
return redirect("complaints:complaint_list")
|
|
|
|
# OPTIMIZED: Use prefetched data instead of re-querying
|
|
timeline = sorted(complaint.updates.all(), key=lambda x: x.created_at, reverse=True)
|
|
attachments = sorted(complaint.attachments.all(), key=lambda x: x.created_at, reverse=True)
|
|
|
|
# Get related PX actions (using ContentType since PXAction uses GenericForeignKey)
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from apps.px_action_center.models import PXAction
|
|
|
|
complaint_ct = ContentType.objects.get_for_model(Complaint)
|
|
px_actions = PXAction.objects.filter(content_type=complaint_ct, object_id=complaint.id).order_by("-created_at")
|
|
|
|
# Get assignable users
|
|
assignable_users = User.objects.filter(is_active=True)
|
|
if complaint.hospital:
|
|
assignable_users = assignable_users.filter(hospital=complaint.hospital)
|
|
|
|
# Get departments for the complaint's hospital
|
|
hospital_departments = []
|
|
if complaint.hospital:
|
|
hospital_departments = Department.objects.filter(hospital=complaint.hospital, status="active").order_by("name")
|
|
|
|
# Check if overdue (only update if necessary)
|
|
if complaint.is_active_status and not complaint.is_overdue:
|
|
complaint.check_overdue()
|
|
|
|
# OPTIMIZED: Use prefetched explanations
|
|
explanations = complaint.explanations.all()
|
|
explanation = explanations.first() if explanations else None
|
|
|
|
# OPTIMIZED: Attachments are already prefetched
|
|
explanation_attachments = explanation.attachments.all() if explanation else []
|
|
|
|
# OPTIMIZED: Escalation targets - only query managers and direct reports
|
|
escalation_targets = []
|
|
default_escalation_target = None
|
|
|
|
if complaint.hospital:
|
|
# OPTIMIZED: Only query managers and potential escalation targets
|
|
# instead of ALL staff in the hospital
|
|
from django.db.models import Q
|
|
|
|
# Get potential escalation targets:
|
|
# 1. The staff's direct manager (if exists)
|
|
# 2. Department managers in the hospital
|
|
# 3. Hospital admins in the hospital
|
|
escalation_targets_qs = Staff.objects.filter(
|
|
hospital=complaint.hospital,
|
|
status='active',
|
|
user__isnull=False,
|
|
user__is_active=True
|
|
).filter(
|
|
# Either is the staff's manager, or is a manager/admin
|
|
Q(id=complaint.staff.report_to.id if complaint.staff and complaint.staff.report_to else None) |
|
|
Q(user__groups__name__in=['Hospital Admin', 'Department Manager']) |
|
|
Q(direct_reports__isnull=False)
|
|
).exclude(
|
|
id=complaint.staff.id if complaint.staff else None
|
|
).select_related(
|
|
'user', 'department', 'report_to'
|
|
).distinct().order_by('first_name', 'last_name')
|
|
|
|
# Build list of escalation targets
|
|
for staff in escalation_targets_qs:
|
|
escalation_targets.append({
|
|
'staff': staff,
|
|
'has_user': True, # Already filtered for active user
|
|
'user_id': str(staff.user.id),
|
|
'is_manager': staff.direct_reports.exists(),
|
|
'is_line_manager': complaint.staff and complaint.staff.report_to == staff
|
|
})
|
|
|
|
# Sort: Line manager first, then other managers, then others
|
|
escalation_targets.sort(key=lambda x: (
|
|
not x['is_line_manager'],
|
|
not x['is_manager'],
|
|
x['staff'].get_full_name()
|
|
))
|
|
|
|
# Set default to staff's line manager if exists
|
|
if complaint.staff and complaint.staff.report_to:
|
|
default_escalation_target = str(complaint.staff.report_to.id)
|
|
|
|
# OPTIMIZED: Use prefetched adverse actions
|
|
adverse_actions = complaint.adverse_actions.all()
|
|
|
|
context = {
|
|
'complaint': complaint,
|
|
'timeline': timeline,
|
|
'attachments': attachments,
|
|
'px_actions': px_actions,
|
|
'assignable_users': assignable_users,
|
|
'status_choices': ComplaintStatus.choices,
|
|
'base_layout': base_layout,
|
|
'source_user': source_user,
|
|
"complaint": complaint,
|
|
"timeline": timeline,
|
|
"attachments": attachments,
|
|
"px_actions": px_actions,
|
|
"assignable_users": assignable_users,
|
|
"status_choices": ComplaintStatus.choices,
|
|
"can_edit": can_manage_complaint(user, complaint),
|
|
"is_active_status": complaint.is_active_status,
|
|
"hospital_departments": hospital_departments,
|
|
"explanation": explanation,
|
|
"explanations": explanations,
|
|
"explanation_attachments": explanation_attachments,
|
|
"escalation_targets": escalation_targets,
|
|
"default_escalation_target": default_escalation_target,
|
|
"current_user": user,
|
|
"adverse_actions": adverse_actions,
|
|
}
|
|
|
|
return render(request, "complaints/complaint_detail.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def complaint_create(request):
|
|
"""Create new complaint with AI-powered classification"""
|
|
from apps.complaints.forms import ComplaintForm
|
|
|
|
# Determine base layout based on user type
|
|
from apps.px_sources.models import SourceUser
|
|
source_user = SourceUser.objects.filter(user=request.user).first()
|
|
base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html'
|
|
|
|
if request.method == 'POST':
|
|
|
|
# Handle form submission
|
|
form = ComplaintForm(request.POST, user=request.user)
|
|
|
|
if not form.is_valid():
|
|
# Debug: print form errors
|
|
print("Form validation errors:", form.errors)
|
|
messages.error(request, f"Please correct the errors: {form.errors}")
|
|
context = {
|
|
'form': form,
|
|
'base_layout': base_layout,
|
|
'source_user': source_user,
|
|
}
|
|
return render(request, 'complaints/complaint_form.html', context)
|
|
|
|
try:
|
|
# Create complaint with AI defaults
|
|
complaint = form.save(commit=False)
|
|
|
|
# Set AI-determined defaults
|
|
complaint.title = 'Complaint' # AI will generate title
|
|
# category can be None, AI will determine it
|
|
complaint.subcategory = '' # AI will determine
|
|
|
|
# Set source from logged-in source user
|
|
if source_user and source_user.source:
|
|
complaint.source = source_user.source
|
|
else:
|
|
# Fallback: get or create a 'staff' source
|
|
from apps.px_sources.models import PXSource
|
|
try:
|
|
source_obj = PXSource.objects.get(code='staff')
|
|
except PXSource.DoesNotExist:
|
|
source_obj = PXSource.objects.create(
|
|
code='staff',
|
|
name='Staff',
|
|
description='Complaints submitted by staff members'
|
|
)
|
|
complaint.source = source_obj
|
|
|
|
complaint.priority = 'medium' # AI will update
|
|
complaint.severity = 'medium' # AI will update
|
|
complaint.created_by = request.user
|
|
|
|
# Generate unique reference number: CMP-YYYYMMDD-XXXXX
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
today = datetime.now().strftime("%Y%m%d")
|
|
random_suffix = str(uuid.uuid4().int)[:6]
|
|
complaint.reference_number = f"CMP-{today}-{random_suffix}"
|
|
|
|
complaint.save()
|
|
|
|
# Create initial update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="note",
|
|
message="Complaint created. AI analysis running in background.",
|
|
created_by=request.user,
|
|
)
|
|
|
|
# Trigger AI analysis in background using Celery
|
|
from .tasks import analyze_complaint_with_ai
|
|
|
|
analyze_complaint_with_ai.delay(str(complaint.id))
|
|
|
|
# Notify PX Admins about new complaint
|
|
# During working hours: ALL admins notified
|
|
# Outside working hours: Only ON-CALL admins notified
|
|
from .tasks import notify_admins_new_complaint
|
|
notify_admins_new_complaint.delay(str(complaint.id))
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="complaint_created",
|
|
description=f"Complaint created: {complaint.title}",
|
|
user=request.user,
|
|
content_object=complaint,
|
|
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,
|
|
},
|
|
)
|
|
|
|
messages.success(
|
|
request,
|
|
f"Complaint #{complaint.id} created successfully. AI is analyzing and classifying the complaint.",
|
|
)
|
|
return redirect("complaints:complaint_detail", pk=complaint.id)
|
|
|
|
except ComplaintCategory.DoesNotExist:
|
|
messages.error(request, "Selected category not found.")
|
|
return redirect("complaints:complaint_create")
|
|
|
|
# GET request - show form
|
|
# Check for hospital parameter from URL (for pre-selection)
|
|
initial_data = {}
|
|
hospital_id = request.GET.get('hospital')
|
|
if hospital_id:
|
|
initial_data['hospital'] = hospital_id
|
|
|
|
form = ComplaintForm(user=request.user, initial=initial_data)
|
|
|
|
context = {
|
|
'form': form,
|
|
'base_layout': base_layout,
|
|
'source_user': source_user,
|
|
# "hospitals": hospitals,
|
|
}
|
|
|
|
return render(request, "complaints/complaint_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_assign(request, pk):
|
|
"""Assign complaint to user"""
|
|
complaint = get_object_or_404(Complaint, pk=pk)
|
|
|
|
# Check if complaint is in active status
|
|
if not complaint.is_active_status:
|
|
messages.error(request, f"Cannot assign complaint with status '{complaint.get_status_display()}'. Complaint must be Open, In Progress, or Partially Resolved.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to assign complaints.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
user_id = request.POST.get("user_id")
|
|
if not user_id:
|
|
messages.error(request, "Please select a user to assign.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
try:
|
|
assignee = User.objects.get(id=user_id)
|
|
complaint.assigned_to = assignee
|
|
complaint.assigned_at = timezone.now()
|
|
complaint.save(update_fields=["assigned_to", "assigned_at"])
|
|
|
|
# Create update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="assignment",
|
|
message=f"Assigned to {assignee.get_full_name()}",
|
|
created_by=request.user,
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="assignment",
|
|
description=f"Complaint assigned to {assignee.get_full_name()}",
|
|
user=request.user,
|
|
content_object=complaint,
|
|
)
|
|
|
|
messages.success(request, f"Complaint assigned to {assignee.get_full_name()}.")
|
|
|
|
except User.DoesNotExist:
|
|
messages.error(request, "User not found.")
|
|
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_change_status(request, pk):
|
|
"""Change complaint status"""
|
|
complaint = get_object_or_404(Complaint, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to change complaint status.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
new_status = request.POST.get("status")
|
|
note = request.POST.get("note", "")
|
|
resolution = request.POST.get("resolution", "")
|
|
|
|
if not new_status:
|
|
messages.error(request, "Please select a status.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
old_status = complaint.status
|
|
complaint.status = new_status
|
|
|
|
# Handle status-specific logic
|
|
if new_status == ComplaintStatus.RESOLVED:
|
|
complaint.resolved_at = timezone.now()
|
|
complaint.resolved_by = request.user
|
|
# Save resolution note if provided
|
|
if resolution:
|
|
complaint.resolution = resolution
|
|
complaint.resolution_sent_at = timezone.now()
|
|
elif new_status == ComplaintStatus.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,
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="status_change",
|
|
description=f"Complaint status changed from {old_status} to {new_status}",
|
|
user=request.user,
|
|
content_object=complaint,
|
|
metadata={"old_status": old_status, "new_status": new_status},
|
|
)
|
|
|
|
messages.success(request, f"Complaint status changed to {new_status}.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_add_note(request, pk):
|
|
"""Add note to complaint"""
|
|
complaint = get_object_or_404(Complaint, pk=pk)
|
|
|
|
# Check if complaint is in active status
|
|
if not complaint.is_active_status:
|
|
messages.error(request, f"Cannot add notes to complaint with status '{complaint.get_status_display()}'. Complaint must be Open, In Progress, or Partially Resolved.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
note = request.POST.get("note")
|
|
if not note:
|
|
messages.error(request, "Please enter a note.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
# Create update
|
|
ComplaintUpdate.objects.create(complaint=complaint, update_type="note", message=note, created_by=request.user)
|
|
|
|
messages.success(request, "Note added successfully.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_change_department(request, pk):
|
|
"""Change complaint department"""
|
|
complaint = get_object_or_404(Complaint, pk=pk)
|
|
|
|
# Check if complaint is in active status
|
|
if not complaint.is_active_status:
|
|
messages.error(request, f"Cannot change department for complaint with status '{complaint.get_status_display()}'. Complaint must be Open, In Progress, or Partially Resolved.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to change complaint department.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
department_id = request.POST.get("department_id")
|
|
if not department_id:
|
|
messages.error(request, "Please select a department.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
try:
|
|
department = Department.objects.get(id=department_id)
|
|
|
|
# Check department belongs to same hospital
|
|
if department.hospital != complaint.hospital:
|
|
messages.error(request, "Department does not belong to this complaint's hospital.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
old_department = complaint.department
|
|
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": str(old_department.id) if old_department else None,
|
|
"new_department_id": str(department.id),
|
|
},
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="department_change",
|
|
description=f"Complaint department changed to {department.name}",
|
|
user=request.user,
|
|
content_object=complaint,
|
|
metadata={
|
|
"old_department_id": str(old_department.id) if old_department else None,
|
|
"new_department_id": str(department.id),
|
|
},
|
|
)
|
|
|
|
messages.success(request, f"Department changed to {department.name}.")
|
|
|
|
except Department.DoesNotExist:
|
|
messages.error(request, "Department not found.")
|
|
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_escalate(request, pk):
|
|
"""Escalate complaint to selected staff (default is staff's manager)"""
|
|
complaint = get_object_or_404(Complaint, pk=pk)
|
|
|
|
# Check if complaint is in active status
|
|
if not complaint.is_active_status:
|
|
messages.error(request, f"Cannot escalate complaint with status '{complaint.get_status_display()}'. Complaint must be Open, In Progress, or Partially Resolved.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to escalate complaints.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
reason = request.POST.get("reason", "")
|
|
escalate_to_id = request.POST.get("escalate_to", "")
|
|
|
|
# Get the escalation target staff
|
|
escalate_to_staff = None
|
|
escalate_to_user = None
|
|
|
|
if escalate_to_id:
|
|
try:
|
|
escalate_to_staff = Staff.objects.get(id=escalate_to_id)
|
|
if escalate_to_staff.user and escalate_to_staff.user.is_active:
|
|
escalate_to_user = escalate_to_staff.user
|
|
except Staff.DoesNotExist:
|
|
pass
|
|
|
|
# If no staff selected or not found, default to staff's manager
|
|
if not escalate_to_staff and complaint.staff and complaint.staff.report_to:
|
|
escalate_to_staff = complaint.staff.report_to
|
|
if escalate_to_staff.user and escalate_to_staff.user.is_active:
|
|
escalate_to_user = escalate_to_staff.user
|
|
|
|
# Mark as escalated and assign to selected user
|
|
complaint.escalated_at = timezone.now()
|
|
if escalate_to_user:
|
|
complaint.assigned_to = escalate_to_user
|
|
complaint.save(update_fields=["escalated_at", "assigned_to"])
|
|
|
|
# Create update with escalation details
|
|
escalation_message = f"Complaint escalated. Reason: {reason}"
|
|
if escalate_to_user:
|
|
escalation_message += f" Escalated to: {escalate_to_user.get_full_name()}"
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="escalation",
|
|
message=escalation_message,
|
|
created_by=request.user,
|
|
metadata={
|
|
"reason": reason,
|
|
"escalated_to_user_id": str(escalate_to_user.id) if escalate_to_user else None,
|
|
"escalated_to_user_name": escalate_to_user.get_full_name() if escalate_to_user else None,
|
|
},
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="escalation",
|
|
description=f"Complaint escalated to {escalate_to_user.get_full_name() if escalate_to_user else 'manager'}",
|
|
user=request.user,
|
|
content_object=complaint,
|
|
metadata={
|
|
"reason": reason,
|
|
"escalated_to_user_id": str(escalate_to_user.id) if escalate_to_user else None,
|
|
"escalated_to_user_name": escalate_to_user.get_full_name() if escalate_to_user else None,
|
|
},
|
|
)
|
|
|
|
# Send notification to the escalated user
|
|
if escalate_to_user and escalate_to_user.email:
|
|
from apps.notifications.services import NotificationService
|
|
try:
|
|
NotificationService.send_email(
|
|
email=escalate_to_user.email,
|
|
subject=f"Complaint Escalated - #{complaint.id}",
|
|
message=f"""
|
|
Dear {escalate_to_user.get_full_name()},
|
|
|
|
A complaint has been escalated to you for attention.
|
|
|
|
COMPLAINT DETAILS:
|
|
------------------
|
|
Reference: #{complaint.id}
|
|
Title: {complaint.title}
|
|
Severity: {complaint.get_severity_display()}
|
|
Priority: {complaint.get_priority_display()}
|
|
|
|
ESCALATION REASON:
|
|
------------------
|
|
{reason}
|
|
|
|
Please review and take necessary action.
|
|
|
|
View complaint: https://{request.get_host()}/complaints/{complaint.id}/
|
|
|
|
---
|
|
This is an automated message from PX360 Complaint Management System.
|
|
""",
|
|
related_object=complaint,
|
|
metadata={
|
|
'notification_type': 'complaint_escalated',
|
|
'escalated_by': str(request.user.id),
|
|
'reason': reason
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to send escalation notification: {e}")
|
|
|
|
messages.success(request, f"Complaint escalated successfully{f' to {escalate_to_user.get_full_name()}' if escalate_to_user else ''}.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_activate(request, pk):
|
|
"""
|
|
Activate complaint and assign to current user.
|
|
|
|
This allows a user to take ownership of an unassigned complaint
|
|
or reassign it to themselves.
|
|
"""
|
|
complaint = get_object_or_404(Complaint, pk=pk)
|
|
|
|
# Check if complaint is in active status
|
|
if not complaint.is_active_status:
|
|
messages.error(request, f"Cannot activate complaint with status '{complaint.get_status_display()}'. Complaint must be Open, In Progress, or Partially Resolved.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
# Check permission - must be able to edit the complaint
|
|
user = request.user
|
|
can_activate = (
|
|
user.is_px_admin() or
|
|
user.is_hospital_admin() or
|
|
(user.is_department_manager() and complaint.department == user.department) or
|
|
(complaint.hospital == user.hospital)
|
|
)
|
|
|
|
if not can_activate:
|
|
messages.error(request, "You don't have permission to activate this complaint.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
# Store previous assignee for logging
|
|
previous_assignee = complaint.assigned_to
|
|
|
|
# Assign to current user
|
|
complaint.assigned_to = user
|
|
complaint.assigned_at = timezone.now()
|
|
complaint.save(update_fields=["assigned_to", "assigned_at"])
|
|
|
|
# Create update
|
|
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()})"
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="assignment",
|
|
message=assign_message,
|
|
created_by=user,
|
|
metadata={
|
|
"assigned_to_user_id": str(user.id),
|
|
"assigned_to_user_name": user.get_full_name(),
|
|
"previous_assignee_id": str(previous_assignee.id) if previous_assignee else None,
|
|
"previous_assignee_name": previous_assignee.get_full_name() if previous_assignee else None,
|
|
"activation": True,
|
|
},
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="activation",
|
|
description=f"Complaint activated and assigned to {user.get_full_name()}",
|
|
user=user,
|
|
content_object=complaint,
|
|
metadata={
|
|
"assigned_to_user_id": str(user.id),
|
|
"assigned_to_user_name": user.get_full_name(),
|
|
"previous_assignee_id": str(previous_assignee.id) if previous_assignee else None,
|
|
"previous_assignee_name": previous_assignee.get_full_name() if previous_assignee else None,
|
|
},
|
|
)
|
|
|
|
messages.success(request, f"Complaint activated and assigned to you successfully.")
|
|
return redirect("complaints:complaint_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
def complaint_export_csv(request):
|
|
"""Export complaints to CSV"""
|
|
from apps.complaints.utils import export_complaints_csv
|
|
|
|
# Get filtered queryset (reuse list view logic)
|
|
queryset = Complaint.objects.select_related(
|
|
"patient", "hospital", "department", "staff", "assigned_to", "resolved_by", "closed_by"
|
|
)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if user.is_px_admin():
|
|
pass
|
|
elif user.is_hospital_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
elif user.is_department_manager() and user.department:
|
|
queryset = queryset.filter(department=user.department)
|
|
elif user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
else:
|
|
queryset = queryset.none()
|
|
|
|
# Apply filters from request
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
severity_filter = request.GET.get("severity")
|
|
if severity_filter:
|
|
queryset = queryset.filter(severity=severity_filter)
|
|
|
|
priority_filter = request.GET.get("priority")
|
|
if priority_filter:
|
|
queryset = queryset.filter(priority=priority_filter)
|
|
|
|
hospital_filter = request.GET.get("hospital")
|
|
if hospital_filter:
|
|
queryset = queryset.filter(hospital_id=hospital_filter)
|
|
|
|
department_filter = request.GET.get("department")
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
overdue_filter = request.GET.get("is_overdue")
|
|
if overdue_filter == "true":
|
|
queryset = queryset.filter(is_overdue=True)
|
|
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(title__icontains=search_query)
|
|
| Q(description__icontains=search_query)
|
|
| Q(patient__mrn__icontains=search_query)
|
|
)
|
|
|
|
return export_complaints_csv(queryset, request.GET.dict())
|
|
|
|
|
|
@login_required
|
|
def complaint_export_excel(request):
|
|
"""Export complaints to Excel"""
|
|
from apps.complaints.utils import export_complaints_excel
|
|
|
|
# Get filtered queryset (same as CSV)
|
|
queryset = Complaint.objects.select_related(
|
|
"patient", "hospital", "department", "staff", "assigned_to", "resolved_by", "closed_by"
|
|
)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if user.is_px_admin():
|
|
pass
|
|
elif user.is_hospital_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
elif user.is_department_manager() and user.department:
|
|
queryset = queryset.filter(department=user.department)
|
|
elif user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
else:
|
|
queryset = queryset.none()
|
|
|
|
# Apply filters from request
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
severity_filter = request.GET.get("severity")
|
|
if severity_filter:
|
|
queryset = queryset.filter(severity=severity_filter)
|
|
|
|
priority_filter = request.GET.get("priority")
|
|
if priority_filter:
|
|
queryset = queryset.filter(priority=priority_filter)
|
|
|
|
hospital_filter = request.GET.get("hospital")
|
|
if hospital_filter:
|
|
queryset = queryset.filter(hospital_id=hospital_filter)
|
|
|
|
department_filter = request.GET.get("department")
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
overdue_filter = request.GET.get("is_overdue")
|
|
if overdue_filter == "true":
|
|
queryset = queryset.filter(is_overdue=True)
|
|
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(title__icontains=search_query)
|
|
| Q(description__icontains=search_query)
|
|
| Q(patient__mrn__icontains=search_query)
|
|
)
|
|
|
|
return export_complaints_excel(queryset, request.GET.dict())
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_bulk_assign(request):
|
|
"""Bulk assign complaints"""
|
|
from apps.complaints.utils import bulk_assign_complaints
|
|
import json
|
|
|
|
# Check permission
|
|
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
|
|
return JsonResponse({"success": False, "error": "Permission denied"}, status=403)
|
|
|
|
try:
|
|
data = json.loads(request.body)
|
|
complaint_ids = data.get("complaint_ids", [])
|
|
user_id = data.get("user_id")
|
|
|
|
if not complaint_ids or not user_id:
|
|
return JsonResponse({"success": False, "error": "Missing required fields"}, status=400)
|
|
|
|
result = bulk_assign_complaints(complaint_ids, user_id, request.user)
|
|
|
|
if result["success"]:
|
|
messages.success(request, f"Successfully assigned {result['success_count']} complaints.")
|
|
|
|
return JsonResponse(result)
|
|
|
|
except json.JSONDecodeError:
|
|
return JsonResponse({"success": False, "error": "Invalid JSON"}, status=400)
|
|
except Exception as e:
|
|
return JsonResponse({"success": False, "error": str(e)}, status=500)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_bulk_status(request):
|
|
"""Bulk change complaint status"""
|
|
from apps.complaints.utils import bulk_change_status
|
|
import json
|
|
|
|
# Check permission
|
|
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
|
|
return JsonResponse({"success": False, "error": "Permission denied"}, status=403)
|
|
|
|
try:
|
|
data = json.loads(request.body)
|
|
complaint_ids = data.get("complaint_ids", [])
|
|
new_status = data.get("status")
|
|
note = data.get("note", "")
|
|
|
|
if not complaint_ids or not new_status:
|
|
return JsonResponse({"success": False, "error": "Missing required fields"}, status=400)
|
|
|
|
result = bulk_change_status(complaint_ids, new_status, request.user, note)
|
|
|
|
if result["success"]:
|
|
messages.success(request, f"Successfully updated {result['success_count']} complaints.")
|
|
|
|
return JsonResponse(result)
|
|
|
|
except json.JSONDecodeError:
|
|
return JsonResponse({"success": False, "error": "Invalid JSON"}, status=400)
|
|
except Exception as e:
|
|
return JsonResponse({"success": False, "error": str(e)}, status=500)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_bulk_escalate(request):
|
|
"""Bulk escalate complaints"""
|
|
from apps.complaints.utils import bulk_escalate_complaints
|
|
import json
|
|
|
|
# Check permission
|
|
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
|
|
return JsonResponse({"success": False, "error": "Permission denied"}, status=403)
|
|
|
|
try:
|
|
data = json.loads(request.body)
|
|
complaint_ids = data.get("complaint_ids", [])
|
|
reason = data.get("reason", "")
|
|
|
|
if not complaint_ids:
|
|
return JsonResponse({"success": False, "error": "No complaints selected"}, status=400)
|
|
|
|
result = bulk_escalate_complaints(complaint_ids, request.user, reason)
|
|
|
|
if result["success"]:
|
|
messages.success(request, f"Successfully escalated {result['success_count']} complaints.")
|
|
|
|
return JsonResponse(result)
|
|
|
|
except json.JSONDecodeError:
|
|
return JsonResponse({"success": False, "error": "Invalid JSON"}, status=400)
|
|
except Exception as e:
|
|
return JsonResponse({"success": False, "error": str(e)}, status=500)
|
|
|
|
|
|
# ============================================================================
|
|
# INQUIRIES VIEWS
|
|
# ============================================================================
|
|
|
|
|
|
@login_required
|
|
def inquiry_list(request):
|
|
"""
|
|
Inquiries list view with filters and pagination.
|
|
"""
|
|
from .models import Inquiry
|
|
|
|
# Base queryset with optimizations
|
|
queryset = Inquiry.objects.select_related("patient", "hospital", "department", "assigned_to", "responded_by")
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if user.is_px_admin():
|
|
pass # See all
|
|
elif user.is_hospital_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
elif user.is_department_manager() and user.department:
|
|
queryset = queryset.filter(department=user.department)
|
|
elif user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
else:
|
|
queryset = queryset.none()
|
|
|
|
# Apply filters
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
category_filter = request.GET.get("category")
|
|
if category_filter:
|
|
queryset = queryset.filter(category=category_filter)
|
|
|
|
hospital_filter = request.GET.get("hospital")
|
|
if hospital_filter:
|
|
queryset = queryset.filter(hospital_id=hospital_filter)
|
|
|
|
department_filter = request.GET.get("department")
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
# Search
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(subject__icontains=search_query)
|
|
| Q(message__icontains=search_query)
|
|
| Q(contact_name__icontains=search_query)
|
|
| Q(contact_email__icontains=search_query)
|
|
)
|
|
|
|
# Ordering
|
|
order_by = request.GET.get("order_by", "-created_at")
|
|
queryset = queryset.order_by(order_by)
|
|
|
|
# Pagination
|
|
page_size = int(request.GET.get("page_size", 25))
|
|
paginator = Paginator(queryset, page_size)
|
|
page_number = request.GET.get("page", 1)
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
# Get filter options
|
|
hospitals = Hospital.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
hospitals = hospitals.filter(id=user.hospital.id)
|
|
|
|
departments = Department.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
|
|
# Statistics
|
|
stats = {
|
|
"total": queryset.count(),
|
|
"open": queryset.filter(status="open").count(),
|
|
"in_progress": queryset.filter(status="in_progress").count(),
|
|
"resolved": queryset.filter(status="resolved").count(),
|
|
}
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"inquiries": page_obj.object_list,
|
|
"stats": stats,
|
|
"hospitals": hospitals,
|
|
"departments": departments,
|
|
"filters": request.GET,
|
|
}
|
|
|
|
return render(request, "complaints/inquiry_list.html", context)
|
|
|
|
|
|
@login_required
|
|
def inquiry_detail(request, pk):
|
|
"""
|
|
Inquiry detail view with timeline and attachments.
|
|
|
|
Features:
|
|
- Full inquiry details
|
|
- Timeline of all updates
|
|
- Attachments management
|
|
- Workflow actions (assign, status change, add note, respond)
|
|
"""
|
|
from apps.px_sources.models import SourceUser
|
|
source_user = SourceUser.objects.filter(user=request.user).first()
|
|
base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html'
|
|
|
|
inquiry = get_object_or_404(
|
|
Inquiry.objects.select_related(
|
|
"patient", "hospital", "department", "assigned_to", "responded_by"
|
|
).prefetch_related("attachments", "updates__created_by"),
|
|
pk=pk,
|
|
)
|
|
|
|
# Check access
|
|
user = request.user
|
|
if not user.is_px_admin():
|
|
if user.is_hospital_admin() and inquiry.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to view this inquiry.")
|
|
return redirect("complaints:inquiry_list")
|
|
elif user.hospital and inquiry.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to view this inquiry.")
|
|
return redirect("complaints:inquiry_list")
|
|
|
|
# Get timeline (updates)
|
|
timeline = inquiry.updates.all().order_by("-created_at")
|
|
|
|
# Get attachments
|
|
attachments = inquiry.attachments.all().order_by("-created_at")
|
|
|
|
# Get assignable users
|
|
assignable_users = User.objects.filter(is_active=True)
|
|
if inquiry.hospital:
|
|
assignable_users = assignable_users.filter(hospital=inquiry.hospital)
|
|
|
|
# Status choices for the form
|
|
status_choices = [
|
|
("open", "Open"),
|
|
("in_progress", "In Progress"),
|
|
("resolved", "Resolved"),
|
|
("closed", "Closed"),
|
|
]
|
|
|
|
context = {
|
|
'inquiry': inquiry,
|
|
'timeline': timeline,
|
|
'attachments': attachments,
|
|
'assignable_users': assignable_users,
|
|
'status_choices': status_choices,
|
|
'can_edit': user.is_px_admin() or user.is_hospital_admin(),
|
|
'base_layout': base_layout,
|
|
'source_user': source_user,
|
|
}
|
|
|
|
return render(request, "complaints/inquiry_detail.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def inquiry_create(request):
|
|
"""Create new inquiry"""
|
|
from .models import Inquiry
|
|
from .forms import InquiryForm
|
|
from apps.px_sources.models import SourceUser
|
|
|
|
# Determine base layout based on user type
|
|
source_user = SourceUser.objects.filter(user=request.user).first()
|
|
base_layout = 'layouts/source_user_base.html' if source_user else 'layouts/base.html'
|
|
|
|
if request.method == "POST":
|
|
form = InquiryForm(request.POST, user=request.user)
|
|
|
|
if form.is_valid():
|
|
try:
|
|
inquiry = form.save(commit=False)
|
|
|
|
# Set category from form
|
|
inquiry.category = request.POST.get("category")
|
|
|
|
inquiry.save()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="inquiry_created",
|
|
description=f"Inquiry created: {inquiry.subject}",
|
|
user=request.user,
|
|
content_object=inquiry,
|
|
metadata={"category": inquiry.category},
|
|
)
|
|
|
|
messages.success(request, f"Inquiry #{inquiry.id} created successfully.")
|
|
return redirect("complaints:inquiry_detail", pk=inquiry.id)
|
|
|
|
except Exception as e:
|
|
messages.error(request, f"Error creating inquiry: {str(e)}")
|
|
else:
|
|
messages.error(request, f"Please correct the errors: {form.errors}")
|
|
else:
|
|
# GET request - show form
|
|
# Check for hospital parameter from URL (for pre-selection)
|
|
initial_data = {}
|
|
hospital_id = request.GET.get('hospital')
|
|
if hospital_id:
|
|
initial_data['hospital'] = hospital_id
|
|
|
|
form = InquiryForm(user=request.user, initial=initial_data)
|
|
|
|
context = {
|
|
"form": form,
|
|
"base_layout": base_layout,
|
|
"source_user": source_user,
|
|
}
|
|
|
|
return render(request, "complaints/inquiry_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def inquiry_activate(request, pk):
|
|
"""Activate inquiry by assigning it to current logged-in user"""
|
|
from .models import Inquiry
|
|
|
|
inquiry = get_object_or_404(Inquiry, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to activate inquiries.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
# Check if already assigned to current user
|
|
if inquiry.assigned_to == user:
|
|
messages.info(request, "This inquiry is already assigned to you.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
old_assignee = inquiry.assigned_to
|
|
old_status = inquiry.status
|
|
|
|
# Update inquiry
|
|
inquiry.assigned_to = user
|
|
inquiry.assigned_at = timezone.now()
|
|
|
|
# Only change status to in_progress if it's currently open
|
|
if inquiry.status == "open":
|
|
inquiry.status = "in_progress"
|
|
|
|
inquiry.save(update_fields=["assigned_to", "assigned_at", "status"])
|
|
|
|
# Create update
|
|
roles_display = ', '.join(user.get_role_names())
|
|
InquiryUpdate.objects.create(
|
|
inquiry=inquiry,
|
|
update_type="assignment",
|
|
message=f"Inquiry activated and 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(),
|
|
'old_status': old_status,
|
|
'new_status': inquiry.status,
|
|
'activated_by_current_user': True
|
|
}
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="inquiry_activated",
|
|
description=f"Inquiry activated by {user.get_full_name()}",
|
|
user=request.user,
|
|
content_object=inquiry,
|
|
metadata={
|
|
'old_assignee_id': str(old_assignee.id) if old_assignee else None,
|
|
'new_assignee_id': str(user.id),
|
|
'old_status': old_status,
|
|
'new_status': inquiry.status
|
|
}
|
|
)
|
|
|
|
messages.success(request, f"Inquiry activated and assigned to you successfully.")
|
|
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def inquiry_assign(request, pk):
|
|
"""Assign inquiry to user"""
|
|
from .models import Inquiry
|
|
|
|
inquiry = get_object_or_404(Inquiry, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to assign inquiries.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
user_id = request.POST.get("user_id")
|
|
if not user_id:
|
|
messages.error(request, "Please select a user to assign.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
try:
|
|
assignee = User.objects.get(id=user_id)
|
|
inquiry.assigned_to = assignee
|
|
inquiry.save(update_fields=["assigned_to"])
|
|
|
|
# Create update
|
|
InquiryUpdate.objects.create(
|
|
inquiry=inquiry,
|
|
update_type="assignment",
|
|
message=f"Assigned to {assignee.get_full_name()}",
|
|
created_by=request.user,
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="assignment",
|
|
description=f"Inquiry assigned to {assignee.get_full_name()}",
|
|
user=request.user,
|
|
content_object=inquiry,
|
|
)
|
|
|
|
messages.success(request, f"Inquiry assigned to {assignee.get_full_name()}.")
|
|
|
|
except User.DoesNotExist:
|
|
messages.error(request, "User not found.")
|
|
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def inquiry_change_status(request, pk):
|
|
"""Change inquiry status"""
|
|
from .models import Inquiry
|
|
|
|
inquiry = get_object_or_404(Inquiry, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to change inquiry status.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
new_status = request.POST.get("status")
|
|
note = request.POST.get("note", "")
|
|
|
|
if not new_status:
|
|
messages.error(request, "Please select a status.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
old_status = inquiry.status
|
|
inquiry.status = new_status
|
|
|
|
# Handle status-specific logic
|
|
if new_status == "resolved" and not inquiry.response:
|
|
messages.error(request, "Please add a response before resolving.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
inquiry.save()
|
|
|
|
# Create update
|
|
InquiryUpdate.objects.create(
|
|
inquiry=inquiry,
|
|
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,
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="status_change",
|
|
description=f"Inquiry status changed from {old_status} to {new_status}",
|
|
user=request.user,
|
|
content_object=inquiry,
|
|
metadata={"old_status": old_status, "new_status": new_status},
|
|
)
|
|
|
|
messages.success(request, f"Inquiry status changed to {new_status}.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def inquiry_add_note(request, pk):
|
|
"""Add note to inquiry"""
|
|
from .models import Inquiry
|
|
|
|
inquiry = get_object_or_404(Inquiry, pk=pk)
|
|
|
|
note = request.POST.get("note")
|
|
if not note:
|
|
messages.error(request, "Please enter a note.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
# Create update
|
|
InquiryUpdate.objects.create(inquiry=inquiry, update_type="note", message=note, created_by=request.user)
|
|
|
|
messages.success(request, "Note added successfully.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def inquiry_respond(request, pk):
|
|
"""Respond to inquiry"""
|
|
from .models import Inquiry
|
|
|
|
inquiry = get_object_or_404(Inquiry, pk=pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to respond to inquiries.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
response = request.POST.get("response")
|
|
if not response:
|
|
messages.error(request, "Please enter a response.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
inquiry.response = response
|
|
inquiry.responded_at = timezone.now()
|
|
inquiry.responded_by = request.user
|
|
inquiry.status = "resolved"
|
|
inquiry.save()
|
|
|
|
# Create update
|
|
InquiryUpdate.objects.create(
|
|
inquiry=inquiry, update_type="response", message="Response sent", created_by=request.user
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="inquiry_responded",
|
|
description=f"Inquiry responded to: {inquiry.subject}",
|
|
user=request.user,
|
|
content_object=inquiry,
|
|
)
|
|
|
|
messages.success(request, "Response sent successfully.")
|
|
return redirect("complaints:inquiry_detail", pk=pk)
|
|
|
|
|
|
# ============================================================================
|
|
# ANALYTICS VIEWS
|
|
# ============================================================================
|
|
|
|
|
|
@login_required
|
|
def complaints_analytics(request):
|
|
"""
|
|
Complaints analytics dashboard.
|
|
"""
|
|
from .analytics import ComplaintAnalytics
|
|
|
|
user = request.user
|
|
hospital = None
|
|
|
|
# Apply RBAC
|
|
if not user.is_px_admin() and user.hospital:
|
|
hospital = user.hospital
|
|
|
|
# Get date range from request
|
|
date_range = int(request.GET.get("date_range", 30))
|
|
|
|
# Get analytics data
|
|
dashboard_summary = ComplaintAnalytics.get_dashboard_summary(hospital)
|
|
trends = ComplaintAnalytics.get_complaint_trends(hospital, date_range)
|
|
sla_compliance = ComplaintAnalytics.get_sla_compliance(hospital, date_range)
|
|
resolution_rate = ComplaintAnalytics.get_resolution_rate(hospital, date_range)
|
|
top_categories = ComplaintAnalytics.get_top_categories(hospital, date_range)
|
|
overdue_complaints = ComplaintAnalytics.get_overdue_complaints(hospital)
|
|
|
|
context = {
|
|
"dashboard_summary": dashboard_summary,
|
|
"trends": trends,
|
|
"sla_compliance": sla_compliance,
|
|
"resolution_rate": resolution_rate,
|
|
"top_categories": top_categories,
|
|
"overdue_complaints": overdue_complaints,
|
|
"date_range": date_range,
|
|
}
|
|
|
|
return render(request, "complaints/analytics.html", context)
|
|
|
|
|
|
# ============================================================================
|
|
# PUBLIC COMPLAINT FORM (No Authentication Required)
|
|
# ============================================================================
|
|
|
|
|
|
def public_complaint_submit(request):
|
|
"""
|
|
Public complaint submission form (accessible without login).
|
|
Handles both GET (show form) and POST (submit complaint).
|
|
|
|
Updated to match form structure:
|
|
- Location hierarchy: Location, Main Section, Subsection (3-level dropdowns)
|
|
- Complainant information: name, email, mobile, relation to patient
|
|
- Patient information: name, national ID, incident date
|
|
- Staff reference: staff name
|
|
- Complaint details and expected result
|
|
"""
|
|
if request.method == "POST":
|
|
try:
|
|
# Get form data from public complaint form
|
|
complainant_name = request.POST.get("complainant_name")
|
|
email = request.POST.get("email")
|
|
mobile_number = request.POST.get("mobile_number")
|
|
relation_to_patient = request.POST.get("relation_to_patient")
|
|
hospital_id = request.POST.get("hospital")
|
|
location_id = request.POST.get("location")
|
|
main_section_id = request.POST.get("main_section")
|
|
subsection_id = request.POST.get("subsection")
|
|
patient_name = request.POST.get("patient_name")
|
|
national_id = request.POST.get("national_id")
|
|
incident_date = request.POST.get("incident_date")
|
|
staff_name = request.POST.get("staff_name")
|
|
complaint_details = request.POST.get("complaint_details")
|
|
expected_result = request.POST.get("expected_result")
|
|
|
|
# Validate required fields
|
|
errors = []
|
|
if not complainant_name:
|
|
errors.append(_("Complainant name is required"))
|
|
if not mobile_number:
|
|
errors.append(_("Mobile number is required"))
|
|
if not hospital_id:
|
|
errors.append(_("Hospital is required"))
|
|
if not location_id:
|
|
errors.append(_("Location is required"))
|
|
if not main_section_id:
|
|
errors.append(_("Main section is required"))
|
|
if not complaint_details:
|
|
errors.append(_("Complaint details are required"))
|
|
|
|
if errors:
|
|
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
|
return JsonResponse({"success": False, "errors": errors}, status=400)
|
|
else:
|
|
messages.error(request, _("Please fill in all required fields."))
|
|
return render(
|
|
request,
|
|
"complaints/public_complaint_form.html",
|
|
{
|
|
"hospitals": Hospital.objects.filter(status="active").order_by("name"),
|
|
},
|
|
)
|
|
|
|
# Get hospital
|
|
hospital = Hospital.objects.get(id=hospital_id)
|
|
|
|
# Get location hierarchy objects
|
|
from apps.organizations.models import Location, MainSection, SubSection
|
|
|
|
location = Location.objects.get(id=location_id)
|
|
main_section = MainSection.objects.get(id=main_section_id)
|
|
subsection = None
|
|
if subsection_id:
|
|
subsection = SubSection.objects.get(internal_id=subsection_id)
|
|
|
|
# Generate unique reference number: CMP-YYYYMMDD-XXXXX
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
today = datetime.now().strftime("%Y%m%d")
|
|
random_suffix = str(uuid.uuid4().int)[:6]
|
|
reference_number = f"CMP-{today}-{random_suffix}"
|
|
|
|
# Create complaint with location hierarchy and all form fields
|
|
complaint = Complaint.objects.create(
|
|
patient=None, # No patient record for public submissions
|
|
hospital=hospital,
|
|
department=None, # AI will determine this
|
|
title="Complaint", # AI will generate title
|
|
description=complaint_details,
|
|
severity="medium", # Default, AI will update
|
|
priority="medium", # Default, AI will update
|
|
status="open", # Start as open
|
|
reference_number=reference_number,
|
|
# Location hierarchy (FK relationships)
|
|
location=location,
|
|
main_section=main_section,
|
|
subsection=subsection,
|
|
# Complainant information
|
|
contact_name=complainant_name,
|
|
contact_phone=mobile_number,
|
|
contact_email=email,
|
|
# Store additional information in metadata
|
|
metadata={
|
|
'relation_to_patient': relation_to_patient,
|
|
'patient_name': patient_name,
|
|
'national_id': national_id,
|
|
'incident_date': incident_date,
|
|
'staff_name': staff_name,
|
|
'expected_result': expected_result,
|
|
}
|
|
)
|
|
|
|
# Create initial update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="note",
|
|
message="Complaint submitted via public form. AI analysis running in background.",
|
|
)
|
|
|
|
# Trigger AI analysis in the background using Celery
|
|
from .tasks import analyze_complaint_with_ai
|
|
|
|
analyze_complaint_with_ai.delay(str(complaint.id))
|
|
|
|
# If form was submitted via AJAX, return JSON
|
|
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
|
return JsonResponse(
|
|
{
|
|
"success": True,
|
|
"reference_number": reference_number,
|
|
"message": "Complaint submitted successfully. AI has analyzed and classified your complaint.",
|
|
}
|
|
)
|
|
|
|
# Otherwise, redirect to success page
|
|
return redirect("complaints:public_complaint_success", reference=reference_number)
|
|
|
|
except Hospital.DoesNotExist:
|
|
error_msg = _("Selected hospital not found.")
|
|
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
|
return JsonResponse({"success": False, "message": error_msg}, status=400)
|
|
messages.error(request, error_msg)
|
|
return render(
|
|
request,
|
|
"complaints/public_complaint_form.html",
|
|
{
|
|
"hospitals": Hospital.objects.filter(status="active").order_by("name"),
|
|
},
|
|
)
|
|
except Exception as e:
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
# If AJAX, return error JSON
|
|
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
|
return JsonResponse({"success": False, "message": str(e)}, status=400)
|
|
|
|
# Otherwise, show error message
|
|
return render(
|
|
request,
|
|
"complaints/public_complaint_form.html",
|
|
{"hospitals": Hospital.objects.filter(status="active").order_by("name"), "error": str(e)},
|
|
)
|
|
|
|
# GET request - show form
|
|
return render(
|
|
request,
|
|
"complaints/public_complaint_form.html",
|
|
{
|
|
"hospitals": Hospital.objects.filter(status="active").order_by("name"),
|
|
},
|
|
)
|
|
|
|
|
|
def public_complaint_track(request):
|
|
"""
|
|
Public complaint tracking page.
|
|
|
|
Allows complainants to check the status of their complaint using the reference number.
|
|
No authentication required.
|
|
|
|
Features:
|
|
- Form to enter reference number
|
|
- Display complaint status when found
|
|
- Show basic information (status, category, submission date, last update)
|
|
- Timeline of public updates (without exposing internal notes)
|
|
- SLA deadline information
|
|
"""
|
|
complaint = None
|
|
error_message = None
|
|
reference_number = request.GET.get("reference", "").strip()
|
|
|
|
if request.method == "POST":
|
|
reference_number = request.POST.get("reference_number", "").strip()
|
|
|
|
if not reference_number:
|
|
error_message = _("Please enter a reference number.")
|
|
else:
|
|
# Try to find complaint by reference number
|
|
try:
|
|
complaint = Complaint.objects.select_related(
|
|
"hospital", "department", "location", "main_section", "subsection"
|
|
).prefetch_related("updates").get(reference_number__iexact=reference_number)
|
|
|
|
# Check overdue status
|
|
complaint.check_overdue()
|
|
|
|
except Complaint.DoesNotExist:
|
|
error_message = _("No complaint found with this reference number. Please check and try again.")
|
|
|
|
elif reference_number:
|
|
# GET request with reference parameter
|
|
try:
|
|
complaint = Complaint.objects.select_related(
|
|
"hospital", "department", "location", "main_section", "subsection"
|
|
).prefetch_related("updates").get(reference_number__iexact=reference_number)
|
|
|
|
# Check overdue status
|
|
complaint.check_overdue()
|
|
|
|
except Complaint.DoesNotExist:
|
|
error_message = _("No complaint found with this reference number. Please check and try again.")
|
|
|
|
# Get public updates only (exclude internal notes)
|
|
public_updates = []
|
|
if complaint:
|
|
public_updates = complaint.updates.filter(
|
|
update_type__in=["status_change", "resolution", "communication"]
|
|
).order_by("-created_at")
|
|
|
|
context = {
|
|
"complaint": complaint,
|
|
"public_updates": public_updates,
|
|
"error_message": error_message,
|
|
"reference_number": reference_number,
|
|
}
|
|
|
|
return render(request, "complaints/public_complaint_track.html", context)
|
|
|
|
|
|
def public_complaint_success(request, reference):
|
|
"""
|
|
Success page after public complaint submission.
|
|
"""
|
|
return render(request, "complaints/public_complaint_success.html", {"reference_number": reference})
|
|
|
|
|
|
def api_lookup_patient(request):
|
|
"""
|
|
AJAX endpoint to look up patient by national ID.
|
|
No authentication required for public form.
|
|
"""
|
|
from apps.organizations.models import Patient
|
|
|
|
national_id = request.GET.get("national_id")
|
|
|
|
if not national_id:
|
|
return JsonResponse({"found": False, "error": "National ID required"}, status=400)
|
|
|
|
try:
|
|
patient = Patient.objects.get(national_id=national_id, status="active")
|
|
|
|
return JsonResponse(
|
|
{
|
|
"found": True,
|
|
"mrn": patient.mrn,
|
|
"name": patient.get_full_name(),
|
|
"phone": patient.phone or "",
|
|
"email": patient.email or "",
|
|
}
|
|
)
|
|
|
|
except Patient.DoesNotExist:
|
|
return JsonResponse({"found": False, "message": "Patient not found"})
|
|
|
|
|
|
def api_load_departments(request):
|
|
"""
|
|
AJAX endpoint to load departments for a hospital.
|
|
No authentication required for public form.
|
|
"""
|
|
hospital_id = request.GET.get("hospital_id")
|
|
|
|
if not hospital_id:
|
|
return JsonResponse({"departments": []})
|
|
|
|
departments = Department.objects.filter(hospital_id=hospital_id, status="active").values("id", "name")
|
|
|
|
return JsonResponse({"departments": list(departments)})
|
|
|
|
|
|
def api_load_categories(request):
|
|
"""
|
|
AJAX endpoint to load complaint categories for a hospital.
|
|
Shows hospital-specific categories first, then system-wide categories.
|
|
Returns both parent categories and their subcategories with parent_id.
|
|
Now includes level field for 4-level hierarchy support (Domain, Category, Subcategory, Classification).
|
|
No authentication required for public form.
|
|
|
|
Updated: Always returns system-wide categories even without hospital_id,
|
|
to support initial form loading.
|
|
"""
|
|
from .models import ComplaintCategory
|
|
|
|
hospital_id = request.GET.get("hospital_id")
|
|
|
|
# Build queryset - always include system-wide categories
|
|
if hospital_id:
|
|
# Return hospital-specific and system-wide categories
|
|
# Empty hospitals list = system-wide
|
|
categories_queryset = (
|
|
ComplaintCategory.objects.filter(
|
|
Q(hospitals__id=hospital_id) | Q(hospitals__isnull=True),
|
|
is_active=True
|
|
)
|
|
.distinct()
|
|
.order_by("level", "order", "name_en")
|
|
)
|
|
else:
|
|
# Return all system-wide categories (empty hospitals list)
|
|
# This allows form to load domains on initial page load
|
|
categories_queryset = ComplaintCategory.objects.filter(
|
|
Q(hospitals__isnull=True),
|
|
is_active=True
|
|
).order_by("level", "order", "name_en")
|
|
|
|
# Get all categories with parent_id, level, domain_type, and descriptions
|
|
categories = categories_queryset.values(
|
|
"id", "name_en", "name_ar", "code", "parent_id", "level", "domain_type", "description_en", "description_ar"
|
|
)
|
|
|
|
return JsonResponse({"categories": list(categories)})
|
|
|
|
|
|
# ============================================================================
|
|
# AJAX/API HELPERS (Authentication Required)
|
|
# ============================================================================
|
|
|
|
|
|
@login_required
|
|
def get_departments_by_hospital(request):
|
|
"""Get departments for a hospital (AJAX)"""
|
|
hospital_id = request.GET.get("hospital_id")
|
|
if not hospital_id:
|
|
return JsonResponse({"departments": []})
|
|
|
|
departments = Department.objects.filter(hospital_id=hospital_id, status="active").values("id", "name", "name_ar")
|
|
|
|
return JsonResponse({"departments": list(departments)})
|
|
|
|
|
|
@login_required
|
|
def get_staff_by_department(request):
|
|
"""Get staff for a department (AJAX)"""
|
|
department_id = request.GET.get("department_id")
|
|
if not department_id:
|
|
return JsonResponse({"staff": []})
|
|
|
|
staff_members = Staff.objects.filter(department_id=department_id, status="active").values(
|
|
"id", "first_name", "last_name", "staff_type", "job_title"
|
|
)
|
|
|
|
return JsonResponse({"staff": list(staff_members)})
|
|
|
|
|
|
@login_required
|
|
def search_patients(request):
|
|
"""Search patients by MRN or name (AJAX)"""
|
|
from apps.organizations.models import Patient
|
|
|
|
query = request.GET.get("q", "")
|
|
if len(query) < 2:
|
|
return JsonResponse({"patients": []})
|
|
|
|
patients = Patient.objects.filter(
|
|
Q(mrn__icontains=query)
|
|
| Q(first_name__icontains=query)
|
|
| Q(last_name__icontains=query)
|
|
| Q(national_id__icontains=query)
|
|
)[:10]
|
|
|
|
results = [
|
|
{
|
|
"id": str(p.id),
|
|
"mrn": p.mrn,
|
|
"name": p.get_full_name(),
|
|
"phone": p.phone,
|
|
"email": p.email,
|
|
}
|
|
for p in patients
|
|
]
|
|
|
|
return JsonResponse({"patients": results})
|
|
|
|
|
|
# ============================================================================
|
|
# SLA CONFIGURATION MANAGEMENT
|
|
# ============================================================================
|
|
|
|
|
|
@login_required
|
|
def sla_config_list(request):
|
|
"""
|
|
SLA Configuration list view with filters.
|
|
|
|
Allows Hospital Admins and PX Admins to manage SLA configurations.
|
|
"""
|
|
from .models import ComplaintSLAConfig
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to manage SLA configurations.")
|
|
return redirect("accounts:settings")
|
|
|
|
# Base queryset
|
|
queryset = ComplaintSLAConfig.objects.select_related("hospital").all()
|
|
|
|
# Apply hospital filter
|
|
if not user.is_px_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
|
|
# Apply filters from request
|
|
hospital_filter = request.GET.get("hospital")
|
|
if hospital_filter:
|
|
queryset = queryset.filter(hospital_id=hospital_filter)
|
|
|
|
severity_filter = request.GET.get("severity")
|
|
if severity_filter:
|
|
queryset = queryset.filter(severity=severity_filter)
|
|
|
|
priority_filter = request.GET.get("priority")
|
|
if priority_filter:
|
|
queryset = queryset.filter(priority=priority_filter)
|
|
|
|
is_active_filter = request.GET.get("is_active")
|
|
if is_active_filter:
|
|
queryset = queryset.filter(is_active=(is_active_filter == "true"))
|
|
|
|
# Ordering
|
|
order_by = request.GET.get("order_by", "hospital__name")
|
|
queryset = queryset.order_by(order_by)
|
|
|
|
# Pagination
|
|
page_size = int(request.GET.get("page_size", 25))
|
|
paginator = Paginator(queryset, page_size)
|
|
page_number = request.GET.get("page", 1)
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
# Get filter options
|
|
hospitals = Hospital.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
hospitals = hospitals.filter(id=user.hospital.id)
|
|
|
|
from apps.core.models import SeverityChoices, PriorityChoices
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"sla_configs": page_obj.object_list,
|
|
"hospitals": hospitals,
|
|
"severity_choices": SeverityChoices.choices,
|
|
"priority_choices": PriorityChoices.choices,
|
|
"filters": request.GET,
|
|
}
|
|
|
|
return render(request, "complaints/sla_config_list.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def sla_config_create(request):
|
|
"""
|
|
Create new SLA configuration.
|
|
"""
|
|
from .models import ComplaintSLAConfig
|
|
from .forms import SLAConfigForm
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to create SLA configurations.")
|
|
return redirect("accounts:settings")
|
|
|
|
if request.method == "POST":
|
|
form = SLAConfigForm(request.POST, user=user)
|
|
|
|
if form.is_valid():
|
|
sla_config = form.save()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="sla_config_created",
|
|
description=f"SLA configuration created: {sla_config}",
|
|
user=request.user,
|
|
content_object=sla_config,
|
|
metadata={
|
|
"hospital": str(sla_config.hospital),
|
|
"severity": sla_config.severity,
|
|
"priority": sla_config.priority,
|
|
"sla_hours": sla_config.sla_hours,
|
|
},
|
|
)
|
|
|
|
messages.success(request, "SLA configuration created successfully.")
|
|
return redirect("complaints:sla_config_list")
|
|
else:
|
|
messages.error(request, "Please correct the errors below.")
|
|
else:
|
|
form = SLAConfigForm(user=user)
|
|
|
|
context = {
|
|
"form": form,
|
|
"title": "Create SLA Configuration",
|
|
"action": "Create",
|
|
}
|
|
|
|
return render(request, "complaints/sla_config_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def sla_config_edit(request, pk):
|
|
"""
|
|
Edit existing SLA configuration.
|
|
"""
|
|
from .models import ComplaintSLAConfig
|
|
from .forms import SLAConfigForm
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to edit SLA configurations.")
|
|
return redirect("accounts:settings")
|
|
|
|
sla_config = get_object_or_404(ComplaintSLAConfig, pk=pk)
|
|
|
|
# Check if user can edit this config
|
|
if not user.is_px_admin() and sla_config.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to edit this SLA configuration.")
|
|
return redirect("complaints:sla_config_list")
|
|
|
|
if request.method == "POST":
|
|
form = SLAConfigForm(request.POST, user=user, instance=sla_config)
|
|
|
|
if form.is_valid():
|
|
sla_config = form.save()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="sla_config_updated",
|
|
description=f"SLA configuration updated: {sla_config}",
|
|
user=request.user,
|
|
content_object=sla_config,
|
|
metadata={
|
|
"hospital": str(sla_config.hospital),
|
|
"severity": sla_config.severity,
|
|
"priority": sla_config.priority,
|
|
"sla_hours": sla_config.sla_hours,
|
|
},
|
|
)
|
|
|
|
messages.success(request, "SLA configuration updated successfully.")
|
|
return redirect("complaints:sla_config_list")
|
|
else:
|
|
messages.error(request, "Please correct the errors below.")
|
|
else:
|
|
form = SLAConfigForm(user=user, instance=sla_config)
|
|
|
|
context = {
|
|
"form": form,
|
|
"sla_config": sla_config,
|
|
"title": "Edit SLA Configuration",
|
|
"action": "Update",
|
|
}
|
|
|
|
return render(request, "complaints/sla_config_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def sla_config_delete(request, pk):
|
|
"""
|
|
Delete SLA configuration.
|
|
"""
|
|
from .models import ComplaintSLAConfig
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to delete SLA configurations.")
|
|
return redirect("accounts:settings")
|
|
|
|
sla_config = get_object_or_404(ComplaintSLAConfig, pk=pk)
|
|
|
|
# Check if user can delete this config
|
|
if not user.is_px_admin() and sla_config.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to delete this SLA configuration.")
|
|
return redirect("complaints:sla_config_list")
|
|
|
|
sla_config.delete()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="sla_config_deleted",
|
|
description=f"SLA configuration deleted: {sla_config}",
|
|
user=request.user,
|
|
metadata={
|
|
"hospital": str(sla_config.hospital),
|
|
"severity": sla_config.severity,
|
|
"priority": sla_config.priority,
|
|
},
|
|
)
|
|
|
|
messages.success(request, "SLA configuration deleted successfully.")
|
|
return redirect("complaints:sla_config_list")
|
|
|
|
|
|
@login_required
|
|
def escalation_rule_list(request):
|
|
"""
|
|
Escalation Rules list view with filters.
|
|
|
|
Allows Hospital Admins and PX Admins to manage escalation rules.
|
|
"""
|
|
from .models import EscalationRule
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to manage escalation rules.")
|
|
return redirect("accounts:settings")
|
|
|
|
# Base queryset
|
|
queryset = EscalationRule.objects.select_related("hospital", "escalate_to_user").all()
|
|
|
|
# Apply hospital filter
|
|
if not user.is_px_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
|
|
# Apply filters from request
|
|
hospital_filter = request.GET.get("hospital")
|
|
if hospital_filter:
|
|
queryset = queryset.filter(hospital_id=hospital_filter)
|
|
|
|
escalation_level_filter = request.GET.get("escalation_level")
|
|
if escalation_level_filter:
|
|
queryset = queryset.filter(escalation_level=escalation_level_filter)
|
|
|
|
is_active_filter = request.GET.get("is_active")
|
|
if is_active_filter:
|
|
queryset = queryset.filter(is_active=(is_active_filter == "true"))
|
|
|
|
# Ordering
|
|
order_by = request.GET.get("order_by", "hospital__name")
|
|
queryset = queryset.order_by(order_by)
|
|
|
|
# Pagination
|
|
page_size = int(request.GET.get("page_size", 25))
|
|
paginator = Paginator(queryset, page_size)
|
|
page_number = request.GET.get("page", 1)
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
# Get filter options
|
|
hospitals = Hospital.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
hospitals = hospitals.filter(id=user.hospital.id)
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"escalation_rules": page_obj.object_list,
|
|
"hospitals": hospitals,
|
|
"filters": request.GET,
|
|
}
|
|
|
|
return render(request, "complaints/escalation_rule_list.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def escalation_rule_create(request):
|
|
"""
|
|
Create new escalation rule.
|
|
"""
|
|
from .models import EscalationRule
|
|
from .forms import EscalationRuleForm
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to create escalation rules.")
|
|
return redirect("accounts:settings")
|
|
|
|
if request.method == "POST":
|
|
form = EscalationRuleForm(request.POST, user=user)
|
|
|
|
if form.is_valid():
|
|
escalation_rule = form.save()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="escalation_rule_created",
|
|
description=f"Escalation rule created: {escalation_rule}",
|
|
user=request.user,
|
|
content_object=escalation_rule,
|
|
metadata={
|
|
"hospital": str(escalation_rule.hospital),
|
|
"name": escalation_rule.name,
|
|
"escalation_level": escalation_rule.escalation_level,
|
|
},
|
|
)
|
|
|
|
messages.success(request, "Escalation rule created successfully.")
|
|
return redirect("complaints:escalation_rule_list")
|
|
else:
|
|
messages.error(request, "Please correct the errors below.")
|
|
else:
|
|
form = EscalationRuleForm(user=user)
|
|
|
|
context = {
|
|
"form": form,
|
|
"title": "Create Escalation Rule",
|
|
"action": "Create",
|
|
}
|
|
|
|
return render(request, "complaints/escalation_rule_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def escalation_rule_edit(request, pk):
|
|
"""
|
|
Edit existing escalation rule.
|
|
"""
|
|
from .models import EscalationRule
|
|
from .forms import EscalationRuleForm
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to edit escalation rules.")
|
|
return redirect("accounts:settings")
|
|
|
|
escalation_rule = get_object_or_404(EscalationRule, pk=pk)
|
|
|
|
# Check if user can edit this rule
|
|
if not user.is_px_admin() and escalation_rule.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to edit this escalation rule.")
|
|
return redirect("complaints:escalation_rule_list")
|
|
|
|
if request.method == "POST":
|
|
form = EscalationRuleForm(request.POST, user=user, instance=escalation_rule)
|
|
|
|
if form.is_valid():
|
|
escalation_rule = form.save()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="escalation_rule_updated",
|
|
description=f"Escalation rule updated: {escalation_rule}",
|
|
user=request.user,
|
|
content_object=escalation_rule,
|
|
metadata={
|
|
"hospital": str(escalation_rule.hospital),
|
|
"name": escalation_rule.name,
|
|
"escalation_level": escalation_rule.escalation_level,
|
|
},
|
|
)
|
|
|
|
messages.success(request, "Escalation rule updated successfully.")
|
|
return redirect("complaints:escalation_rule_list")
|
|
else:
|
|
messages.error(request, "Please correct the errors below.")
|
|
else:
|
|
form = EscalationRuleForm(user=user, instance=escalation_rule)
|
|
|
|
context = {
|
|
"form": form,
|
|
"escalation_rule": escalation_rule,
|
|
"title": "Edit Escalation Rule",
|
|
"action": "Update",
|
|
}
|
|
|
|
return render(request, "complaints/escalation_rule_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def escalation_rule_delete(request, pk):
|
|
"""
|
|
Delete escalation rule.
|
|
"""
|
|
from .models import EscalationRule
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to delete escalation rules.")
|
|
return redirect("accounts:settings")
|
|
|
|
escalation_rule = get_object_or_404(EscalationRule, pk=pk)
|
|
|
|
# Check if user can delete this rule
|
|
if not user.is_px_admin() and escalation_rule.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to delete this escalation rule.")
|
|
return redirect("complaints:escalation_rule_list")
|
|
|
|
escalation_rule.delete()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="escalation_rule_deleted",
|
|
description=f"Escalation rule deleted: {escalation_rule}",
|
|
user=request.user,
|
|
metadata={
|
|
"hospital": str(escalation_rule.hospital),
|
|
"name": escalation_rule.name,
|
|
},
|
|
)
|
|
|
|
messages.success(request, "Escalation rule deleted successfully.")
|
|
return redirect("complaints:escalation_rule_list")
|
|
|
|
|
|
@login_required
|
|
def complaint_threshold_list(request):
|
|
"""
|
|
Complaint Threshold list view with filters.
|
|
|
|
Allows Hospital Admins and PX Admins to manage complaint thresholds.
|
|
"""
|
|
from .models import ComplaintThreshold
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to manage complaint thresholds.")
|
|
return redirect("accounts:settings")
|
|
|
|
# Base queryset
|
|
queryset = ComplaintThreshold.objects.select_related("hospital").all()
|
|
|
|
# Apply hospital filter
|
|
if not user.is_px_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
|
|
# Apply filters from request
|
|
hospital_filter = request.GET.get("hospital")
|
|
if hospital_filter:
|
|
queryset = queryset.filter(hospital_id=hospital_filter)
|
|
|
|
threshold_type_filter = request.GET.get("threshold_type")
|
|
if threshold_type_filter:
|
|
queryset = queryset.filter(threshold_type=threshold_type_filter)
|
|
|
|
is_active_filter = request.GET.get("is_active")
|
|
if is_active_filter:
|
|
queryset = queryset.filter(is_active=(is_active_filter == "true"))
|
|
|
|
# Ordering
|
|
order_by = request.GET.get("order_by", "hospital__name")
|
|
queryset = queryset.order_by(order_by)
|
|
|
|
# Pagination
|
|
page_size = int(request.GET.get("page_size", 25))
|
|
paginator = Paginator(queryset, page_size)
|
|
page_number = request.GET.get("page", 1)
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
# Get filter options
|
|
hospitals = Hospital.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
hospitals = hospitals.filter(id=user.hospital.id)
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"thresholds": page_obj.object_list,
|
|
"hospitals": hospitals,
|
|
"filters": request.GET,
|
|
}
|
|
|
|
return render(request, "complaints/complaint_threshold_list.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def complaint_threshold_create(request):
|
|
"""
|
|
Create new complaint threshold.
|
|
"""
|
|
from .models import ComplaintThreshold
|
|
from .forms import ComplaintThresholdForm
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to create complaint thresholds.")
|
|
return redirect("accounts:settings")
|
|
|
|
if request.method == "POST":
|
|
form = ComplaintThresholdForm(request.POST, user=user)
|
|
|
|
if form.is_valid():
|
|
threshold = form.save()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="complaint_threshold_created",
|
|
description=f"Complaint threshold created: {threshold}",
|
|
user=request.user,
|
|
content_object=threshold,
|
|
metadata={
|
|
"hospital": str(threshold.hospital),
|
|
"threshold_type": threshold.threshold_type,
|
|
"threshold_value": threshold.threshold_value,
|
|
},
|
|
)
|
|
|
|
messages.success(request, "Complaint threshold created successfully.")
|
|
return redirect("complaints:complaint_threshold_list")
|
|
else:
|
|
messages.error(request, "Please correct the errors below.")
|
|
else:
|
|
form = ComplaintThresholdForm(user=user)
|
|
|
|
context = {
|
|
"form": form,
|
|
"title": "Create Complaint Threshold",
|
|
"action": "Create",
|
|
}
|
|
|
|
return render(request, "complaints/complaint_threshold_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def complaint_threshold_edit(request, pk):
|
|
"""
|
|
Edit existing complaint threshold.
|
|
"""
|
|
from .models import ComplaintThreshold
|
|
from .forms import ComplaintThresholdForm
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to edit complaint thresholds.")
|
|
return redirect("accounts:settings")
|
|
|
|
threshold = get_object_or_404(ComplaintThreshold, pk=pk)
|
|
|
|
# Check if user can edit this threshold
|
|
if not user.is_px_admin() and threshold.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to edit this complaint threshold.")
|
|
return redirect("complaints:complaint_threshold_list")
|
|
|
|
if request.method == "POST":
|
|
form = ComplaintThresholdForm(request.POST, user=user, instance=threshold)
|
|
|
|
if form.is_valid():
|
|
threshold = form.save()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="complaint_threshold_updated",
|
|
description=f"Complaint threshold updated: {threshold}",
|
|
user=request.user,
|
|
content_object=threshold,
|
|
metadata={
|
|
"hospital": str(threshold.hospital),
|
|
"threshold_type": threshold.threshold_type,
|
|
"threshold_value": threshold.threshold_value,
|
|
},
|
|
)
|
|
|
|
messages.success(request, "Complaint threshold updated successfully.")
|
|
return redirect("complaints:complaint_threshold_list")
|
|
else:
|
|
messages.error(request, "Please correct the errors below.")
|
|
else:
|
|
form = ComplaintThresholdForm(user=user, instance=threshold)
|
|
|
|
context = {
|
|
"form": form,
|
|
"threshold": threshold,
|
|
"title": "Edit Complaint Threshold",
|
|
"action": "Update",
|
|
}
|
|
|
|
return render(request, "complaints/complaint_threshold_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def complaint_threshold_delete(request, pk):
|
|
"""
|
|
Delete complaint threshold.
|
|
"""
|
|
from .models import ComplaintThreshold
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, "You don't have permission to delete complaint thresholds.")
|
|
return redirect("accounts:settings")
|
|
|
|
threshold = get_object_or_404(ComplaintThreshold, pk=pk)
|
|
|
|
# Check if user can delete this threshold
|
|
if not user.is_px_admin() and threshold.hospital != user.hospital:
|
|
messages.error(request, "You don't have permission to delete this complaint threshold.")
|
|
return redirect("complaints:complaint_threshold_list")
|
|
|
|
threshold.delete()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type="complaint_threshold_deleted",
|
|
description=f"Complaint threshold deleted: {threshold}",
|
|
user=request.user,
|
|
metadata={
|
|
"hospital": str(threshold.hospital),
|
|
"threshold_type": threshold.threshold_type,
|
|
},
|
|
)
|
|
|
|
messages.success(request, "Complaint threshold deleted successfully.")
|
|
return redirect("complaints:complaint_threshold_list")
|
|
|
|
|
|
|
|
# ============================================================================
|
|
# Involved Departments and Staff Views
|
|
# ============================================================================
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def involved_department_add(request, complaint_pk):
|
|
"""
|
|
Add an involved department to a complaint.
|
|
|
|
Allows assigning multiple departments with different roles:
|
|
- Primary: Main responsible department
|
|
- Secondary/Supporting: Assisting departments
|
|
- Coordination: Only for coordination
|
|
- Investigating: Leading the investigation
|
|
"""
|
|
complaint = get_object_or_404(Complaint, pk=complaint_pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not can_manage_complaint(user, complaint):
|
|
messages.error(request, _("You don't have permission to manage this complaint."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
if request.method == "POST":
|
|
form = ComplaintInvolvedDepartmentForm(
|
|
request.POST,
|
|
complaint=complaint,
|
|
user=user
|
|
)
|
|
|
|
if form.is_valid():
|
|
involved_dept = form.save(commit=False)
|
|
involved_dept.complaint = complaint
|
|
involved_dept.added_by = user
|
|
|
|
# Set assignment timestamp if assigned
|
|
if involved_dept.assigned_to and not involved_dept.assigned_at:
|
|
involved_dept.assigned_at = timezone.now()
|
|
|
|
involved_dept.save()
|
|
|
|
# Log the update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="assignment",
|
|
message=f"Department added: {involved_dept.department.name} ({involved_dept.get_role_display()})",
|
|
created_by=user,
|
|
)
|
|
|
|
# Audit log
|
|
AuditService.log_event(
|
|
event_type="complaint_department_added",
|
|
description=f"Department {involved_dept.department.name} added to complaint {complaint.reference_number}",
|
|
user=user,
|
|
content_object=complaint,
|
|
metadata={
|
|
"department_id": str(involved_dept.department.pk),
|
|
"department_name": involved_dept.department.name,
|
|
"role": involved_dept.role,
|
|
"is_primary": involved_dept.is_primary,
|
|
},
|
|
)
|
|
|
|
messages.success(
|
|
request,
|
|
_("Department '%(dept)s' added successfully as %(role)s.") % {
|
|
'dept': involved_dept.department.name,
|
|
'role': involved_dept.get_role_display()
|
|
}
|
|
)
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
else:
|
|
messages.error(request, _("Please correct the errors below."))
|
|
else:
|
|
form = ComplaintInvolvedDepartmentForm(complaint=complaint, user=user)
|
|
|
|
context = {
|
|
"form": form,
|
|
"complaint": complaint,
|
|
"title": _("Add Involved Department"),
|
|
}
|
|
|
|
return render(request, "complaints/involved_department_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def involved_department_edit(request, pk):
|
|
"""
|
|
Edit an involved department's details.
|
|
"""
|
|
from .models import ComplaintInvolvedDepartment
|
|
|
|
involved_dept = get_object_or_404(ComplaintInvolvedDepartment, pk=pk)
|
|
complaint = involved_dept.complaint
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not can_manage_complaint(user, complaint):
|
|
messages.error(request, _("You don't have permission to manage this complaint."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
if request.method == "POST":
|
|
form = ComplaintInvolvedDepartmentForm(
|
|
request.POST,
|
|
instance=involved_dept,
|
|
complaint=complaint,
|
|
user=user
|
|
)
|
|
|
|
if form.is_valid():
|
|
# Check if assignment is being changed
|
|
old_assigned_to = involved_dept.assigned_to
|
|
involved_dept = form.save(commit=False)
|
|
|
|
# Set assignment timestamp if newly assigned
|
|
if involved_dept.assigned_to and not old_assigned_to:
|
|
involved_dept.assigned_at = timezone.now()
|
|
|
|
involved_dept.save()
|
|
|
|
# Log the update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="assignment",
|
|
message=f"Department updated: {involved_dept.department.name} ({involved_dept.get_role_display()})",
|
|
created_by=user,
|
|
)
|
|
|
|
messages.success(request, _("Department involvement updated successfully."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
else:
|
|
messages.error(request, _("Please correct the errors below."))
|
|
else:
|
|
form = ComplaintInvolvedDepartmentForm(
|
|
instance=involved_dept,
|
|
complaint=complaint,
|
|
user=user
|
|
)
|
|
|
|
context = {
|
|
"form": form,
|
|
"complaint": complaint,
|
|
"involved_dept": involved_dept,
|
|
"title": _("Edit Involved Department"),
|
|
}
|
|
|
|
return render(request, "complaints/involved_department_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def involved_department_remove(request, pk):
|
|
"""
|
|
Remove an involved department from a complaint.
|
|
"""
|
|
from .models import ComplaintInvolvedDepartment
|
|
|
|
involved_dept = get_object_or_404(ComplaintInvolvedDepartment, pk=pk)
|
|
complaint = involved_dept.complaint
|
|
department_name = involved_dept.department.name
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not can_manage_complaint(user, complaint):
|
|
messages.error(request, _("You don't have permission to manage this complaint."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
involved_dept.delete()
|
|
|
|
# Log the update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="assignment",
|
|
message=f"Department removed: {department_name}",
|
|
created_by=user,
|
|
)
|
|
|
|
# Audit log
|
|
AuditService.log_event(
|
|
event_type="complaint_department_removed",
|
|
description=f"Department {department_name} removed from complaint {complaint.reference_number}",
|
|
user=user,
|
|
content_object=complaint,
|
|
)
|
|
|
|
messages.success(request, _("Department removed successfully."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def involved_department_response(request, pk):
|
|
"""
|
|
Submit a department's response to the complaint.
|
|
"""
|
|
from .models import ComplaintInvolvedDepartment
|
|
|
|
involved_dept = get_object_or_404(ComplaintInvolvedDepartment, pk=pk)
|
|
complaint = involved_dept.complaint
|
|
user = request.user
|
|
|
|
# Check permission - must be assigned to this department or have complaint management rights
|
|
can_respond = (
|
|
involved_dept.assigned_to == user or
|
|
can_manage_complaint(user, complaint)
|
|
)
|
|
|
|
if not can_respond:
|
|
messages.error(request, _("You don't have permission to submit a response for this department."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
form = DepartmentResponseForm(request.POST, instance=involved_dept)
|
|
|
|
if form.is_valid():
|
|
involved_dept = form.save(commit=False)
|
|
involved_dept.response_submitted = True
|
|
involved_dept.response_submitted_at = timezone.now()
|
|
involved_dept.save()
|
|
|
|
# Log the update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="note",
|
|
message=f"Response submitted by {involved_dept.department.name}",
|
|
created_by=user,
|
|
)
|
|
|
|
messages.success(request, _("Department response submitted successfully."))
|
|
else:
|
|
messages.error(request, _("Please provide a valid response."))
|
|
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def involved_staff_add(request, complaint_pk):
|
|
"""
|
|
Add an involved staff member to a complaint.
|
|
|
|
Allows assigning multiple staff members with different roles:
|
|
- Accused/Involved: Staff member involved in the incident
|
|
- Witness: Staff member who witnessed the incident
|
|
- Responsible: Staff responsible for resolution
|
|
- Investigator: Staff investigating the complaint
|
|
- Support: Support staff
|
|
- Coordinator: Coordination role
|
|
"""
|
|
complaint = get_object_or_404(Complaint, pk=complaint_pk)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not can_manage_complaint(user, complaint):
|
|
messages.error(request, _("You don't have permission to manage this complaint."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
if request.method == "POST":
|
|
form = ComplaintInvolvedStaffForm(
|
|
request.POST,
|
|
complaint=complaint,
|
|
user=user
|
|
)
|
|
|
|
if form.is_valid():
|
|
involved_staff = form.save(commit=False)
|
|
involved_staff.complaint = complaint
|
|
involved_staff.added_by = user
|
|
involved_staff.save()
|
|
|
|
# Log the update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="assignment",
|
|
message=f"Staff added: {involved_staff.staff} ({involved_staff.get_role_display()})",
|
|
created_by=user,
|
|
)
|
|
|
|
# Audit log
|
|
AuditService.log_event(
|
|
event_type="complaint_staff_added",
|
|
description=f"Staff {involved_staff.staff} added to complaint {complaint.reference_number}",
|
|
user=user,
|
|
content_object=complaint,
|
|
metadata={
|
|
"staff_id": str(involved_staff.staff.pk),
|
|
"staff_name": str(involved_staff.staff),
|
|
"role": involved_staff.role,
|
|
},
|
|
)
|
|
|
|
messages.success(
|
|
request,
|
|
_("Staff member '%(staff)s' added successfully as %(role)s.") % {
|
|
'staff': involved_staff.staff,
|
|
'role': involved_staff.get_role_display()
|
|
}
|
|
)
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
else:
|
|
messages.error(request, _("Please correct the errors below."))
|
|
else:
|
|
form = ComplaintInvolvedStaffForm(complaint=complaint, user=user)
|
|
|
|
context = {
|
|
"form": form,
|
|
"complaint": complaint,
|
|
"title": _("Add Involved Staff"),
|
|
}
|
|
|
|
return render(request, "complaints/involved_staff_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def involved_staff_edit(request, pk):
|
|
"""
|
|
Edit an involved staff member's details.
|
|
"""
|
|
from .models import ComplaintInvolvedStaff
|
|
|
|
involved_staff = get_object_or_404(ComplaintInvolvedStaff, pk=pk)
|
|
complaint = involved_staff.complaint
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not can_manage_complaint(user, complaint):
|
|
messages.error(request, _("You don't have permission to manage this complaint."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
if request.method == "POST":
|
|
form = ComplaintInvolvedStaffForm(
|
|
request.POST,
|
|
instance=involved_staff,
|
|
complaint=complaint,
|
|
user=user
|
|
)
|
|
|
|
if form.is_valid():
|
|
form.save()
|
|
|
|
# Log the update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="assignment",
|
|
message=f"Staff updated: {involved_staff.staff} ({involved_staff.get_role_display()})",
|
|
created_by=user,
|
|
)
|
|
|
|
messages.success(request, _("Staff involvement updated successfully."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
else:
|
|
messages.error(request, _("Please correct the errors below."))
|
|
else:
|
|
form = ComplaintInvolvedStaffForm(
|
|
instance=involved_staff,
|
|
complaint=complaint,
|
|
user=user
|
|
)
|
|
|
|
context = {
|
|
"form": form,
|
|
"complaint": complaint,
|
|
"involved_staff": involved_staff,
|
|
"title": _("Edit Involved Staff"),
|
|
}
|
|
|
|
return render(request, "complaints/involved_staff_form.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def involved_staff_remove(request, pk):
|
|
"""
|
|
Remove an involved staff member from a complaint.
|
|
"""
|
|
from .models import ComplaintInvolvedStaff
|
|
|
|
involved_staff = get_object_or_404(ComplaintInvolvedStaff, pk=pk)
|
|
complaint = involved_staff.complaint
|
|
staff_name = str(involved_staff.staff)
|
|
|
|
# Check permission
|
|
user = request.user
|
|
if not can_manage_complaint(user, complaint):
|
|
messages.error(request, _("You don't have permission to manage this complaint."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
involved_staff.delete()
|
|
|
|
# Log the update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="assignment",
|
|
message=f"Staff removed: {staff_name}",
|
|
created_by=user,
|
|
)
|
|
|
|
# Audit log
|
|
AuditService.log_event(
|
|
event_type="complaint_staff_removed",
|
|
description=f"Staff {staff_name} removed from complaint {complaint.reference_number}",
|
|
user=user,
|
|
content_object=complaint,
|
|
)
|
|
|
|
messages.success(request, _("Staff member removed successfully."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def involved_staff_explanation(request, pk):
|
|
"""
|
|
Submit an explanation as an involved staff member.
|
|
"""
|
|
from .models import ComplaintInvolvedStaff
|
|
|
|
involved_staff = get_object_or_404(ComplaintInvolvedStaff, pk=pk)
|
|
complaint = involved_staff.complaint
|
|
user = request.user
|
|
|
|
# Check permission - must be the staff member themselves or have management rights
|
|
can_submit = (
|
|
(involved_staff.staff.user == user if hasattr(involved_staff.staff, 'user') else False) or
|
|
can_manage_complaint(user, complaint)
|
|
)
|
|
|
|
if not can_submit:
|
|
messages.error(request, _("You don't have permission to submit an explanation for this staff member."))
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
form = StaffExplanationForm(request.POST, instance=involved_staff)
|
|
|
|
if form.is_valid():
|
|
involved_staff = form.save(commit=False)
|
|
involved_staff.explanation_received = True
|
|
involved_staff.explanation_received_at = timezone.now()
|
|
involved_staff.save()
|
|
|
|
# Log the update
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="note",
|
|
message=f"Explanation submitted by {involved_staff.staff}",
|
|
created_by=user,
|
|
)
|
|
|
|
messages.success(request, _("Explanation submitted successfully."))
|
|
else:
|
|
messages.error(request, _("Please provide a valid explanation."))
|
|
|
|
return redirect("complaints:complaint_detail", pk=complaint.pk)
|
|
|
|
|
|
# =============================================================================
|
|
# Complaint Adverse Action Views
|
|
# =============================================================================
|
|
|
|
@login_required
|
|
def adverse_action_list(request):
|
|
"""
|
|
List all adverse actions related to complaints.
|
|
|
|
Features:
|
|
- Filter by status, severity, action type
|
|
- Search by complaint reference or description
|
|
- Paginated results
|
|
"""
|
|
# Check permission
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
messages.error(request, _("You don't have permission to view adverse actions."))
|
|
return redirect('complaints:complaint_list')
|
|
|
|
# Base queryset
|
|
queryset = ComplaintAdverseAction.objects.select_related(
|
|
'complaint', 'complaint__hospital', 'reported_by'
|
|
).prefetch_related('involved_staff')
|
|
|
|
# Apply filters
|
|
status_filter = request.GET.get('status')
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
severity_filter = request.GET.get('severity')
|
|
if severity_filter:
|
|
queryset = queryset.filter(severity=severity_filter)
|
|
|
|
action_type_filter = request.GET.get('action_type')
|
|
if action_type_filter:
|
|
queryset = queryset.filter(action_type=action_type_filter)
|
|
|
|
# Filter by hospital for hospital admins
|
|
if user.is_hospital_admin() and user.hospital:
|
|
queryset = queryset.filter(complaint__hospital=user.hospital)
|
|
|
|
# Search
|
|
search_query = request.GET.get('search')
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(complaint__reference_number__icontains=search_query) |
|
|
Q(description__icontains=search_query) |
|
|
Q(patient_impact__icontains=search_query)
|
|
)
|
|
|
|
# Pagination
|
|
paginator = Paginator(queryset.order_by('-incident_date'), 25)
|
|
page_number = request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context = {
|
|
'page_obj': page_obj,
|
|
'status_choices': ComplaintAdverseAction.VerificationStatus.choices,
|
|
'severity_choices': ComplaintAdverseAction.SeverityLevel.choices,
|
|
'action_type_choices': ComplaintAdverseAction.ActionType.choices,
|
|
'filters': request.GET,
|
|
}
|
|
|
|
return render(request, 'complaints/adverse_action_list.html', context)
|
|
|
|
|
|
@login_required
|
|
def adverse_action_add(request, complaint_pk):
|
|
"""
|
|
Add a new adverse action to a complaint.
|
|
"""
|
|
complaint = get_object_or_404(Complaint, pk=complaint_pk)
|
|
user = request.user
|
|
|
|
# Check permission
|
|
if not can_manage_complaint(user, complaint):
|
|
messages.error(request, _("You don't have permission to add adverse actions to this complaint."))
|
|
return redirect('complaints:complaint_detail', pk=complaint_pk)
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
# Parse form data
|
|
action_type = request.POST.get('action_type')
|
|
severity = request.POST.get('severity')
|
|
description = request.POST.get('description', '').strip()
|
|
patient_impact = request.POST.get('patient_impact', '').strip()
|
|
incident_date = request.POST.get('incident_date')
|
|
location = request.POST.get('location', '').strip()
|
|
involved_staff_ids = request.POST.getlist('involved_staff')
|
|
|
|
if not description:
|
|
messages.error(request, _("Description is required."))
|
|
return render(request, 'complaints/adverse_action_form.html', {
|
|
'complaint': complaint,
|
|
'action_type_choices': ComplaintAdverseAction.ActionType.choices,
|
|
'severity_choices': ComplaintAdverseAction.SeverityLevel.choices,
|
|
'staff_list': Staff.objects.filter(hospital=complaint.hospital, is_active=True),
|
|
})
|
|
|
|
# Create adverse action
|
|
adverse_action = ComplaintAdverseAction.objects.create(
|
|
complaint=complaint,
|
|
action_type=action_type or ComplaintAdverseAction.ActionType.OTHER,
|
|
severity=severity or ComplaintAdverseAction.SeverityLevel.MEDIUM,
|
|
description=description,
|
|
patient_impact=patient_impact,
|
|
incident_date=incident_date or timezone.now(),
|
|
location=location,
|
|
reported_by=user,
|
|
status=ComplaintAdverseAction.VerificationStatus.REPORTED
|
|
)
|
|
|
|
# Add involved staff
|
|
if involved_staff_ids:
|
|
adverse_action.involved_staff.set(involved_staff_ids)
|
|
|
|
# Create timeline entry
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='note',
|
|
message=f"Adverse action reported: {adverse_action.get_action_type_display()}",
|
|
created_by=user,
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type='adverse_action_created',
|
|
description=f"Adverse action added to complaint {complaint.reference_number}: {adverse_action.get_action_type_display()}",
|
|
user=user,
|
|
content_object=adverse_action,
|
|
metadata={
|
|
'complaint_id': str(complaint.id),
|
|
'action_type': action_type,
|
|
'severity': severity,
|
|
}
|
|
)
|
|
|
|
messages.success(request, _("Adverse action reported successfully."))
|
|
return redirect('complaints:complaint_detail', pk=complaint_pk)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating adverse action: {str(e)}", exc_info=True)
|
|
messages.error(request, f"Error creating adverse action: {str(e)}")
|
|
|
|
context = {
|
|
'complaint': complaint,
|
|
'action_type_choices': ComplaintAdverseAction.ActionType.choices,
|
|
'severity_choices': ComplaintAdverseAction.SeverityLevel.choices,
|
|
'staff_list': Staff.objects.filter(hospital=complaint.hospital, is_active=True) if complaint.hospital else Staff.objects.filter(is_active=True),
|
|
}
|
|
|
|
return render(request, 'complaints/adverse_action_form.html', context)
|
|
|
|
|
|
@login_required
|
|
def adverse_action_edit(request, pk):
|
|
"""
|
|
Edit an existing adverse action.
|
|
"""
|
|
adverse_action = get_object_or_404(
|
|
ComplaintAdverseAction.objects.select_related('complaint'),
|
|
pk=pk
|
|
)
|
|
complaint = adverse_action.complaint
|
|
user = request.user
|
|
|
|
# Check permission
|
|
if not can_manage_complaint(user, complaint):
|
|
messages.error(request, _("You don't have permission to edit this adverse action."))
|
|
return redirect('complaints:complaint_detail', pk=complaint.id)
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
# Update fields
|
|
adverse_action.action_type = request.POST.get('action_type', adverse_action.action_type)
|
|
adverse_action.severity = request.POST.get('severity', adverse_action.severity)
|
|
adverse_action.description = request.POST.get('description', adverse_action.description).strip()
|
|
adverse_action.patient_impact = request.POST.get('patient_impact', adverse_action.patient_impact).strip()
|
|
adverse_action.location = request.POST.get('location', adverse_action.location).strip()
|
|
|
|
incident_date = request.POST.get('incident_date')
|
|
if incident_date:
|
|
adverse_action.incident_date = incident_date
|
|
|
|
# Update involved staff
|
|
involved_staff_ids = request.POST.getlist('involved_staff')
|
|
if involved_staff_ids:
|
|
adverse_action.involved_staff.set(involved_staff_ids)
|
|
|
|
adverse_action.save()
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type='adverse_action_updated',
|
|
description=f"Adverse action updated for complaint {complaint.reference_number}",
|
|
user=user,
|
|
content_object=adverse_action,
|
|
)
|
|
|
|
messages.success(request, _("Adverse action updated successfully."))
|
|
return redirect('complaints:complaint_detail', pk=complaint.id)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating adverse action: {str(e)}", exc_info=True)
|
|
messages.error(request, f"Error updating adverse action: {str(e)}")
|
|
|
|
context = {
|
|
'adverse_action': adverse_action,
|
|
'complaint': complaint,
|
|
'action_type_choices': ComplaintAdverseAction.ActionType.choices,
|
|
'severity_choices': ComplaintAdverseAction.SeverityLevel.choices,
|
|
'staff_list': Staff.objects.filter(hospital=complaint.hospital, is_active=True) if complaint.hospital else Staff.objects.filter(is_active=True),
|
|
'selected_staff': list(adverse_action.involved_staff.values_list('id', flat=True)),
|
|
}
|
|
|
|
return render(request, 'complaints/adverse_action_form.html', context)
|
|
|
|
|
|
@login_required
|
|
def adverse_action_update_status(request, pk):
|
|
"""
|
|
Update the status of an adverse action (investigation, resolution).
|
|
"""
|
|
adverse_action = get_object_or_404(
|
|
ComplaintAdverseAction.objects.select_related('complaint'),
|
|
pk=pk
|
|
)
|
|
complaint = adverse_action.complaint
|
|
user = request.user
|
|
|
|
# Check permission
|
|
if not can_manage_complaint(user, complaint):
|
|
messages.error(request, _("You don't have permission to update this adverse action."))
|
|
return redirect('complaints:complaint_detail', pk=complaint.id)
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
new_status = request.POST.get('status')
|
|
investigation_notes = request.POST.get('investigation_notes', '').strip()
|
|
resolution = request.POST.get('resolution', '').strip()
|
|
|
|
old_status = adverse_action.status
|
|
adverse_action.status = new_status
|
|
|
|
if investigation_notes:
|
|
adverse_action.investigation_notes = investigation_notes
|
|
|
|
if resolution:
|
|
adverse_action.resolution = resolution
|
|
|
|
# Handle status-specific updates
|
|
if new_status == ComplaintAdverseAction.VerificationStatus.UNDER_INVESTIGATION:
|
|
adverse_action.investigated_by = user
|
|
adverse_action.investigated_at = timezone.now()
|
|
|
|
elif new_status == ComplaintAdverseAction.VerificationStatus.RESOLVED:
|
|
adverse_action.resolved_by = user
|
|
adverse_action.resolved_at = timezone.now()
|
|
|
|
elif new_status == ComplaintAdverseAction.VerificationStatus.VERIFIED:
|
|
adverse_action.investigated_by = user
|
|
adverse_action.investigated_at = timezone.now()
|
|
|
|
adverse_action.save()
|
|
|
|
# Create timeline entry
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='note',
|
|
message=f"Adverse action status changed from {adverse_action.get_status_display()} to {dict(ComplaintAdverseAction.VerificationStatus.choices)[new_status]}",
|
|
created_by=user,
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type='adverse_action_status_changed',
|
|
description=f"Adverse action status changed from {old_status} to {new_status} for complaint {complaint.reference_number}",
|
|
user=user,
|
|
content_object=adverse_action,
|
|
metadata={
|
|
'old_status': old_status,
|
|
'new_status': new_status,
|
|
}
|
|
)
|
|
|
|
messages.success(request, _("Adverse action status updated successfully."))
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating adverse action status: {str(e)}", exc_info=True)
|
|
messages.error(request, f"Error updating status: {str(e)}")
|
|
|
|
return redirect('complaints:complaint_detail', pk=complaint.id)
|
|
|
|
|
|
@login_required
|
|
def adverse_action_escalate(request, pk):
|
|
"""
|
|
Escalate an adverse action to management.
|
|
"""
|
|
adverse_action = get_object_or_404(
|
|
ComplaintAdverseAction.objects.select_related('complaint'),
|
|
pk=pk
|
|
)
|
|
complaint = adverse_action.complaint
|
|
user = request.user
|
|
|
|
# Check permission
|
|
if not can_manage_complaint(user, complaint):
|
|
messages.error(request, _("You don't have permission to escalate this adverse action."))
|
|
return redirect('complaints:complaint_detail', pk=complaint.id)
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
adverse_action.is_escalated = True
|
|
adverse_action.escalated_at = timezone.now()
|
|
adverse_action.save()
|
|
|
|
# Create timeline entry
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type='escalation',
|
|
message=f"Adverse action escalated: {adverse_action.get_action_type_display()}",
|
|
created_by=user,
|
|
)
|
|
|
|
# Log audit
|
|
AuditService.log_event(
|
|
event_type='adverse_action_escalated',
|
|
description=f"Adverse action escalated for complaint {complaint.reference_number}",
|
|
user=user,
|
|
content_object=adverse_action,
|
|
)
|
|
|
|
messages.success(request, _("Adverse action escalated successfully."))
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error escalating adverse action: {str(e)}", exc_info=True)
|
|
messages.error(request, f"Error escalating: {str(e)}")
|
|
|
|
return redirect('complaints:complaint_detail', pk=complaint.id)
|
|
|
|
|
|
@login_required
|
|
def adverse_action_delete(request, pk):
|
|
"""
|
|
Delete an adverse action.
|
|
"""
|
|
adverse_action = get_object_or_404(
|
|
ComplaintAdverseAction.objects.select_related('complaint'),
|
|
pk=pk
|
|
)
|
|
complaint = adverse_action.complaint
|
|
user = request.user
|
|
|
|
# Check permission - only PX Admin or Hospital Admin can delete
|
|
if not (user.is_px_admin() or (user.is_hospital_admin() and user.hospital == complaint.hospital)):
|
|
messages.error(request, _("You don't have permission to delete adverse actions."))
|
|
return redirect('complaints:complaint_detail', pk=complaint.id)
|
|
|
|
if request.method == 'POST':
|
|
try:
|
|
complaint_pk = complaint.id
|
|
|
|
# Log before deletion
|
|
AuditService.log_event(
|
|
event_type='adverse_action_deleted',
|
|
description=f"Adverse action deleted from complaint {complaint.reference_number}: {adverse_action.get_action_type_display()}",
|
|
user=user,
|
|
metadata={
|
|
'complaint_id': str(complaint.id),
|
|
'action_type': adverse_action.action_type,
|
|
'description': adverse_action.description[:100],
|
|
}
|
|
)
|
|
|
|
adverse_action.delete()
|
|
messages.success(request, _("Adverse action deleted successfully."))
|
|
return redirect('complaints:complaint_detail', pk=complaint_pk)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error deleting adverse action: {str(e)}", exc_info=True)
|
|
messages.error(request, f"Error deleting: {str(e)}")
|
|
|
|
return redirect('complaints:complaint_detail', pk=complaint.id)
|