HH/apps/complaints/ui_views_explanation.py
ismail 45b75eb9ef
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m27s
fix: align workflow behavior (Phase 1) + lucide icon-race mitigation
Behavioral consistency across complaint/inquiry/observation (re-run: 0 FAIL):
- activation gate on complaints: complaint_send_to + send_to_department_form now
  reject status=open ("Activate this complaint before sending it to a department")
- observation resolve dead-end fixed: observation_change_status now allows
  hospital_admin (was triage_perm/px_admin only) + added a Resolve action on the
  observation detail page -> accepted dept responses can be resolved from the flow
- status validation: Observation.clean() rejects invalid statuses + a DB
  CheckConstraint (migration 0016 normalizes legacy "new"->"open" first)
- inquiry sent_to_department consistency: inquiry_transfer_to_department now also
  sets sent_to_department=True/At (matches observation/complaint for cross-module queries)

Lucide icon-race mitigation: added a defensive `window.lucide || {createIcons:noop}`
shim to the three base layouts + standalone CDN pages (login, select_hospital,
password reset) so the "lucide is not defined" ReferenceError can't throw during
navigation races. Audit listener classifies any residual occurrence as WARN (cosmetic).

Docs: docs/workflows.md records the intentional complaint vs inquiry/observation
differences (multi-dept join + manager review vs single-dept flat) + the field-name map.

Specs: champion spec now activates before send (matches the new gate).
2026-06-14 23:27:03 +03:00

247 lines
10 KiB
Python

