HH/apps/complaints/ui_views.py

1480 lines
50 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.http import JsonResponse
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, Staff
from .models import (
Complaint,
ComplaintAttachment,
ComplaintCategory,
ComplaintStatus,
ComplaintUpdate,
Inquiry,
InquiryAttachment,
InquiryUpdate,
)
@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'
)
# 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)
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(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', 'staff',
'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 with AI-powered classification"""
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)
staff_id = request.POST.get('staff_id', None)
description = request.POST.get('description')
category_id = request.POST.get('category')
subcategory_id = request.POST.get('subcategory', '')
source = request.POST.get('source')
encounter_id = request.POST.get('encounter_id', '')
# Validate required fields
if not all([patient_id, hospital_id, description, category_id, source]):
messages.error(request, "Please fill in all required fields.")
return redirect('complaints:complaint_create')
# Get category and subcategory objects
category = ComplaintCategory.objects.get(id=category_id)
subcategory_obj = None
if subcategory_id:
subcategory_obj = ComplaintCategory.objects.get(id=subcategory_id)
# Create complaint with AI defaults
complaint = Complaint.objects.create(
patient_id=patient_id,
hospital_id=hospital_id,
department_id=department_id if department_id else None,
staff_id=staff_id if staff_id else None,
title='Complaint', # AI will generate title
description=description,
category=category,
subcategory=subcategory_obj.code if subcategory_obj else '',
priority='medium', # AI will update
severity='medium', # AI will update
source=source,
encounter_id=encounter_id,
)
# 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 the background using Celery
from apps.complaints.tasks import analyze_complaint_with_ai
analyze_complaint_with_ai.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={
'category': category.name_en,
'severity': complaint.severity,
'patient_mrn': complaint.patient.mrn,
'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')
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)
@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):
"""
<<<<<<< HEAD
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)
"""
=======
Inquiry detail view.
"""
from .models import Inquiry
>>>>>>> 12310a5 (update complain and add ai and sentiment analysis)
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')
<<<<<<< HEAD
# Get timeline (updates)
timeline = inquiry.updates.all().order_by('-created_at')
# Get attachments
attachments = inquiry.attachments.all().order_by('-created_at')
=======
>>>>>>> 12310a5 (update complain and add ai and sentiment analysis)
# Get assignable users
assignable_users = User.objects.filter(is_active=True)
if inquiry.hospital:
assignable_users = assignable_users.filter(hospital=inquiry.hospital)
<<<<<<< HEAD
# Status choices for the form
status_choices = [
('open', 'Open'),
('in_progress', 'In Progress'),
('resolved', 'Resolved'),
('closed', 'Closed'),
]
=======
>>>>>>> 12310a5 (update complain and add ai and sentiment analysis)
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(),
}
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 apps.organizations.models import Patient
if request.method == 'POST':
try:
# Get form data
patient_id = request.POST.get('patient_id', None)
hospital_id = request.POST.get('hospital_id')
department_id = request.POST.get('department_id', None)
subject = request.POST.get('subject')
message = request.POST.get('message')
category = request.POST.get('category')
# Contact info (if no patient)
contact_name = request.POST.get('contact_name', '')
contact_phone = request.POST.get('contact_phone', '')
contact_email = request.POST.get('contact_email', '')
# Validate required fields
if not all([hospital_id, subject, message, category]):
messages.error(request, "Please fill in all required fields.")
return redirect('complaints:inquiry_create')
# Create inquiry
inquiry = Inquiry.objects.create(
patient_id=patient_id if patient_id else None,
hospital_id=hospital_id,
department_id=department_id if department_id else None,
subject=subject,
message=message,
category=category,
contact_name=contact_name,
contact_phone=contact_phone,
contact_email=contact_email,
)
# 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)}")
return redirect('complaints:inquiry_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/inquiry_form.html', context)
@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()
<<<<<<< HEAD
# Create update
InquiryUpdate.objects.create(
inquiry=inquiry,
update_type='response',
message="Response sent",
created_by=request.user
)
=======
>>>>>>> 12310a5 (update complain and add ai and sentiment analysis)
# 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).
Key changes for AI-powered classification:
- Simplified form with only 5 required fields: name, email, phone, hospital, description
- AI generates: title, category, subcategory, department, severity, priority
- Patient lookup removed - contact info stored directly
"""
if request.method == 'POST':
try:
# Get form data from simplified form
name = request.POST.get('name')
email = request.POST.get('email')
phone = request.POST.get('phone')
hospital_id = request.POST.get('hospital')
category_id = request.POST.get('category')
subcategory_id = request.POST.get('subcategory')
description = request.POST.get('description')
# Validate required fields
errors = []
if not name:
errors.append("Name is required")
if not email:
errors.append("Email is required")
if not phone:
errors.append("Phone is required")
if not hospital_id:
errors.append("Hospital is required")
if not category_id:
errors.append("Category is required")
if not description:
errors.append("Description is 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 category and subcategory
from .models import ComplaintCategory
category = ComplaintCategory.objects.get(id=category_id)
subcategory = None
if subcategory_id:
subcategory = ComplaintCategory.objects.get(id=subcategory_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 user-selected category/subcategory
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=description,
category=category, # category is ForeignKey, assign the instance
subcategory=subcategory.code if subcategory else '', # subcategory is CharField, assign the code
severity='medium', # Default, AI will update
priority='medium', # Default, AI will update
source='public', # Mark as public submission
status='open', # Start as open
reference_number=reference_number,
# Contact info from simplified form
contact_name=name,
contact_phone=phone,
contact_email=email,
)
# 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_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.
No authentication required for public form.
"""
from .models import ComplaintCategory
hospital_id = request.GET.get('hospital_id')
# Build queryset
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('order', 'name_en')
else:
# Return only system-wide categories (empty hospitals list)
categories_queryset = ComplaintCategory.objects.filter(
hospitals__isnull=True,
is_active=True
).order_by('order', 'name_en')
# Get all categories with parent_id and descriptions
categories = categories_queryset.values(
'id',
'name_en',
'name_ar',
'code',
'parent_id',
'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})