""" PX Action Center UI views - Server-rendered templates for action center console """ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.contenttypes.models import ContentType from django.core.paginator import Paginator from django.db.models import Q, 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 from .forms import ManualActionForm from .models import ( PXAction, PXActionAttachment, PXActionLog, ActionStatus, ActionSource, ) @login_required def action_list(request): """ PX Actions list view with advanced filters and views. Features: - Multiple views (All, My Actions, Overdue, Escalated, By Source) - Server-side pagination - Advanced filters - Search functionality - Bulk actions support """ # Base queryset with optimizations queryset = PXAction.objects.select_related( "hospital", "department", "assigned_to", "approved_by", "closed_by", "content_type" ) # Apply RBAC filters user = request.user # Get selected hospital for PX Admins (from middleware) selected_hospital = getattr(request, "tenant_hospital", None) if user.is_px_admin(): # PX Admins see all, but filter by selected hospital if set if selected_hospital: queryset = queryset.filter(hospital=selected_hospital) 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() # View filter (My Actions, Overdue, etc.) view_filter = request.GET.get("view", "all") if view_filter == "my_actions": queryset = queryset.filter(assigned_to=user) elif view_filter == "overdue": queryset = queryset.filter(is_overdue=True, status__in=[ActionStatus.OPEN, ActionStatus.IN_PROGRESS]) elif view_filter == "escalated": queryset = queryset.filter(escalation_level__gt=0) elif view_filter == "pending_approval": queryset = queryset.filter(status=ActionStatus.PENDING_APPROVAL) elif view_filter == "from_surveys": queryset = queryset.filter(source_type=ActionSource.SURVEY) elif view_filter == "from_complaints": queryset = queryset.filter(source_type__in=[ActionSource.COMPLAINT, ActionSource.COMPLAINT_RESOLUTION]) elif view_filter == "from_social": queryset = queryset.filter(source_type=ActionSource.SOCIAL_MEDIA) # 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_type_filter = request.GET.get("source_type") if source_type_filter: queryset = queryset.filter(source_type=source_type_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) assigned_to_filter = request.GET.get("assigned_to") if assigned_to_filter: queryset = queryset.filter(assigned_to_id=assigned_to_filter) # Search search_query = request.GET.get("search") if search_query: queryset = queryset.filter(Q(title__icontains=search_query) | Q(description__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=ActionStatus.OPEN).count(), "in_progress": queryset.filter(status=ActionStatus.IN_PROGRESS).count(), "overdue": queryset.filter(is_overdue=True).count(), "pending_approval": queryset.filter(status=ActionStatus.PENDING_APPROVAL).count(), "my_actions": queryset.filter(assigned_to=user).count(), } context = { "page_obj": page_obj, "actions": page_obj.object_list, "stats": stats, "hospitals": hospitals, "departments": departments, "assignable_users": assignable_users, "status_choices": ActionStatus.choices, "source_choices": ActionSource.choices, "filters": request.GET, "current_view": view_filter, } return render(request, "actions/action_list.html", context) @login_required def action_detail(request, pk): """ PX Action detail view with logs, attachments, and workflow actions. Features: - Full action details - Activity log timeline - Evidence/attachments management - SLA tracking with progress bar - Workflow actions (assign, status change, approve, add note) - Source object link """ action = get_object_or_404( PXAction.objects.select_related( "hospital", "department", "assigned_to", "approved_by", "closed_by", "content_type" ).prefetch_related("logs__created_by", "attachments__uploaded_by"), pk=pk, ) # Check access user = request.user if not user.is_px_admin(): if user.is_hospital_admin() and action.hospital != user.hospital: messages.error(request, "You don't have permission to view this action.") return redirect("actions:action_list") elif user.is_department_manager() and action.department != user.department: messages.error(request, "You don't have permission to view this action.") return redirect("actions:action_list") elif user.hospital and action.hospital != user.hospital: messages.error(request, "You don't have permission to view this action.") return redirect("actions:action_list") # Get logs (timeline) logs = action.logs.all().order_by("-created_at") # Get attachments attachments = action.attachments.all().order_by("-created_at") evidence_attachments = attachments.filter(is_evidence=True) # Get assignable users assignable_users = User.objects.filter(is_active=True) if action.hospital: assignable_users = assignable_users.filter(hospital=action.hospital) # Check if overdue action.check_overdue() # Calculate SLA progress percentage if action.created_at and action.due_at: total_duration = (action.due_at - action.created_at).total_seconds() elapsed_duration = (timezone.now() - action.created_at).total_seconds() sla_progress = min(100, int((elapsed_duration / total_duration) * 100)) if total_duration > 0 else 0 else: sla_progress = 0 context = { "action": action, "logs": logs, "attachments": attachments, "evidence_attachments": evidence_attachments, "assignable_users": assignable_users, "status_choices": ActionStatus.choices, "sla_progress": sla_progress, "can_edit": user.is_px_admin() or user.is_hospital_admin() or action.assigned_to == user, "can_approve": user.is_px_admin(), } return render(request, "actions/action_detail.html", context) @login_required @require_http_methods(["POST"]) def action_assign(request, pk): """Assign action to user""" action = get_object_or_404(PXAction, 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 actions.") return redirect("actions:action_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("actions:action_detail", pk=pk) try: assignee = User.objects.get(id=user_id) action.assigned_to = assignee action.assigned_at = timezone.now() action.save(update_fields=["assigned_to", "assigned_at"]) # Create log PXActionLog.objects.create( action=action, log_type="assignment", message=f"Assigned to {assignee.get_full_name()}", created_by=request.user, ) # Audit log AuditService.log_event( event_type="assignment", description=f"Action assigned to {assignee.get_full_name()}", user=request.user, content_object=action, ) messages.success(request, f"Action assigned to {assignee.get_full_name()}.") except User.DoesNotExist: messages.error(request, "User not found.") return redirect("actions:action_detail", pk=pk) @login_required @require_http_methods(["POST"]) def action_change_status(request, pk): """Change action status""" action = get_object_or_404(PXAction, pk=pk) # Check permission user = request.user if not (user.is_px_admin() or user.is_hospital_admin() or action.assigned_to == user): messages.error(request, "You don't have permission to change action status.") return redirect("actions:action_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("actions:action_detail", pk=pk) # Validate status transitions if new_status == ActionStatus.PENDING_APPROVAL: if action.requires_approval: evidence_count = action.attachments.filter(is_evidence=True).count() if evidence_count == 0: messages.error(request, "Evidence is required before requesting approval.") return redirect("actions:action_detail", pk=pk) elif new_status == ActionStatus.APPROVED: if not user.is_px_admin(): messages.error(request, "Only PX Admins can approve actions.") return redirect("actions:action_detail", pk=pk) action.approved_by = user action.approved_at = timezone.now() elif new_status == ActionStatus.CLOSED: action.closed_at = timezone.now() action.closed_by = user old_status = action.status action.status = new_status action.save() # Create log PXActionLog.objects.create( action=action, log_type="status_change", message=note or f"Status changed from {old_status} to {new_status}", created_by=user, old_status=old_status, new_status=new_status, ) # Audit log AuditService.log_event( event_type="status_change", description=f"Action status changed from {old_status} to {new_status}", user=user, content_object=action, metadata={"old_status": old_status, "new_status": new_status}, ) messages.success(request, f"Action status changed to {new_status}.") return redirect("actions:action_detail", pk=pk) @login_required @require_http_methods(["POST"]) def action_add_note(request, pk): """Add note to action""" action = get_object_or_404(PXAction, pk=pk) note = request.POST.get("note") if not note: messages.error(request, "Please enter a note.") return redirect("actions:action_detail", pk=pk) # Create log PXActionLog.objects.create(action=action, log_type="note", message=note, created_by=request.user) messages.success(request, "Note added successfully.") return redirect("actions:action_detail", pk=pk) @login_required @require_http_methods(["POST"]) def action_escalate(request, pk): """Escalate action""" action = get_object_or_404(PXAction, 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 actions.") return redirect("actions:action_detail", pk=pk) reason = request.POST.get("reason", "") # Increment escalation level action.escalation_level += 1 action.escalated_at = timezone.now() action.save(update_fields=["escalation_level", "escalated_at"]) # Create log PXActionLog.objects.create( action=action, log_type="escalation", message=f"Action escalated (Level {action.escalation_level}). Reason: {reason}", created_by=user, ) # Audit log AuditService.log_event( event_type="escalation", description=f"Action escalated to level {action.escalation_level}", user=user, content_object=action, metadata={"reason": reason, "escalation_level": action.escalation_level}, ) messages.success(request, f"Action escalated to level {action.escalation_level}.") return redirect("actions:action_detail", pk=pk) @login_required @require_http_methods(["POST"]) def action_approve(request, pk): """Approve action (PX Admin only)""" action = get_object_or_404(PXAction, pk=pk) # Check permission if not request.user.is_px_admin(): messages.error(request, "Only PX Admins can approve actions.") return redirect("actions:action_detail", pk=pk) if action.status != ActionStatus.PENDING_APPROVAL: messages.error(request, "Action is not pending approval.") return redirect("actions:action_detail", pk=pk) # Approve action.status = ActionStatus.APPROVED action.approved_by = request.user action.approved_at = timezone.now() action.save() # Create log PXActionLog.objects.create( action=action, log_type="approval", message=f"Action approved by {request.user.get_full_name()}", created_by=request.user, old_status=ActionStatus.PENDING_APPROVAL, new_status=ActionStatus.APPROVED, ) # Audit log AuditService.log_event( event_type="approval", description="Action approved", user=request.user, content_object=action ) messages.success(request, "Action approved successfully.") return redirect("actions:action_detail", pk=pk) @login_required def action_create(request): """ Create a new PX Action manually. Features: - Source type selection (including meeting types) - Hospital/department selection (filtered by permissions) - Assignment to users - Priority, severity, category selection - Due date configuration - Action plan input """ # Check permission user = request.user if not (user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager()): messages.error(request, "You don't have permission to create actions.") return redirect("actions:action_list") if request.method == "POST": form = ManualActionForm(request.POST, request=request) if form.is_valid(): action = form.save(commit=False) action.created_by = user # Set status to OPEN action.status = ActionStatus.OPEN # If assigned, set assignment timestamp if action.assigned_to: action.assigned_at = timezone.now() action.save() # Create log PXActionLog.objects.create( action=action, log_type="status_change", message=f"Action created manually from {action.get_source_type_display()}", created_by=user, ) # Audit log AuditService.log_event( event_type="action_created", description=f"Manual action created from {action.get_source_type_display()}", user=user, content_object=action, ) # Notify assigned user if action.assigned_to: from apps.notifications.services import NotificationService NotificationService.send_notification( recipient=action.assigned_to, title=f"New Action Assigned: {action.title}", message=f"You have been assigned a new action. Due: {action.due_at.strftime('%Y-%m-%d %H:%M')}", notification_type="action_assigned", metadata={"link": f"/actions/{action.id}/"}, ) messages.success(request, "Action created successfully.") return redirect("actions:action_detail", pk=action.id) else: # Pre-fill hospital if user has one initial = {} if user.hospital: initial["hospital"] = user.hospital.id form = ManualActionForm(request=request) context = { "form": form, "source_choices": ActionSource.choices, "status_choices": ActionStatus.choices, } return render(request, "actions/action_create.html", context) @login_required @require_http_methods(["POST"]) def action_create_from_ai(request, complaint_id): """ Create a PX Action automatically from AI suggestion. Creates action in background and returns JSON response. Links action to the complaint automatically. """ from apps.complaints.models import Complaint import json complaint = get_object_or_404(Complaint, id=complaint_id) # Check permission user = request.user if not (user.is_px_admin() or user.is_hospital_admin()): return JsonResponse({"success": False, "error": "You do not have permission to create actions."}, status=403) # Get action data from POST action_text = request.POST.get("action", "") priority = request.POST.get("priority", "medium") category = request.POST.get("category", "process_improvement") if not action_text: return JsonResponse({"success": False, "error": "Action description is required."}, status=400) try: # Map priority/severity priority_map = {"high": "high", "medium": "medium", "low": "low"} # Create action action = PXAction.objects.create( source_type="complaint", content_type=ContentType.objects.get_for_model(Complaint), object_id=complaint.id, title=f"AI Suggested Action - {complaint.reference_number}", description=action_text, hospital=complaint.hospital, department=complaint.department, category=category, priority=priority_map.get(priority, "medium"), severity=priority_map.get(priority, "medium"), status=ActionStatus.OPEN, assigned_to=complaint.assigned_to, # Assign to same person as complaint assigned_at=timezone.now() if complaint.assigned_to else None, ) # Set due date based on priority (SLA) from datetime import timedelta due_days = {"high": 3, "medium": 7, "low": 14} action.due_at = timezone.now() + timedelta(days=due_days.get(priority, 7)) action.save() # Create log PXActionLog.objects.create( action=action, log_type="status_change", message=f"Action created automatically from AI suggestion for complaint {complaint.reference_number}", ) # Audit log AuditService.log_event( event_type="action_created", description=f"Action created automatically from AI suggestion", user=user, content_object=action, metadata={ "complaint_id": str(complaint.id), "complaint_reference": complaint.reference_number, "source": "ai_suggestion", }, ) # Notify assigned user if action.assigned_to: from apps.notifications.services import NotificationService NotificationService.send_notification( recipient=action.assigned_to, title=f"New Action from AI: {action.title}", message=f"AI suggested a new action for complaint {complaint.reference_number}. Due: {action.due_at.strftime('%Y-%m-%d')}", notification_type="action_assigned", metadata={"link": f"/actions/{action.id}/"}, ) return JsonResponse( { "success": True, "action_id": str(action.id), "action_url": f"/actions/{action.id}/", "message": "PX Action created successfully from AI suggestion.", } ) except Exception as e: return JsonResponse({"success": False, "error": str(e)}, status=500)