638 lines
22 KiB
Python
638 lines
22 KiB
Python
"""
|
|
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)
|