HH/apps/feedback/views.py
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

1009 lines
36 KiB
Python

"""
Feedback views - Server-rendered templates for feedback management
"""
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, Avg
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from apps.accounts.models import User
from apps.accounts.services import StaffActivityService
from apps.core.services import AuditService
from apps.organizations.models import Department, Hospital, Patient, Staff
from .models import (
Feedback,
FeedbackAttachment,
FeedbackResponse,
FeedbackStatus,
FeedbackType,
FeedbackCategory,
)
from .forms import (
FeedbackForm,
FeedbackResponseForm,
FeedbackStatusChangeForm,
)
@login_required
def feedback_list(request):
"""
Feedback list view with advanced filters and pagination.
Features:
- Server-side pagination
- Advanced filters (status, type, sentiment, category, hospital, etc.)
- Search by title, message, patient name
- Statistics dashboard
- Export capability
"""
# Base queryset with optimizations
queryset = Feedback.objects.select_related(
"patient", "hospital", "department", "staff", "assigned_to", "reviewed_by", "acknowledged_by", "closed_by"
).filter(is_deleted=False)
# 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()
# Apply filters from request
feedback_type_filter = request.GET.get("feedback_type")
if feedback_type_filter:
queryset = queryset.filter(feedback_type=feedback_type_filter)
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)
sentiment_filter = request.GET.get("sentiment")
if sentiment_filter:
queryset = queryset.filter(sentiment=sentiment_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)
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)
rating_min = request.GET.get("rating_min")
if rating_min:
queryset = queryset.filter(rating__gte=rating_min)
rating_max = request.GET.get("rating_max")
if rating_max:
queryset = queryset.filter(rating__lte=rating_max)
is_featured = request.GET.get("is_featured")
if is_featured == "true":
queryset = queryset.filter(is_featured=True)
requires_follow_up = request.GET.get("requires_follow_up")
if requires_follow_up == "true":
queryset = queryset.filter(requires_follow_up=True)
# Search
search_query = request.GET.get("search")
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query)
| Q(message__icontains=search_query)
| Q(patient__mrn__icontains=search_query)
| Q(patient__first_name__icontains=search_query)
| Q(patient__last_name__icontains=search_query)
| Q(contact_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)
departments = Department.objects.filter(status="active")
if not user.is_px_admin() and user.hospital:
departments = departments.filter(hospital=user.hospital)
# Get assignable users
from apps.core.utils import get_assignable_users
_hospital = request.tenant_hospital if (user.is_px_admin() and hasattr(request, "tenant_hospital") and request.tenant_hospital) else user.hospital
assignable_users = get_assignable_users(_hospital)
# Statistics
stats = {
"total": queryset.count(),
"submitted": queryset.filter(status=FeedbackStatus.SUBMITTED).count(),
"reviewed": queryset.filter(status=FeedbackStatus.REVIEWED).count(),
"acknowledged": queryset.filter(status=FeedbackStatus.ACKNOWLEDGED).count(),
"compliments": queryset.filter(feedback_type=FeedbackType.COMPLIMENT).count(),
"suggestions": queryset.filter(feedback_type=FeedbackType.SUGGESTION).count(),
"avg_rating": queryset.aggregate(Avg("rating"))["rating__avg"] or 0,
"positive": queryset.filter(sentiment="positive").count(),
"negative": queryset.filter(sentiment="negative").count(),
}
context = {
"page_obj": page_obj,
"feedbacks": page_obj.object_list,
"stats": stats,
"departments": departments,
"assignable_users": assignable_users,
"status_choices": FeedbackStatus.choices,
"type_choices": FeedbackType.choices,
"category_choices": FeedbackCategory.choices,
"filters": request.GET,
}
return render(request, "feedback/feedback_list.html", context)
@login_required
def feedback_detail(request, pk):
"""
Feedback detail view with timeline, attachments, and actions.
Features:
- Full feedback details
- Timeline of all responses
- Attachments management
- Workflow actions (assign, status change, add response)
"""
feedback = get_object_or_404(
Feedback.objects.select_related(
"patient", "hospital", "department", "staff", "assigned_to", "reviewed_by", "acknowledged_by", "closed_by"
).prefetch_related("attachments", "responses__created_by"),
pk=pk,
is_deleted=False,
)
# Check access
user = request.user
if not user.is_px_admin():
if user.is_hospital_admin() and feedback.hospital != user.hospital:
messages.error(request, "You don't have permission to view this suggestion.")
return redirect("feedback:feedback_list")
elif user.is_department_manager() and feedback.department != user.department:
messages.error(request, "You don't have permission to view this suggestion.")
return redirect("feedback:feedback_list")
elif user.hospital and feedback.hospital != user.hospital:
messages.error(request, "You don't have permission to view this suggestion.")
return redirect("feedback:feedback_list")
# Get timeline (responses)
timeline = feedback.responses.all().order_by("-created_at")
# Get attachments
attachments = feedback.attachments.all().order_by("-created_at")
# Get assignable users
from apps.core.utils import get_assignable_users
assignable_users = get_assignable_users(feedback.hospital)
from apps.rca.models import RootCauseAnalysis as RCA
from django.contrib.contenttypes.models import ContentType as CT
feedback_ct = CT.objects.get_for_model(Feedback)
linked_rcas = RCA.objects.filter(content_type=feedback_ct, object_id=feedback.pk, is_deleted=False).select_related(
"assigned_to", "created_by"
)
context = {
"feedback": feedback,
"timeline": timeline,
"attachments": attachments,
"assignable_users": assignable_users,
"status_choices": FeedbackStatus.choices,
"can_edit": user.is_px_admin() or user.is_hospital_admin(),
"linked_rcas": linked_rcas,
}
return render(request, "feedback/feedback_detail.html", context)
@login_required
@require_http_methods(["GET", "POST"])
def feedback_create(request):
from apps.organizations.models import Location, MainSection, SubSection, Hospital
from apps.feedback.models import FeedbackType, FeedbackCategory, FeedbackStatus
communication_request = None
comm_req_id_get = request.GET.get("comm_req")
prefill = {}
if comm_req_id_get:
try:
from apps.px_sources.models import CommunicationRequest
cr_data = CommunicationRequest.get_initial_data(comm_req_id_get)
communication_request = cr_data["communication_request"]
prefill = cr_data["initial"]
except Exception:
pass
if request.method == "POST":
contact_name = request.POST.get("contact_name", "").strip()
contact_phone = request.POST.get("contact_phone", "").strip()
message = request.POST.get("message", "").strip()
hospital_id = request.POST.get("hospital", "")
title = request.POST.get("title", message[:100]).strip()
location_id = request.POST.get("location", "").strip()
main_section_id = request.POST.get("main_section", "").strip()
subsection_id = request.POST.get("subsection", "").strip()
errors = []
if not contact_name:
errors.append("Name is required")
if not contact_phone:
errors.append("Phone is required")
if not message:
errors.append("Message is required")
if not hospital_id:
errors.append("Hospital is required")
if errors:
for e in errors:
messages.error(request, e)
else:
try:
hospital = Hospital.objects.get(id=hospital_id)
location = Location.objects.filter(id=location_id).first() if location_id else None
main_section = MainSection.objects.filter(id=main_section_id).first() if main_section_id else None
subsection = SubSection.objects.filter(id=subsection_id).first() if subsection_id else None
feedback = Feedback(
hospital=hospital,
feedback_type=FeedbackType.SUGGESTION,
title=title,
message=message,
category=FeedbackCategory.OTHER,
contact_name=contact_name,
contact_phone=contact_phone,
is_anonymous=False,
status=FeedbackStatus.SUBMITTED,
location=location,
main_section=main_section,
subsection=subsection,
)
feedback.save()
comm_req_id = request.POST.get("comm_req")
if comm_req_id:
try:
from apps.px_sources.models import CommunicationRequest
cr = CommunicationRequest.objects.get(pk=comm_req_id)
cr.link_to_record(feedback)
except Exception:
pass
from apps.feedback.tasks import analyze_suggestion_with_ai
from apps.complaints.tasks import notify_staff_new_item
analyze_suggestion_with_ai.delay(str(feedback.id))
notify_staff_new_item.delay("suggestion", str(feedback.id))
FeedbackResponse.objects.create(
feedback=feedback,
response_type="note",
message=f"Suggestion submitted by {request.user.get_full_name()}",
created_by=request.user,
is_internal=True,
)
AuditService.log_event(
event_type="suggestion_created",
description=f"Suggestion created: {title}",
user=request.user,
content_object=feedback,
metadata={"source": "internal_form"},
)
StaffActivityService.log_from_request(
request,
activity_type="create",
description=f"Created suggestion {feedback.id}",
content_object=feedback,
module="feedback",
)
messages.success(request, f"Suggestion #{feedback.id} created successfully.")
return redirect("feedback:feedback_detail", pk=feedback.id)
except Exception as e:
messages.error(request, f"Error creating suggestion: {str(e)}")
hospitals = Hospital.objects.filter(status="active").order_by("name")
context = {
"hospitals": hospitals,
"is_create": True,
"communication_request": communication_request,
"prefill": prefill,
}
return render(request, "feedback/feedback_form.html", context)
@login_required
@require_http_methods(["GET", "POST"])
def feedback_update(request, pk):
"""Update existing feedback"""
feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False)
# Check permission
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to edit this suggestion.")
return redirect("feedback:feedback_detail", pk=pk)
if request.method == "POST":
form = FeedbackForm(request.POST, instance=feedback, request=request)
if form.is_valid():
try:
feedback = form.save()
# Create update response
FeedbackResponse.objects.create(
feedback=feedback,
response_type="note",
message=f"Suggestion updated by {request.user.get_full_name()}",
created_by=request.user,
is_internal=True,
)
# Log audit
AuditService.log_event(
event_type="suggestion_updated",
description=f"Suggestion updated: {feedback.title}",
user=request.user,
content_object=feedback,
)
StaffActivityService.log_from_request(
request,
activity_type="update",
description=f"Updated suggestion {feedback.id}",
content_object=feedback,
module="feedback",
)
messages.success(request, "Suggestion updated successfully.")
return redirect("feedback:feedback_detail", pk=feedback.id)
except Exception as e:
messages.error(request, f"Error updating suggestion: {str(e)}")
else:
messages.error(request, "Please correct the errors below.")
else:
form = FeedbackForm(instance=feedback, request=request)
# Get patients for selection
patients = Patient.objects.filter(status="active")
if request.user.hospital:
patients = patients.filter(primary_hospital=request.user.hospital)
context = {
"form": form,
"feedback": feedback,
"patients": patients,
"is_create": False,
}
return render(request, "feedback/feedback_form.html", context)
@login_required
@require_http_methods(["GET", "POST"])
def feedback_delete(request, pk):
"""Soft delete feedback"""
feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False)
# Check permission
user = request.user
if not (user.is_px_admin() or user.is_hospital_admin()):
messages.error(request, "You don't have permission to delete this suggestion.")
return redirect("feedback:feedback_detail", pk=pk)
if request.method == "POST":
try:
feedback.soft_delete(user=request.user)
# Log audit
AuditService.log_event(
event_type="suggestion_deleted",
description=f"Suggestion deleted: {feedback.title}",
user=request.user,
content_object=feedback,
)
StaffActivityService.log_from_request(
request,
activity_type="delete",
description=f"Deleted suggestion {feedback.id}",
content_object=feedback,
module="feedback",
)
messages.success(request, "Suggestion deleted successfully.")
return redirect("feedback:feedback_list")
except Exception as e:
messages.error(request, f"Error deleting suggestion: {str(e)}")
return redirect("feedback:feedback_detail", pk=pk)
@login_required
def comment_import_list(request):
"""
Step 0 — Comment Imports list view.
"""
from django.core.exceptions import PermissionDenied
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
raise PermissionDenied
from .models import CommentImport
qs = CommentImport.objects.select_related("hospital", "imported_by")
if request.user.is_hospital_admin() and request.user.hospital:
qs = qs.filter(hospital=request.user.hospital)
elif request.user.is_px_admin() and request.tenant_hospital:
qs = qs.filter(hospital=request.tenant_hospital)
qs = qs.order_by("-year", "-month")
return render(request, "feedback/comment_import_list.html", {"imports": qs})
@login_required
def comment_list(request):
"""
Step 1 — Classified Patient Comments list view with filters.
"""
from django.core.exceptions import PermissionDenied
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
raise PermissionDenied
from .models import PatientComment
qs = PatientComment.objects.select_related("hospital")
if request.user.is_hospital_admin() and request.user.hospital:
qs = qs.filter(hospital=request.user.hospital)
elif request.user.is_px_admin() and request.tenant_hospital:
qs = qs.filter(hospital=request.tenant_hospital)
classification = request.GET.get("classification")
sub_category = request.GET.get("sub_category")
sentiment = request.GET.get("sentiment")
year = request.GET.get("year")
month = request.GET.get("month")
source_category = request.GET.get("source_category")
if classification:
qs = qs.filter(classification=classification)
if sub_category:
qs = qs.filter(sub_category=sub_category)
if sentiment:
qs = qs.filter(sentiment=sentiment)
if year:
qs = qs.filter(year=int(year))
if month:
qs = qs.filter(month=int(month))
if source_category:
qs = qs.filter(source_category=source_category)
if not request.GET.get("classified_only"):
qs = qs.filter(is_classified=True)
qs = qs.order_by("-year", "-month", "-serial_number")
from django.core.paginator import Paginator
paginator = Paginator(qs, 50)
page = request.GET.get("page", 1)
page_obj = paginator.get_page(page)
from .models import CommentClassification, CommentSubCategory, CommentSourceCategory, SentimentChoices
context = {
"page_obj": page_obj,
"classifications": CommentClassification.choices,
"sub_categories": CommentSubCategory.choices,
"sentiments": SentimentChoices.choices,
"source_categories": CommentSourceCategory.choices,
"years_range": range(2024, timezone.now().year + 1),
"months_range": range(1, 13),
"selected_classification": classification,
"selected_sub_category": sub_category,
"selected_sentiment": sentiment,
"selected_year": year,
"selected_month": month,
"selected_source_category": source_category,
}
return render(request, "feedback/comment_list.html", context)
@login_required
def action_plan_list(request):
"""
Step 5 — Comment Action Plan tracking view.
"""
from django.core.exceptions import PermissionDenied
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
raise PermissionDenied
from .models import CommentActionPlan
qs = CommentActionPlan.objects.select_related("hospital", "department")
if request.user.is_hospital_admin() and request.user.hospital:
qs = qs.filter(hospital=request.user.hospital)
elif request.user.is_px_admin() and request.tenant_hospital:
qs = qs.filter(hospital=request.tenant_hospital)
status_filter = request.GET.get("status")
year = request.GET.get("year")
if status_filter:
qs = qs.filter(status=status_filter)
if year:
qs = qs.filter(year=int(year))
qs = qs.order_by("department_label", "problem_number")
context = {
"action_plans": qs,
"selected_status": status_filter,
"selected_year": year,
"years_range": range(2024, timezone.now().year + 1),
}
return render(request, "feedback/action_plan_list.html", context)
@login_required
def export_comments_step1(request):
"""
Step 1 — Export classified comments to Excel.
"""
from django.core.exceptions import PermissionDenied
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
raise PermissionDenied
from .models import PatientComment
from .export_utils import export_classified_comments
qs = PatientComment.objects.filter(is_classified=True).select_related("hospital")
if request.user.is_hospital_admin() and request.user.hospital:
qs = qs.filter(hospital=request.user.hospital)
elif request.user.is_px_admin() and request.tenant_hospital:
qs = qs.filter(hospital=request.tenant_hospital)
year = request.GET.get("year")
month = request.GET.get("month")
if year:
qs = qs.filter(year=int(year))
if month:
qs = qs.filter(month=int(month))
return export_classified_comments(qs)
@login_required
def export_comments_step2(request):
"""
Step 2 — Export filtered comments by department to Excel.
"""
from django.core.exceptions import PermissionDenied
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
raise PermissionDenied
from .models import PatientComment
from .export_utils import export_filtered_comments_by_dept
qs = PatientComment.objects.filter(is_classified=True).select_related("hospital")
if request.user.is_hospital_admin() and request.user.hospital:
qs = qs.filter(hospital=request.user.hospital)
elif request.user.is_px_admin() and request.tenant_hospital:
qs = qs.filter(hospital=request.tenant_hospital)
year = request.GET.get("year")
if year:
qs = qs.filter(year=int(year))
return export_filtered_comments_by_dept(qs)
@login_required
def export_action_plans(request):
"""
Step 3/5 — Export action plans to Excel.
"""
from django.core.exceptions import PermissionDenied
if not (request.user.is_px_admin() or request.user.is_hospital_admin()):
raise PermissionDenied
from .models import CommentActionPlan
from .export_utils import export_action_plans
qs = CommentActionPlan.objects.select_related("hospital", "department")
if request.user.is_hospital_admin() and request.user.hospital:
qs = qs.filter(hospital=request.user.hospital)
elif request.user.is_px_admin() and request.tenant_hospital:
qs = qs.filter(hospital=request.tenant_hospital)
return export_action_plans(qs)
context = {
"feedback": feedback,
}
return render(request, "feedback/feedback_delete_confirm.html", context)
@login_required
@require_http_methods(["POST"])
def feedback_assign(request, pk):
"""Assign feedback to user"""
feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False)
# 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 this suggestion.")
return redirect("feedback:feedback_detail", pk=pk)
user_id = request.POST.get("user_id")
note = request.POST.get("note", "")
if not user_id:
messages.error(request, "Please select a user to assign.")
return redirect("feedback:feedback_detail", pk=pk)
try:
assignee = User.objects.get(id=user_id)
feedback.assigned_to = assignee
feedback.assigned_at = timezone.now()
feedback.save(update_fields=["assigned_to", "assigned_at"])
# Create response
message = f"Assigned to {assignee.get_full_name()}"
if note:
message += f"\nNote: {note}"
FeedbackResponse.objects.create(
feedback=feedback, response_type="assignment", message=message, created_by=request.user, is_internal=True
)
# Log audit
AuditService.log_event(
event_type="assignment",
description=f"Suggestion assigned to {assignee.get_full_name()}",
user=request.user,
content_object=feedback,
)
messages.success(request, f"Suggestion assigned to {assignee.get_full_name()}.")
except User.DoesNotExist:
messages.error(request, "User not found.")
return redirect("feedback:feedback_detail", pk=pk)
@login_required
@require_http_methods(["POST"])
def feedback_change_status(request, pk):
"""Change feedback status"""
feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False)
# 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 this suggestion's status.")
return redirect("feedback:feedback_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("feedback:feedback_detail", pk=pk)
old_status = feedback.status
feedback.status = new_status
# Handle status-specific logic
if new_status == FeedbackStatus.REVIEWED:
feedback.reviewed_at = timezone.now()
feedback.reviewed_by = request.user
elif new_status == FeedbackStatus.ACKNOWLEDGED:
feedback.acknowledged_at = timezone.now()
feedback.acknowledged_by = request.user
elif new_status == FeedbackStatus.CLOSED:
feedback.closed_at = timezone.now()
feedback.closed_by = request.user
feedback.save()
# Create response
message = note or f"Status changed from {old_status} to {new_status}"
FeedbackResponse.objects.create(
feedback=feedback,
response_type="status_change",
message=message,
created_by=request.user,
old_status=old_status,
new_status=new_status,
is_internal=True,
)
# Log audit
AuditService.log_event(
event_type="status_change",
description=f"Suggestion status changed from {old_status} to {new_status}",
user=request.user,
content_object=feedback,
metadata={"old_status": old_status, "new_status": new_status},
)
messages.success(request, f"Suggestion status changed to {new_status}.")
return redirect("feedback:feedback_detail", pk=pk)
@login_required
@require_http_methods(["POST"])
def feedback_add_response(request, pk):
"""Add response to feedback"""
feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False)
response_type = request.POST.get("response_type", "response")
message = request.POST.get("message")
is_internal = request.POST.get("is_internal") == "on"
if not message:
messages.error(request, "Please enter a response message.")
return redirect("feedback:feedback_detail", pk=pk)
# Create response
FeedbackResponse.objects.create(
feedback=feedback,
response_type=response_type,
message=message,
created_by=request.user,
is_internal=is_internal,
)
messages.success(request, "Response added successfully.")
return redirect("feedback:feedback_detail", pk=pk)
@login_required
@require_http_methods(["POST"])
def feedback_toggle_featured(request, pk):
"""Toggle featured status"""
feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False)
# 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 feature this suggestion.")
return redirect("feedback:feedback_detail", pk=pk)
feedback.is_featured = not feedback.is_featured
feedback.save(update_fields=["is_featured"])
status = "featured" if feedback.is_featured else "unfeatured"
messages.success(request, f"Suggestion {status} successfully.")
return redirect("feedback:feedback_detail", pk=pk)
@login_required
@require_http_methods(["POST"])
def feedback_toggle_follow_up(request, pk):
"""Toggle follow-up required status"""
feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False)
# 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 modify this suggestion.")
return redirect("feedback:feedback_detail", pk=pk)
feedback.requires_follow_up = not feedback.requires_follow_up
feedback.save(update_fields=["requires_follow_up"])
status = "marked for follow-up" if feedback.requires_follow_up else "unmarked for follow-up"
messages.success(request, f"Suggestion {status} successfully.")
return redirect("feedback:feedback_detail", pk=pk)
@csrf_exempt
@require_http_methods(["POST"])
def public_suggestion_submit(request):
import json
import logging
logger = logging.getLogger(__name__)
try:
try:
data = json.loads(request.body) if request.content_type == "application/json" else request.POST
except json.JSONDecodeError:
data = request.POST
contact_name = data.get("contact_name", "").strip()
contact_phone = data.get("contact_phone", "").strip()
message = data.get("message", "").strip()
title = data.get("title", message[:100]).strip()
hospital_id = data.get("hospital", "")
category = data.get("category", "general")
location_id = data.get("location", "").strip()
main_section_id = data.get("main_section", "").strip()
subsection_id = data.get("subsection", "").strip()
if not contact_name or not contact_phone or not message:
return JsonResponse({"success": False, "message": "Name, phone, and message are required."}, status=400)
if not hospital_id:
return JsonResponse({"success": False, "message": "Please select a hospital."}, status=400)
try:
hospital = Hospital.objects.get(id=hospital_id)
except Hospital.DoesNotExist:
return JsonResponse({"success": False, "message": "Invalid hospital."}, status=400)
from apps.organizations.models import Location, MainSection, SubSection
location = Location.objects.filter(id=location_id).first() if location_id else None
main_section = MainSection.objects.filter(id=main_section_id).first() if main_section_id else None
subsection = SubSection.objects.filter(id=subsection_id).first() if subsection_id else None
category_map = {
"general": FeedbackCategory.OTHER,
"clinical_care": FeedbackCategory.CLINICAL_CARE,
"facility": FeedbackCategory.FACILITY,
"staff_service": FeedbackCategory.STAFF_SERVICE,
"communication": FeedbackCategory.COMMUNICATION,
"technology": FeedbackCategory.TECHNOLOGY,
"food_service": FeedbackCategory.FOOD_SERVICE,
"appointment": FeedbackCategory.APPOINTMENT,
"other": FeedbackCategory.OTHER,
}
feedback = Feedback(
hospital=hospital,
feedback_type=FeedbackType.SUGGESTION,
title=title,
message=message,
category=category_map.get(category, FeedbackCategory.OTHER),
contact_name=contact_name,
contact_phone=contact_phone,
location=location,
main_section=main_section,
subsection=subsection,
is_anonymous=False,
status=FeedbackStatus.SUBMITTED,
metadata={
"source": "public_suggestion_form",
"suggestion_area": category,
},
)
feedback.save()
try:
from apps.feedback.tasks import analyze_suggestion_with_ai
from apps.complaints.tasks import notify_staff_new_item
analyze_suggestion_with_ai.delay(str(feedback.id))
notify_staff_new_item.delay("suggestion", str(feedback.id))
except Exception:
pass
AuditService.log_event(
event_type="public_suggestion_submitted",
description=f"Public suggestion submitted by {contact_name}: {title}",
content_object=feedback,
metadata={"hospital": str(hospital.id), "category": category},
)
return JsonResponse({"success": True, "reference": f"SG-{str(feedback.pk)[:8]}"})
except Exception as e:
logger.exception("ERROR in public_suggestion_submit")
return JsonResponse({"success": False, "message": str(e)}, status=500)
@login_required
@require_http_methods(["POST"])
def feedback_create_action(request, pk):
from django.contrib.contenttypes.models import ContentType
from apps.px_action_center.models import PXAction, ActionSource
feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False)
action_title = request.POST.get("action_title", "").strip()
action_description = request.POST.get("action_description", "").strip()
action_category = request.POST.get("action_category", "other")
action_priority = request.POST.get("action_priority", "medium")
if not action_title:
action_title = f"Action from Suggestion: {feedback.title}"
if not action_description:
ai = feedback.ai_analysis
action_description = ai.get("short_description_en", feedback.message[:500])
feedback_ct = ContentType.objects.get_for_model(Feedback)
action = PXAction.objects.create(
source_type="suggestion",
content_type=feedback_ct,
object_id=feedback.id,
title=action_title,
description=action_description,
hospital=feedback.hospital,
department=feedback.department,
category=action_category,
priority=action_priority,
severity="low",
status="open",
)
AuditService.log_event(
event_type="px_action_created_from_suggestion",
description=f"PX Action created from suggestion: {action.title}",
user=request.user,
content_object=action,
metadata={"suggestion_id": str(feedback.id)},
)
messages.success(request, f"PX Action created successfully from suggestion.")
return redirect("feedback:feedback_detail", pk=feedback.id)