from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.utils.translation import gettext as _
from django.views.decorators.http import require_http_methods
from apps.core.services import AuditService
from apps.organizations.models import Staff
from .models import Complaint, ComplaintExplanation, ComplaintStatus
from .services.complaint_service import ComplaintService, ComplaintServiceError
@login_required
@require_http_methods(["GET", "POST"])
def send_to_department_form(request, pk):
complaint = get_object_or_404(
Complaint.objects.prefetch_related(
"involved_staff__staff__department",
"involved_staff__staff__report_to",
"involved_departments__department__champion",
"involved_departments__department__manager",
),
pk=pk,
)
user = request.user
can_request = (
user.is_px_admin()
or user.is_hospital_admin()
or (user.is_department_manager() and complaint.department == user.department)
or (complaint.hospital == user.hospital)
)
if not can_request:
return HttpResponseForbidden(_("You don't have permission to request explanations."))
if not complaint.is_active_status:
messages.error(
request,
_(
"Cannot request explanation for complaint with status '{}'. Complaint must be Open, In Progress, or Partially Resolved."
).format(complaint.get_status_display()),
)
return redirect("complaints:complaint_detail", pk=complaint.pk)
# Must be activated (in_progress) before it can be sent to a department
if complaint.status == "open":
messages.error(request, _("Activate this complaint before sending it to a department."))
return redirect("complaints:complaint_detail", pk=complaint.pk)
involved_staff = complaint.involved_staff.select_related(
"staff", "staff__department", "staff__report_to"
).all()
department_groups = {}
ungrouped_staff = []
# Build department groups from involved_departments first
involved_departments = complaint.involved_departments.select_related(
"department__champion", "department__manager"
).all()
for dept_inv in involved_departments:
dept = dept_inv.department
if not dept:
continue
dept_key = str(dept.id)
if dept_key not in department_groups:
champion = dept.champion
champion_email = champion.email or (champion.user.email if champion and champion.user else None) if champion else None
dept_manager = dept.manager
department_groups[dept_key] = {
"department_id": dept_key,
"department_name": dept.get_localized_name(),
"champion": champion,
"champion_id": str(champion.id) if champion else None,
"champion_name": champion.get_full_name() if champion else None,
"champion_email": champion_email,
"dept_manager": dept_manager,
"dept_manager_id": str(dept_manager.id) if dept_manager else None,
"dept_manager_name": dept_manager.get_full_name() if dept_manager else None,
"dept_manager_email": dept_manager.email if dept_manager else None,
"staff_list": [],
}
# Add involved staff to their department groups
for staff_inv in involved_staff:
staff = staff_inv.staff
dept = staff.department
entry = {
"staff_inv": staff_inv,
"staff": staff,
"staff_id": str(staff.id),
"staff_name": staff.get_full_name(),
"staff_email": staff.email or (staff.user.email if staff.user else None),
"role": staff_inv.get_role_display(),
}
if dept:
dept_key = str(dept.id)
if dept_key in department_groups:
department_groups[dept_key]["staff_list"].append(entry)
else:
champion = dept.champion
champion_email = champion.email or (champion.user.email if champion and champion.user else None) if champion else None
dept_manager = dept.manager
department_groups[dept_key] = {
"department_id": dept_key,
"department_name": dept.get_localized_name(),
"champion": champion,
"champion_id": str(champion.id) if champion else None,
"champion_name": champion.get_full_name() if champion else None,
"champion_email": champion_email,
"dept_manager": dept_manager,
"dept_manager_id": str(dept_manager.id) if dept_manager else None,
"dept_manager_name": dept_manager.get_full_name() if dept_manager else None,
"dept_manager_email": dept_manager.email if dept_manager else None,
"staff_list": [entry],
}
else:
ungrouped_staff.append(entry)
if not department_groups and not involved_staff.exists():
if not involved_departments.exists():
messages.error(request, _("No departments are involved in this complaint. Please add a department first."))
return redirect("complaints:complaint_detail", pk=complaint.pk)
if request.method == "POST":
action = request.POST.get("action", "send")
selected_dept_ids = request.POST.getlist("selected_departments")
request_message = request.POST.get("request_message", "").strip()
if not selected_dept_ids:
messages.error(request, _("Please select at least one department."))
return render(
request,
"complaints/send_to_department_form.html",
{
"complaint": complaint,
"department_groups": department_groups,
"ungrouped_staff": ungrouped_staff,
},
)
if action == "preview":
preview_depts = []
for dept_id in selected_dept_ids:
dept_info = department_groups.get(dept_id)
if dept_info:
contact_person_id = request.POST.get(f"contact_person_{dept_id}", "")
if contact_person_id:
from apps.organizations.models import Department as DeptModel
dept_obj = DeptModel.objects.filter(pk=dept_id).first()
if dept_obj:
cinfo = dept_obj.is_valid_contact_person(contact_person_id)
if cinfo:
dept_info["contact_person_id"] = contact_person_id
dept_info["contact_person_name"] = cinfo["name"]
dept_info["contact_person_role"] = cinfo["role_label"]
dept_info["contact_person_email"] = cinfo["email"]
dept_info["contact_person_staff"] = cinfo["staff"]
preview_depts.append(dept_info)
contact_person_map = {
d["department_id"]: d.get("contact_person_id", "")
for d in preview_depts
if d.get("contact_person_id")
}
return render(
request,
"complaints/send_to_department_preview.html",
{
"complaint": complaint,
"preview_depts": preview_depts,
"selected_dept_ids": selected_dept_ids,
"request_message": request_message,
"contact_person_map": contact_person_map,
},
)
from django.contrib.sites.shortcuts import get_current_site
contact_person_map = {}
for dept_id in selected_dept_ids:
cp_id = request.POST.get(f"contact_person_{dept_id}", "")
if cp_id:
contact_person_map[dept_id] = cp_id
site = get_current_site(request)
results = ComplaintService.send_to_department(
complaint,
department_groups,
selected_dept_ids,
request_message,
request.user,
site.domain,
request=request,
contact_person_map=contact_person_map,
)
if not complaint.forwarded_to_dept_at:
complaint.forwarded_to_dept_at = timezone.now()
complaint.save(update_fields=["forwarded_to_dept_at"])
if results["champion_count"] == 0 and results["manager_count"] == 0:
if results["skipped_no_email"] > 0:
messages.warning(
request,
_(
"No explanation requests were sent. {} department(s) have champions without email addresses."
).format(results["skipped_no_email"]),
)
else:
messages.warning(
request, _("No explanation requests were sent. Please check department champion configuration.")
)
elif results["champion_count"] == 0 and results["manager_count"] > 0:
messages.warning(
request,
_(
"Only department manager notifications were sent ({}). Champion explanation requests could not be sent."
).format(results["manager_count"]),
)
else:
messages.success(
request,
_("Explanation requests sent! Champions: {}, Department managers notified: {}.").format(
results["champion_count"], results["manager_count"]
),
)
return redirect("complaints:complaint_detail", pk=complaint.pk)
return render(
request,
"complaints/send_to_department_form.html",
{
"complaint": complaint,
"department_groups": department_groups,
"ungrouped_staff": ungrouped_staff,
},
)