""" 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 django.core.cache import cache from apps.accounts.models import User from apps.accounts.services import StaffActivityService from apps.core.services import AuditService from apps.notifications.services import NotificationService from apps.organizations.models import Department, Hospital, Patient, Staff from .models import ( Feedback, FeedbackAttachment, FeedbackResponse, FeedbackStatus, FeedbackType, FeedbackCategory, VALID_FEEDBACK_TRANSITIONS, ) from .forms import ( FeedbackForm, FeedbackResponseForm, FeedbackStatusChangeForm, ) import logging logger = logging.getLogger(__name__) @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) ALLOWED_ORDER_BY = {"-created_at", "created_at", "-updated_at", "updated_at", "-title", "title", "-rating", "rating"} order_by = request.GET.get("order_by", "-created_at") if order_by not in ALLOWED_ORDER_BY: order_by = "-created_at" queryset = queryset.order_by(order_by) page_size = min(int(request.GET.get("page_size", 25)), 100) 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" ) from django.contrib.contenttypes.models import ContentType feedback_ct = ContentType.objects.get_for_model(Feedback) generic_notes = feedback.notes.select_related("created_by").all() context = { "feedback": feedback, "timeline": timeline, "attachments": attachments, "assignable_users": assignable_users, "departments": Department.objects.filter( hospital=feedback.hospital, status="active" ).order_by("name"), "status_choices": FeedbackStatus.choices, "can_activate": feedback.activated_at is None, "is_active": feedback.activated_at is not None, "can_edit": ( (user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee()) and feedback.activated_at is not None ), "can_admin": user.is_px_admin() or user.is_hospital_admin(), "can_send_to_department": ( user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee() ), "linked_rcas": linked_rcas, "content_type_id": feedback_ct.pk, "object_id": feedback.pk, "notes": generic_notes, "notes_count": generic_notes.count(), } # Workflow stepper _fb_steps = [ {"label": "Submitted", "icon": "plus-circle", "done": True}, {"label": "Activated", "icon": "zap", "done": feedback.activated_at is not None}, {"label": "Sent to Dept", "icon": "send", "done": feedback.department_id is not None or feedback.status in ("acknowledged", "closed")}, {"label": "Acknowledged", "icon": "thumbs-up", "done": feedback.status in ("acknowledged", "closed")}, {"label": "Closed", "icon": "check-circle-2", "done": feedback.status == "closed"}, ] _fb_next = next((i for i, s in enumerate(_fb_steps) if not s["done"]), len(_fb_steps)) if _fb_next < len(_fb_steps): _fb_steps[_fb_next]["current"] = True context["workflow_steps"] = _fb_steps return render(request, "feedback/feedback_detail.html", context) @login_required @require_http_methods(["GET", "POST"]) def feedback_create(request): 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 hospital = getattr(request, "tenant_hospital", None) or request.user.hospital 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() title = request.POST.get("title", message[:100]).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 hospital is None: errors.append("No hospital context. Contact your administrator.") if errors: for e in errors: messages.error(request, e) else: try: 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, ) 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.complaints.tasks import notify_staff_new_item from apps.feedback.tasks import analyze_suggestion_with_ai, link_feedback_patient analyze_suggestion_with_ai.delay(str(feedback.id)) notify_staff_new_item.delay("suggestion", str(feedback.id)) link_feedback_patient.delay(str(feedback.id), phone=contact_phone) 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)}") context = { "selected_hospital_id": str(hospital.id) if hospital else "", "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() or user.is_px_management() or user.is_px_employee()): 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, "prefill": { "contact_name": feedback.contact_name or "", "contact_phone": feedback.contact_phone or "", "contact_email": feedback.contact_email or "", "message": feedback.message or "", "title": feedback.title or "", }, "selected_hospital_id": feedback.hospital_id or "", } 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() or user.is_px_management() or user.is_px_employee()): 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() or request.user.is_px_employee()): 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() or request.user.is_px_employee()): 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() or request.user.is_px_employee()): 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() or request.user.is_px_employee()): 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() or request.user.is_px_employee()): 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() or request.user.is_px_employee()): 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) @login_required @require_http_methods(["POST"]) def feedback_activate(request, pk): """Activate a suggestion and auto-assign to the activating user.""" feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False) user = request.user if not ( user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee() ): messages.error(request, _("You don't have permission to activate this suggestion.")) return redirect("feedback:feedback_detail", pk=pk) if feedback.activated_at: messages.warning(request, _("This suggestion has already been activated.")) return redirect("feedback:feedback_detail", pk=pk) feedback.activated_at = timezone.now() feedback.activated_by = user feedback.assigned_to = user feedback.assigned_at = timezone.now() if feedback.status == FeedbackStatus.SUBMITTED: feedback.status = FeedbackStatus.REVIEWED feedback.save() FeedbackResponse.objects.create( feedback=feedback, response_type="note", message=f"Suggestion activated and assigned to {user.get_full_name()}", created_by=user, is_internal=True, ) AuditService.log_event( event_type="suggestion_activated", description=f"Suggestion '{feedback.title}' activated by {user.get_full_name()}", user=user, content_object=feedback, ) messages.success(request, _("Suggestion activated successfully.")) return redirect("feedback:feedback_detail", pk=pk) @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() or user.is_px_management() or user.is_px_employee() ): 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() or user.is_px_management() or user.is_px_employee() ): 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) valid_status_values = [s[0] for s in FeedbackStatus.choices] if new_status not in valid_status_values: messages.error(request, "Invalid status value.") return redirect("feedback:feedback_detail", pk=pk) old_status = feedback.status if old_status == new_status: messages.info(request, "Suggestion is already in this status.") return redirect("feedback:feedback_detail", pk=pk) allowed = VALID_FEEDBACK_TRANSITIONS.get(old_status, []) if new_status not in allowed: messages.error( request, f"Cannot change status from {old_status} to {new_status}. Allowed transitions: {', '.join(allowed)}.", ) return redirect("feedback:feedback_detail", pk=pk) feedback.status = new_status 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 elif new_status == FeedbackStatus.REOPENED: feedback.closed_at = None feedback.closed_by = None 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}, ) # Notify the suggester on acknowledge/close (closed-loop communication). # Only for suggestions with a patient-facing contact channel; internal / # source-user submissions have no patient to notify. if new_status in (FeedbackStatus.ACKNOWLEDGED, FeedbackStatus.CLOSED) and feedback.contact_phone: try: label = "acknowledged" if new_status == FeedbackStatus.ACKNOWLEDGED else "closed" NotificationService.send_sms( feedback.contact_phone, f"Your suggestion (ref {feedback.reference_number}) has been {label}. " f"Thank you for helping us improve.", related_object=feedback, ) except Exception: logger.exception("Failed to send suggestion status-change SMS") 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) user = request.user if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_department_manager() or user.is_px_employee()): messages.error(request, "You don't have permission to add responses to this suggestion.") return redirect("feedback:feedback_detail", pk=pk) 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() or user.is_px_employee()): 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() or user.is_px_employee()): 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__) client_ip = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',')[0].strip() or request.META.get('REMOTE_ADDR', '') cache_key = f"suggestion_rate:{client_ip}" if cache.get(cache_key, 0) >= 5: return JsonResponse({"success": False, "message": "Too many requests. Please try again later."}, status=429) cache.set(cache_key, cache.get(cache_key, 0) + 1, 300) 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) 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, } # Auto-link patient by phone (local first, then HIS) — moved to background task # to avoid blocking the response when the HIS endpoint is slow. feedback = Feedback( patient=None, 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, is_anonymous=False, status=FeedbackStatus.SUBMITTED, metadata={ "source": "public_suggestion_form", "suggestion_area": category, }, ) feedback.save() # Acknowledge receipt to the suggester (closed-loop communication). if contact_phone: try: NotificationService.send_sms( contact_phone, f"Thank you for your suggestion (ref {feedback.reference_number}). " f"We've received it and our team will review it.", related_object=feedback, ) except Exception: logger.exception("Failed to send suggestion acknowledgement SMS") try: from apps.feedback.tasks import analyze_suggestion_with_ai, link_feedback_patient 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)) link_feedback_patient.delay(str(feedback.id), phone=contact_phone) 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) user = request.user if not (user.is_px_admin() or user.is_hospital_admin() or user.is_px_employee()): messages.error(request, "You don't have permission to create actions from suggestions.") return redirect("feedback:feedback_detail", pk=pk) 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("description_en", "") or 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) @login_required @require_http_methods(["POST"]) def feedback_send_to_department(request, pk): """Send suggestion to its department for awareness (no response required).""" from apps.notifications.services import NotificationService from django.utils import timezone feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False) user = request.user if not ( user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee() ): messages.error(request, "You don't have permission to send this suggestion.") return redirect("feedback:feedback_detail", pk=pk) if not feedback.department: messages.error(request, "No department assigned to this suggestion.") return redirect("feedback:feedback_detail", pk=pk) dept = feedback.department note = request.POST.get("note", "").strip() # Notify department champion and/or manager notified = [] for role_attr in ("champion", "manager", "deputy_manager"): staff = getattr(dept, role_attr, None) if staff and staff.email: try: NotificationService.send_email( email=staff.email, subject=f"Suggestion Notification - {feedback.title or 'Untitled'}", message=( f"Dear {staff.get_full_name()},\n\n" f"A suggestion has been logged for your department ({dept.name}):\n\n" f"Title: {feedback.title or 'Untitled'}\n" f"Category: {feedback.get_category_display()}\n" f"Message: {feedback.message[:500]}\n\n" f"This is for your awareness. No response is required.\n\n" f"{'Additional note: ' + note if note else ''}" ), related_object=feedback, user=staff.user if hasattr(staff, "user") and staff.user else None, ) notified.append(staff.get_full_name()) except Exception: pass FeedbackResponse.objects.create( feedback=feedback, response_type="note", message=f"Suggestion sent to {dept.name}" + (f" — notified: {', '.join(notified)}" if notified else ""), created_by=request.user, is_internal=False, ) messages.success(request, f"Suggestion sent to {dept.name}.") return redirect("feedback:feedback_detail", pk=pk) @login_required @require_http_methods(["POST"]) def feedback_send_to(request, pk): """ Unified AJAX endpoint to send feedback/suggestion to a department. Awareness-only: no response required from the department. """ import logging from apps.notifications.services import NotificationService logger = logging.getLogger(__name__) feedback = get_object_or_404(Feedback, pk=pk, is_deleted=False) user = request.user if not ( user.is_px_admin() or user.is_hospital_admin() or user.is_px_management() or user.is_px_employee() ): return JsonResponse( {"success": False, "error": "You don't have permission to send this feedback."}, status=403, ) recipient_type = request.POST.get("recipient_type", "department") note = request.POST.get("note", "").strip() try: if recipient_type == "person": person_id = request.POST.get("person_id") if not person_id: return JsonResponse( {"success": False, "error": "Please select a person."}, status=400, ) try: person = User.objects.get(id=person_id) except User.DoesNotExist: return JsonResponse( {"success": False, "error": "User not found."}, status=400, ) feedback.assigned_to = person feedback.assigned_at = timezone.now() feedback.save(update_fields=["assigned_to", "assigned_at"]) if person.email: try: NotificationService.send_email( email=person.email, subject=f"Feedback Assigned - {feedback.title or 'Untitled'}", message=( f"Dear {person.get_full_name()},\n\n" f"You have been assigned to the following feedback:\n\n" f"Title: {feedback.title or 'Untitled'}\n" f"Type: {feedback.get_feedback_type_display()}\n" f"Message: {feedback.message[:500]}\n\n" f"{'Additional note: ' + note if note else ''}" ), related_object=feedback, user=person, ) except Exception as e: logger.warning(f"Failed to send feedback assignment email: {e}") response_msg = f"Feedback sent to {person.get_full_name()}." else: # department department_id = request.POST.get("department_id") if not department_id: return JsonResponse( {"success": False, "error": "Please select a department."}, status=400, ) try: department = Department.objects.select_related("champion", "manager").get( pk=department_id, status="active" ) except Department.DoesNotExist: return JsonResponse( {"success": False, "error": "Department not found."}, status=400, ) # Auto-target the department's champion and manager (no contact-person picker) from apps.organizations.department_contacts import get_champion_and_manager targets = get_champion_and_manager(department) if not targets: return JsonResponse( { "success": False, "error": f"Cannot send to {department.get_localized_name()}. This department has no champion or manager assigned. Please assign one before sending.", }, status=400, ) # Link department to feedback (awareness-only, no response required) feedback.department = department feedback.save(update_fields=["department", "updated_at"]) # Notify champion + manager (email + SMS) notified = [] link = f"https://{request.get_host()}/suggestions/{feedback.pk}/" for target in targets: display_name = ( target["staff"].get_full_name() if target.get("staff") else (target["user"].get_full_name() if target.get("user") else target["label"]) ) if target.get("email"): try: NotificationService.send_email( email=target["email"], subject=f"Feedback Notification - {feedback.title or 'Untitled'}", message=( f"The following feedback has been logged for your department ({department.name}):\n\n" f"Title: {feedback.title or 'Untitled'}\n" f"Type: {feedback.get_feedback_type_display()}\n" f"Message: {feedback.message[:500]}\n\n" f"This is for your awareness. No response is required.\n\n" f"{'Additional note: ' + note if note else ''}\n\n{link}" ), related_object=feedback, ) except Exception as e: logger.warning(f"Failed to send feedback department notification: {e}") if target.get("phone"): try: NotificationService.send_sms( target["phone"], f"PX360: Feedback '{feedback.title or 'Untitled'}' logged for {department.name}. {link}", related_object=feedback, ) except Exception: pass notified.append(f"{display_name} ({target['label']})") response_msg = f"Feedback sent to {department.get_localized_name()} — {', '.join(notified)}." FeedbackResponse.objects.create( feedback=feedback, response_type="note", message=response_msg, created_by=user, is_internal=False, ) return JsonResponse({"success": True, "message": response_msg}) except Exception as e: logger.error(f"Error in feedback_send_to: {str(e)}") return JsonResponse( {"success": False, "error": "An error occurred while sending the feedback."}, status=500, )