HH/apps/complaints/ui_views.py
2025-12-24 12:42:31 +03:00

478 lines
16 KiB
Python

"""
Complaints UI views - Server-rendered templates for complaints console
"""
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.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
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, Physician
from .models import (
Complaint,
ComplaintAttachment,
ComplaintStatus,
ComplaintUpdate,
)
@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', 'physician',
'assigned_to', 'resolved_by', 'closed_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 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)
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)
physician_filter = request.GET.get('physician')
if physician_filter:
queryset = queryset.filter(physician_id=physician_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(patient__mrn__icontains=search_query) |
Q(patient__first_name__icontains=search_query) |
Q(patient__last_name__icontains=search_query)
)
# Date range filters
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)
# 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
stats = {
'total': queryset.count(),
'open': queryset.filter(status=ComplaintStatus.OPEN).count(),
'in_progress': queryset.filter(status=ComplaintStatus.IN_PROGRESS).count(),
'overdue': queryset.filter(is_overdue=True).count(),
}
context = {
'page_obj': page_obj,
'complaints': page_obj.object_list,
'stats': stats,
'hospitals': hospitals,
'departments': departments,
'assignable_users': assignable_users,
'status_choices': ComplaintStatus.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)
"""
complaint = get_object_or_404(
Complaint.objects.select_related(
'patient', 'hospital', 'department', 'physician',
'assigned_to', 'resolved_by', 'closed_by', 'resolution_survey'
).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 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')
# Get timeline (updates)
timeline = complaint.updates.all().order_by('-created_at')
# Get attachments
attachments = complaint.attachments.all().order_by('-created_at')
# 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)
# Check if overdue
complaint.check_overdue()
context = {
'complaint': complaint,
'timeline': timeline,
'attachments': attachments,
'px_actions': px_actions,
'assignable_users': assignable_users,
'status_choices': ComplaintStatus.choices,
'can_edit': user.is_px_admin() or user.is_hospital_admin(),
}
return render(request, 'complaints/complaint_detail.html', context)
@login_required
@require_http_methods(["GET", "POST"])
def complaint_create(request):
"""Create new complaint"""
if request.method == 'POST':
# Handle form submission
try:
from apps.organizations.models import Patient
# Get form data
patient_id = request.POST.get('patient_id')
hospital_id = request.POST.get('hospital_id')
department_id = request.POST.get('department_id', None)
physician_id = request.POST.get('physician_id', None)
title = request.POST.get('title')
description = request.POST.get('description')
category = request.POST.get('category')
subcategory = request.POST.get('subcategory', '')
priority = request.POST.get('priority')
severity = request.POST.get('severity')
source = request.POST.get('source')
encounter_id = request.POST.get('encounter_id', '')
# Validate required fields
if not all([patient_id, hospital_id, title, description, category, priority, severity, source]):
messages.error(request, "Please fill in all required fields.")
return redirect('complaints:complaint_create')
# Create complaint
complaint = Complaint.objects.create(
patient_id=patient_id,
hospital_id=hospital_id,
department_id=department_id if department_id else None,
physician_id=physician_id if physician_id else None,
title=title,
description=description,
category=category,
subcategory=subcategory,
priority=priority,
severity=severity,
source=source,
encounter_id=encounter_id,
)
# Log audit
AuditService.log_event(
event_type='complaint_created',
description=f"Complaint created: {complaint.title}",
user=request.user,
content_object=complaint,
metadata={
'category': complaint.category,
'severity': complaint.severity,
'patient_mrn': complaint.patient.mrn
}
)
messages.success(request, f"Complaint #{complaint.id} created successfully.")
return redirect('complaints:complaint_detail', pk=complaint.id)
except Exception as e:
messages.error(request, f"Error creating complaint: {str(e)}")
return redirect('complaints:complaint_create')
# GET request - show form
hospitals = Hospital.objects.filter(status='active')
if not request.user.is_px_admin() and request.user.hospital:
hospitals = hospitals.filter(id=request.user.hospital.id)
context = {
'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 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', '')
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
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)
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_escalate(request, pk):
"""Escalate complaint"""
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 escalate complaints.")
return redirect('complaints:complaint_detail', pk=pk)
reason = request.POST.get('reason', '')
# Mark as escalated
complaint.escalated_at = timezone.now()
complaint.save(update_fields=['escalated_at'])
# Create update
ComplaintUpdate.objects.create(
complaint=complaint,
update_type='escalation',
message=f"Complaint escalated. Reason: {reason}",
created_by=request.user
)
# Log audit
AuditService.log_event(
event_type='escalation',
description="Complaint escalated",
user=request.user,
content_object=complaint,
metadata={'reason': reason}
)
messages.success(request, "Complaint escalated successfully.")
return redirect('complaints:complaint_detail', pk=pk)