4908 lines
190 KiB
Python
4908 lines
190 KiB
Python
from django.conf import settings
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.core.paginator import Paginator
|
|
from django.db.models import Q
|
|
from django.shortcuts import render, redirect, get_object_or_404
|
|
from django.contrib import messages
|
|
from django.views.decorators.http import require_POST, require_http_methods
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
from django.utils import timezone
|
|
from django.utils.translation import activate, get_language, gettext as _
|
|
from django.urls import reverse
|
|
from apps.core.decorators import block_source_user, hospital_admin_required
|
|
|
|
from .models import Department, DepartmentCategory, Hospital, Organization, Patient, Staff, StaffSection, StaffSubsection, Section, SubSection, OrgSubSection
|
|
from apps.accounts.models import User
|
|
from .forms import StaffForm, PatientForm
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def hospital_list(request):
|
|
"""Hospitals list view"""
|
|
queryset = Hospital.objects.all()
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if user.is_px_admin():
|
|
# PX Admins see only their selected tenant hospital
|
|
if request.tenant_hospital:
|
|
queryset = queryset.filter(pk=request.tenant_hospital.pk)
|
|
else:
|
|
queryset = queryset.none()
|
|
elif user.hospital:
|
|
queryset = queryset.filter(pk=user.hospital_id)
|
|
|
|
# Apply filters
|
|
hospital_filter = request.GET.get("hospital")
|
|
if hospital_filter:
|
|
queryset = queryset.filter(pk=hospital_filter)
|
|
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
# Search
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=search_query) | Q(name_ar__icontains=search_query) | Q(code__icontains=search_query)
|
|
)
|
|
|
|
# Ordering
|
|
queryset = queryset.order_by("name")
|
|
|
|
# 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 hospitals for filter
|
|
hospitals = Hospital.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
hospitals = hospitals.filter(id=user.hospital.id)
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"departments": page_obj.object_list,
|
|
"hospitals": hospitals,
|
|
"filters": request.GET,
|
|
}
|
|
|
|
return render(request, "organizations/department_list.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def staff_list(request):
|
|
"""Staff list view - filtered by tenant hospital"""
|
|
lang = request.session.get("django_language", get_language())
|
|
activate(lang)
|
|
queryset = Staff.objects.select_related("hospital", "department", "user")
|
|
|
|
# Always filter by tenant_hospital (set by TenantMiddleware)
|
|
# This handles both PX admins (with selected hospital) and regular users
|
|
if request.tenant_hospital:
|
|
queryset = queryset.filter(hospital=request.tenant_hospital)
|
|
else:
|
|
queryset = queryset.none() # No access without tenant hospital context
|
|
|
|
# Apply filters
|
|
department_filter = request.GET.get("department")
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
staff_type_filter = request.GET.get("department_type")
|
|
if staff_type_filter:
|
|
queryset = queryset.filter(department_type=staff_type_filter)
|
|
|
|
# is_head filter
|
|
is_head_filter = request.GET.get("is_head")
|
|
if is_head_filter:
|
|
if is_head_filter.lower() == "true":
|
|
queryset = queryset.filter(is_head=True)
|
|
elif is_head_filter.lower() == "false":
|
|
queryset = queryset.filter(is_head=False)
|
|
|
|
# Filter by department ForeignKey
|
|
department_filter = request.GET.get("department")
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
section_filter = request.GET.get("section")
|
|
if section_filter:
|
|
queryset = queryset.filter(section__icontains=section_filter)
|
|
|
|
subsection_filter = request.GET.get("subsection")
|
|
if subsection_filter:
|
|
queryset = queryset.filter(subsection__icontains=subsection_filter)
|
|
|
|
# Search
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(first_name__icontains=search_query)
|
|
| Q(last_name__icontains=search_query)
|
|
| Q(employee_id__icontains=search_query)
|
|
| Q(license_number__icontains=search_query)
|
|
| Q(specialization__icontains=search_query)
|
|
| Q(job_title__icontains=search_query)
|
|
)
|
|
|
|
# Ordering
|
|
queryset = queryset.order_by("last_name", "first_name")
|
|
|
|
# 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 departments for filter dropdown (from current hospital context)
|
|
if request.tenant_hospital:
|
|
departments = Department.objects.filter(hospital=request.tenant_hospital, status="active").order_by("name")
|
|
else:
|
|
departments = Department.objects.filter(status="active").order_by("name")
|
|
|
|
# Get unique values for section/subsection filters
|
|
base_queryset = Staff.objects.select_related("hospital", "department", "user")
|
|
if request.tenant_hospital:
|
|
base_queryset = base_queryset.filter(hospital=request.tenant_hospital)
|
|
|
|
sections = list(
|
|
base_queryset.exclude(section="").values_list("section", "section_ar").distinct().order_by("section")
|
|
)
|
|
sections = [(s[0], s[1] if lang == "ar" and s[1] else s[0]) for s in sections]
|
|
subsections = (
|
|
base_queryset.exclude(subsection="")
|
|
.values_list("subsection", "subsection_ar")
|
|
.distinct()
|
|
.order_by("subsection")
|
|
)
|
|
subsections = [(s[0], s[1] if lang == "ar" and s[1] else s[0]) for s in subsections]
|
|
|
|
context = {
|
|
"staff": page_obj,
|
|
"filters": request.GET,
|
|
"departments": departments,
|
|
"sections": sections,
|
|
"subsections": subsections,
|
|
}
|
|
|
|
return render(request, "organizations/staff_list.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def organization_list(request):
|
|
"""Organizations list view"""
|
|
queryset = Organization.objects.all()
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if not user.is_px_admin() and user.hospital and user.hospital.organization:
|
|
queryset = queryset.filter(id=user.hospital.organization.id)
|
|
|
|
# Apply filters
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
city_filter = request.GET.get("city")
|
|
if city_filter:
|
|
queryset = queryset.filter(city__icontains=city_filter)
|
|
|
|
# Search
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=search_query)
|
|
| Q(name_ar__icontains=search_query)
|
|
| Q(code__icontains=search_query)
|
|
| Q(license_number__icontains=search_query)
|
|
)
|
|
|
|
# Ordering
|
|
queryset = queryset.order_by("name")
|
|
|
|
# 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)
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"organizations": page_obj.object_list,
|
|
"filters": request.GET,
|
|
}
|
|
|
|
return render(request, "organizations/organization_list.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def organization_detail(request, pk):
|
|
"""Organization detail view"""
|
|
organization = Organization.objects.get(pk=pk)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if not user.is_px_admin():
|
|
if user.hospital and user.hospital.organization:
|
|
if organization.id != user.hospital.organization.id:
|
|
# User doesn't have access to this organization
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to view this organization")
|
|
else:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to view this organization")
|
|
|
|
hospitals = organization.hospitals.all()
|
|
|
|
context = {
|
|
"organization": organization,
|
|
"hospitals": hospitals,
|
|
}
|
|
|
|
return render(request, "organizations/organization_detail.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def organization_create(request):
|
|
"""Create organization view"""
|
|
# Only PX Admins can create organizations
|
|
user = request.user
|
|
if not user.is_px_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("Only PX Admins can create organizations")
|
|
|
|
if request.method == "POST":
|
|
name = request.POST.get("name")
|
|
name_ar = request.POST.get("name_ar")
|
|
code = request.POST.get("code")
|
|
address = request.POST.get("address", "")
|
|
city = request.POST.get("city", "")
|
|
phone = request.POST.get("phone", "")
|
|
email = request.POST.get("email", "")
|
|
website = request.POST.get("website", "")
|
|
license_number = request.POST.get("license_number", "")
|
|
status = request.POST.get("status", "active")
|
|
|
|
if name and code:
|
|
organization = Organization.objects.create(
|
|
name=name,
|
|
name_ar=name_ar or name,
|
|
code=code,
|
|
address=address,
|
|
city=city,
|
|
phone=phone,
|
|
email=email,
|
|
website=website,
|
|
license_number=license_number,
|
|
status=status,
|
|
)
|
|
|
|
# Redirect to organization detail
|
|
from django.shortcuts import redirect
|
|
|
|
return redirect("organizations:organization_detail", pk=organization.id)
|
|
|
|
return render(request, "organizations/organization_form.html")
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def patient_list(request):
|
|
"""Patients list view"""
|
|
queryset = Patient.objects.select_related("primary_hospital")
|
|
|
|
# Filter by current hospital context
|
|
user = request.user
|
|
if request.tenant_hospital:
|
|
queryset = queryset.filter(primary_hospital=request.tenant_hospital)
|
|
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
gender_filter = request.GET.get("gender")
|
|
if gender_filter:
|
|
queryset = queryset.filter(gender=gender_filter)
|
|
|
|
nationality_filter = request.GET.get("nationality")
|
|
if nationality_filter:
|
|
queryset = queryset.filter(nationality=nationality_filter)
|
|
|
|
# Search
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
from apps.core.encryption import compute_national_id_hash
|
|
|
|
nid_hash = compute_national_id_hash(search_query)
|
|
queryset = queryset.filter(
|
|
Q(mrn__icontains=search_query)
|
|
| Q(first_name__icontains=search_query)
|
|
| Q(last_name__icontains=search_query)
|
|
| Q(national_id_hash=nid_hash)
|
|
| Q(phone__icontains=search_query)
|
|
)
|
|
|
|
# Ordering
|
|
order_by = request.GET.get("order_by", "last_name")
|
|
if order_by not in ["last_name", "-last_name", "-created_at", "created_at"]:
|
|
order_by = "last_name"
|
|
queryset = queryset.order_by(order_by, "first_name")
|
|
|
|
# Stats (computed from filtered queryset)
|
|
stats = {
|
|
"total": queryset.count(),
|
|
"active": queryset.filter(status="active").count(),
|
|
"hospitals": queryset.values("primary_hospital").distinct().count(),
|
|
"visits": 0,
|
|
}
|
|
|
|
# Export CSV
|
|
if request.GET.get("export") == "csv":
|
|
import csv
|
|
from django.http import HttpResponse
|
|
|
|
response = HttpResponse(content_type="text/csv")
|
|
response["Content-Disposition"] = 'attachment; filename="patients.csv"'
|
|
writer = csv.writer(response)
|
|
writer.writerow(
|
|
[
|
|
"MRN",
|
|
"First Name",
|
|
"Last Name",
|
|
"National ID",
|
|
"Gender",
|
|
"Nationality",
|
|
"Phone",
|
|
"Email",
|
|
"Hospital",
|
|
"Status",
|
|
"Created At",
|
|
]
|
|
)
|
|
for p in queryset[:10000]:
|
|
writer.writerow(
|
|
[
|
|
p.mrn,
|
|
p.first_name,
|
|
p.last_name,
|
|
p.get_masked_national_id(),
|
|
p.get_gender_display(),
|
|
p.nationality,
|
|
p.phone,
|
|
p.email,
|
|
p.primary_hospital.name if p.primary_hospital else "",
|
|
p.get_status_display(),
|
|
p.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
]
|
|
)
|
|
return response
|
|
|
|
# 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 nationalities for filter
|
|
nationalities = (
|
|
Patient.objects.exclude(nationality="").values_list("nationality", flat=True).order_by("nationality").distinct()
|
|
)
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"patients": page_obj.object_list,
|
|
"nationalities": nationalities,
|
|
"filters": request.GET,
|
|
"stats": stats,
|
|
}
|
|
|
|
return render(request, "organizations/patient_list.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def staff_detail(request, pk):
|
|
"""Staff detail view"""
|
|
staff = get_object_or_404(Staff.objects.select_related("user", "hospital", "department"), pk=pk)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if not user.is_px_admin() and staff.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to view this staff member")
|
|
|
|
from apps.complaints.models import Complaint, ComplaintInvolvedStaff
|
|
|
|
direct_complaints = Complaint.objects.filter(staff=staff).select_related(
|
|
"patient", "hospital", "department", "assigned_to"
|
|
)
|
|
|
|
involved_records = ComplaintInvolvedStaff.objects.filter(staff=staff).select_related(
|
|
"complaint__patient", "complaint__hospital", "complaint__department", "complaint__assigned_to"
|
|
)
|
|
|
|
complaint_rows = []
|
|
seen_ids = set()
|
|
|
|
for c in direct_complaints:
|
|
complaint_rows.append({"complaint": c, "role": _("Primary")})
|
|
seen_ids.add(c.id)
|
|
|
|
for inv in involved_records:
|
|
if inv.complaint_id not in seen_ids:
|
|
complaint_rows.append({"complaint": inv.complaint, "role": inv.get_role_display()})
|
|
seen_ids.add(inv.complaint_id)
|
|
|
|
complaint_rows.sort(key=lambda r: r["complaint"].created_at, reverse=True)
|
|
|
|
all_complaints = [r["complaint"] for r in complaint_rows]
|
|
complaint_counts = {
|
|
"total": len(all_complaints),
|
|
"open": sum(1 for c in all_complaints if c.status == "open"),
|
|
"in_progress": sum(1 for c in all_complaints if c.status == "in_progress"),
|
|
"resolved": sum(1 for c in all_complaints if c.status in ("resolved", "partially_resolved")),
|
|
"closed": sum(1 for c in all_complaints if c.status == "closed"),
|
|
}
|
|
|
|
context = {
|
|
"staff": staff,
|
|
"complaint_rows": complaint_rows,
|
|
"complaint_counts": complaint_counts,
|
|
}
|
|
|
|
if staff.user and (user.is_px_admin() or user.is_hospital_admin()):
|
|
from apps.accounts.models import StaffActivityLog
|
|
|
|
staff_activities = (
|
|
StaffActivityLog.objects.filter(user=staff.user)
|
|
.select_related("content_type")
|
|
.order_by("-created_at")[:50]
|
|
)
|
|
staff_login_count = StaffActivityLog.objects.filter(user=staff.user, activity_type="login").count()
|
|
staff_action_count = (
|
|
StaffActivityLog.objects.filter(user=staff.user).exclude(activity_type__in=["login", "logout"]).count()
|
|
)
|
|
context["staff_activities"] = staff_activities
|
|
context["staff_login_count"] = staff_login_count
|
|
context["staff_action_count"] = staff_action_count
|
|
|
|
return render(request, "organizations/staff_detail.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def staff_complaints_export(request, pk):
|
|
"""Export complaints related to a staff member as Excel"""
|
|
from django.http import HttpResponseForbidden
|
|
|
|
from openpyxl import Workbook
|
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
|
from openpyxl.utils import get_column_letter
|
|
|
|
from apps.complaints.models import Complaint, ComplaintInvolvedStaff
|
|
|
|
staff = get_object_or_404(Staff, pk=pk)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and staff.hospital != user.hospital:
|
|
return HttpResponseForbidden("You don't have permission")
|
|
|
|
direct_complaints = Complaint.objects.filter(staff=staff).select_related(
|
|
"patient", "hospital", "department"
|
|
)
|
|
involved_records = ComplaintInvolvedStaff.objects.filter(staff=staff).select_related(
|
|
"complaint__patient", "complaint__hospital", "complaint__department"
|
|
)
|
|
|
|
rows = []
|
|
seen_ids = set()
|
|
for c in direct_complaints:
|
|
rows.append((c, _("Primary")))
|
|
seen_ids.add(c.id)
|
|
for inv in involved_records:
|
|
if inv.complaint_id not in seen_ids:
|
|
rows.append((inv.complaint, inv.get_role_display()))
|
|
seen_ids.add(inv.complaint_id)
|
|
|
|
wb = Workbook()
|
|
ws = wb.active
|
|
ws.title = "Staff Complaints"
|
|
|
|
header_font = Font(bold=True, color="FFFFFF")
|
|
header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
|
|
header_alignment = Alignment(horizontal="center", vertical="center")
|
|
thin_border = Border(
|
|
left=Side(style="thin"),
|
|
right=Side(style="thin"),
|
|
top=Side(style="thin"),
|
|
bottom=Side(style="thin"),
|
|
)
|
|
|
|
headers = [
|
|
"Reference #",
|
|
"Title",
|
|
"Patient Name",
|
|
"Hospital",
|
|
"Department",
|
|
"Status",
|
|
"Severity",
|
|
"Priority",
|
|
"Role",
|
|
"Source",
|
|
"Created At",
|
|
"Due At",
|
|
"Resolved At",
|
|
"Description",
|
|
]
|
|
|
|
for col_num, header in enumerate(headers, 1):
|
|
cell = ws.cell(row=1, column=col_num, value=header)
|
|
cell.font = header_font
|
|
cell.fill = header_fill
|
|
cell.alignment = header_alignment
|
|
cell.border = thin_border
|
|
|
|
for row_num, (complaint, role) in enumerate(rows, 2):
|
|
values = [
|
|
complaint.reference_number or "",
|
|
complaint.title,
|
|
complaint.patient.get_full_name() if complaint.patient else complaint.patient_name or "",
|
|
complaint.hospital.name if complaint.hospital else "",
|
|
complaint.department.name if complaint.department else "",
|
|
complaint.get_status_display(),
|
|
complaint.get_severity_display(),
|
|
complaint.get_priority_display(),
|
|
str(role),
|
|
complaint.get_complaint_source_type_display(),
|
|
complaint.created_at.strftime("%Y-%m-%d %H:%M") if complaint.created_at else "",
|
|
complaint.due_at.strftime("%Y-%m-%d %H:%M") if complaint.due_at else "",
|
|
complaint.resolved_at.strftime("%Y-%m-%d %H:%M") if complaint.resolved_at else "",
|
|
complaint.description[:500] if complaint.description else "",
|
|
]
|
|
for col_num, value in enumerate(values, 1):
|
|
cell = ws.cell(row=row_num, column=col_num, value=value)
|
|
cell.border = thin_border
|
|
|
|
for col_num in range(1, len(headers) + 1):
|
|
max_length = max(
|
|
len(str(ws.cell(row=r, column=col_num).value or ""))
|
|
for r in range(1, min(len(rows) + 2, 100))
|
|
)
|
|
ws.column_dimensions[get_column_letter(col_num)].width = min(max_length + 4, 50)
|
|
|
|
response = HttpResponse(
|
|
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
)
|
|
response["Content-Disposition"] = (
|
|
f'attachment; filename="staff_{staff.employee_id or staff.id}_complaints_{timezone.now().strftime("%Y%m%d_%H%M%S")}.xlsx"'
|
|
)
|
|
wb.save(response)
|
|
return response
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def staff_create(request):
|
|
"""Create staff view"""
|
|
# Only PX Admins and Hospital Admins can create staff
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to create staff")
|
|
|
|
if request.method == "POST":
|
|
form = StaffForm(request.POST, request=request)
|
|
if form.is_valid():
|
|
staff = form.save(commit=False)
|
|
|
|
# Handle user account creation
|
|
create_user = request.POST.get("create_user") == "on"
|
|
if create_user and not staff.user and staff.email:
|
|
from .services import StaffService
|
|
|
|
role = request.POST.get("user_role", "staff")
|
|
allowed_roles = ["staff", "px_employee", "hospital_admin", "department_manager"]
|
|
if role not in allowed_roles:
|
|
role = "staff"
|
|
|
|
try:
|
|
user_account, was_created, password = StaffService.create_user_for_staff(
|
|
staff, role=role, request=request
|
|
)
|
|
if was_created and user_account:
|
|
try:
|
|
StaffService.send_password_reset_email(staff, request)
|
|
messages.success(request, "Staff member created and password reset email sent successfully.")
|
|
except Exception as e:
|
|
messages.warning(request, f"Staff member created but email sending failed: {str(e)}")
|
|
elif not was_created:
|
|
messages.success(request, "Existing user account linked successfully.")
|
|
except Exception as e:
|
|
messages.error(request, f"Staff member created but user account creation failed: {str(e)}")
|
|
|
|
staff.save()
|
|
|
|
# Send invitation email if requested
|
|
if create_user and staff.user and request.POST.get("send_email") != "false":
|
|
from .services import StaffService
|
|
|
|
try:
|
|
StaffService.send_password_reset_email(staff, request)
|
|
messages.success(request, "Password reset email sent successfully.")
|
|
except Exception as e:
|
|
messages.warning(request, f"Email sending failed: {str(e)}")
|
|
|
|
messages.success(request, "Staff member created successfully.")
|
|
return redirect("organizations:staff_detail", pk=staff.id)
|
|
else:
|
|
form = StaffForm(request=request)
|
|
|
|
context = {
|
|
"form": form,
|
|
}
|
|
|
|
return render(request, "organizations/staff_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def staff_update(request, pk):
|
|
"""Update staff view"""
|
|
staff = get_object_or_404(Staff.objects.select_related("user"), pk=pk)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to update this staff member")
|
|
|
|
if user.is_hospital_admin() and staff.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to update this staff member")
|
|
|
|
if request.method == "POST":
|
|
form = StaffForm(request.POST, instance=staff)
|
|
if form.is_valid():
|
|
staff = form.save(commit=False)
|
|
|
|
# Handle user account creation
|
|
create_user = request.POST.get("create_user") == "on"
|
|
if create_user and not staff.user and staff.email:
|
|
from .services import StaffService
|
|
|
|
role = request.POST.get("user_role", "staff")
|
|
allowed_roles = ["staff", "px_employee", "hospital_admin", "department_manager"]
|
|
if role not in allowed_roles:
|
|
role = "staff"
|
|
|
|
try:
|
|
user_account, was_created, password = StaffService.create_user_for_staff(
|
|
staff, role=role, request=request
|
|
)
|
|
if was_created and user_account:
|
|
try:
|
|
StaffService.send_password_reset_email(staff, request)
|
|
messages.success(request, "User account created and password reset email sent.")
|
|
except Exception as e:
|
|
messages.warning(request, f"User account created but email sending failed: {str(e)}")
|
|
elif not was_created:
|
|
messages.success(request, "Existing user account linked successfully.")
|
|
except Exception as e:
|
|
messages.error(request, f"User account creation failed: {str(e)}")
|
|
|
|
staff.save()
|
|
|
|
messages.success(request, "Staff member updated successfully.")
|
|
return redirect("organizations:staff_detail", pk=staff.id)
|
|
else:
|
|
form = StaffForm(instance=staff, request=request)
|
|
|
|
context = {
|
|
"form": form,
|
|
"staff": staff,
|
|
}
|
|
|
|
return render(request, "organizations/staff_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def staff_hierarchy(request):
|
|
"""
|
|
Staff hierarchy table view
|
|
Shows organizational structure based on report_to relationships
|
|
"""
|
|
queryset = Staff.objects.select_related("hospital", "department", "report_to")
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
|
|
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)
|
|
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(employee_id__icontains=search_query)
|
|
| Q(first_name__icontains=search_query)
|
|
| Q(last_name__icontains=search_query)
|
|
| Q(first_name_ar__icontains=search_query)
|
|
| Q(last_name_ar__icontains=search_query)
|
|
)
|
|
|
|
queryset = queryset.order_by("hospital__name", "department__name", "first_name")
|
|
|
|
total_staff = queryset.count()
|
|
top_managers = queryset.filter(report_to__isnull=True).count()
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
context = {
|
|
"staff": page_obj,
|
|
"hospitals": hospitals,
|
|
"departments": departments,
|
|
"filters": request.GET,
|
|
"total_staff": total_staff,
|
|
"top_managers": top_managers,
|
|
}
|
|
|
|
return render(request, "organizations/staff_hierarchy.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def staff_hierarchy_d3(request):
|
|
"""
|
|
Staff hierarchy D3 visualization view
|
|
Shows interactive organizational chart using D3.js
|
|
"""
|
|
# Get hospitals for filter (used by client-side filters)
|
|
hospitals = Hospital.objects.filter(status="active")
|
|
user = request.user
|
|
if user.is_px_admin() and hasattr(request, "tenant_hospital") and request.tenant_hospital:
|
|
hospitals = hospitals.filter(id=request.tenant_hospital.id)
|
|
elif not user.is_px_admin() and user.hospital:
|
|
hospitals = hospitals.filter(id=user.hospital.id)
|
|
|
|
context = {
|
|
"hospitals": hospitals,
|
|
}
|
|
|
|
return render(request, "organizations/staff_hierarchy_d3.html", context)
|
|
|
|
|
|
# ==================== Department CRUD ====================
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def department_create(request):
|
|
"""Create department view"""
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to create departments")
|
|
|
|
if request.method == "POST":
|
|
name = request.POST.get("name")
|
|
name_ar = request.POST.get("name_ar", "")
|
|
code = request.POST.get("code")
|
|
hospital_id = request.POST.get("hospital")
|
|
category = request.POST.get("category", "")
|
|
status = request.POST.get("status", "active")
|
|
phone = request.POST.get("phone", "")
|
|
email = request.POST.get("email", "")
|
|
location = request.POST.get("location", "")
|
|
manager_id = request.POST.get("manager", "")
|
|
|
|
if name and code and hospital_id:
|
|
# RBAC: Non-admins can only create in their hospital
|
|
if not user.is_px_admin():
|
|
if str(user.hospital_id) != hospital_id:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You can only create departments in your hospital")
|
|
|
|
department = Department.objects.create(
|
|
name=name,
|
|
name_ar=name_ar or name,
|
|
code=code,
|
|
hospital_id=hospital_id,
|
|
category=category,
|
|
status=status,
|
|
phone=phone,
|
|
email=email,
|
|
location=location,
|
|
manager_id=manager_id if manager_id else None,
|
|
)
|
|
messages.success(request, "Department created successfully.")
|
|
return redirect("organizations:department_list")
|
|
|
|
# Get hospitals for dropdown
|
|
hospitals = Hospital.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
hospitals = hospitals.filter(id=user.hospital.id)
|
|
|
|
# Get head staff for manager dropdown
|
|
hospital_filter = hospitals.first()
|
|
managers = Staff.objects.none()
|
|
if hospital_filter:
|
|
managers = Staff.objects.filter(
|
|
is_head=True, hospital=hospital_filter, status="active", user__isnull=False
|
|
).select_related("user").order_by("first_name", "last_name")
|
|
|
|
context = {
|
|
"hospitals": hospitals,
|
|
"managers": managers,
|
|
}
|
|
return render(request, "organizations/department_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def department_update(request, pk):
|
|
"""Update department view"""
|
|
department = get_object_or_404(Department, pk=pk)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to update departments")
|
|
|
|
if not user.is_px_admin() and department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You can only update departments in your hospital")
|
|
|
|
if request.method == "POST":
|
|
department.name = request.POST.get("name", department.name)
|
|
department.name_ar = request.POST.get("name_ar", "")
|
|
department.code = request.POST.get("code", department.code)
|
|
department.category = request.POST.get("category", "")
|
|
department.status = request.POST.get("status", department.status)
|
|
department.phone = request.POST.get("phone", "")
|
|
department.email = request.POST.get("email", "")
|
|
department.location = request.POST.get("location", "")
|
|
manager_id = request.POST.get("manager", "")
|
|
department.manager_id = manager_id if manager_id else None
|
|
department.save()
|
|
|
|
CATEGORY_TO_STAFF_TYPE = {
|
|
"nursing": "nurse",
|
|
"medical": "physician",
|
|
"non_medical": "admin",
|
|
"support_services": "other",
|
|
}
|
|
new_category = department.category
|
|
if new_category in CATEGORY_TO_STAFF_TYPE:
|
|
updated = Staff.objects.filter(
|
|
department=department, status="active"
|
|
).update(department_type=new_category)
|
|
if updated:
|
|
messages.info(
|
|
request,
|
|
f"{updated} staff department_type updated to {new_category}.",
|
|
)
|
|
|
|
messages.success(request, "Department updated successfully.")
|
|
return redirect("organizations:department_list")
|
|
|
|
managers = Staff.objects.filter(
|
|
is_head=True, hospital=department.hospital, status="active", user__isnull=False
|
|
).select_related("user").order_by("first_name", "last_name")
|
|
|
|
context = {
|
|
"department": department,
|
|
"managers": managers,
|
|
}
|
|
return render(request, "organizations/department_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def department_delete(request, pk):
|
|
"""Delete department view"""
|
|
department = get_object_or_404(Department, pk=pk)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to delete departments")
|
|
|
|
if not user.is_px_admin() and department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You can only delete departments in your hospital")
|
|
|
|
if request.method == "POST":
|
|
# Check for linked staff
|
|
staff_count = department.staff.count()
|
|
if staff_count > 0:
|
|
messages.error(request, f"Cannot delete department. {staff_count} staff members are assigned to it.")
|
|
return redirect("organizations:department_list")
|
|
|
|
department.delete()
|
|
messages.success(request, "Department deleted successfully.")
|
|
return redirect("organizations:department_list")
|
|
|
|
context = {
|
|
"department": department,
|
|
}
|
|
return render(request, "organizations/department_confirm_delete.html", context)
|
|
|
|
|
|
# ==================== Staff Section CRUD ====================
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def section_list(request):
|
|
"""Sections list view"""
|
|
queryset = StaffSection.objects.select_related("department", "department__hospital", "head")
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if not user.is_px_admin() and user.hospital:
|
|
queryset = queryset.filter(department__hospital=user.hospital)
|
|
|
|
# Apply filters
|
|
department_filter = request.GET.get("department")
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
# Search
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=search_query) | Q(name_ar__icontains=search_query) | Q(code__icontains=search_query)
|
|
)
|
|
|
|
# Ordering
|
|
queryset = queryset.order_by("department__name", "name")
|
|
|
|
# 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 departments for filter
|
|
departments = Department.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"sections": page_obj.object_list,
|
|
"departments": departments,
|
|
"filters": request.GET,
|
|
}
|
|
|
|
return render(request, "organizations/section_list.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def section_create(request):
|
|
"""Create section view"""
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to create sections")
|
|
|
|
if request.method == "POST":
|
|
name = request.POST.get("name")
|
|
name_ar = request.POST.get("name_ar", "")
|
|
code = request.POST.get("code", "")
|
|
department_id = request.POST.get("department")
|
|
status = request.POST.get("status", "active")
|
|
head_id = request.POST.get("head")
|
|
|
|
if name and department_id:
|
|
department = get_object_or_404(Department, pk=department_id)
|
|
|
|
# RBAC check
|
|
if not user.is_px_admin() and department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You can only create sections in your hospital")
|
|
|
|
section = StaffSection.objects.create(
|
|
name=name,
|
|
name_ar=name_ar or name,
|
|
code=code,
|
|
department=department,
|
|
status=status,
|
|
head_id=head_id if head_id else None,
|
|
)
|
|
messages.success(request, "Section created successfully.")
|
|
return redirect("organizations:section_list")
|
|
|
|
# Get departments for dropdown
|
|
departments = Department.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
|
|
context = {
|
|
"departments": departments,
|
|
}
|
|
return render(request, "organizations/section_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def section_update(request, pk):
|
|
"""Update section view"""
|
|
section = get_object_or_404(StaffSection, pk=pk)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to update sections")
|
|
|
|
if not user.is_px_admin() and section.department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You can only update sections in your hospital")
|
|
|
|
if request.method == "POST":
|
|
section.name = request.POST.get("name", section.name)
|
|
section.name_ar = request.POST.get("name_ar", "")
|
|
section.code = request.POST.get("code", "")
|
|
section.status = request.POST.get("status", section.status)
|
|
head_id = request.POST.get("head")
|
|
section.head_id = head_id if head_id else None
|
|
section.save()
|
|
|
|
messages.success(request, "Section updated successfully.")
|
|
return redirect("organizations:section_list")
|
|
|
|
# Get departments for dropdown
|
|
departments = Department.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
|
|
context = {
|
|
"section": section,
|
|
"departments": departments,
|
|
}
|
|
return render(request, "organizations/section_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def section_delete(request, pk):
|
|
"""Delete section view"""
|
|
section = get_object_or_404(StaffSection, pk=pk)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to delete sections")
|
|
|
|
if not user.is_px_admin() and section.department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You can only delete sections in your hospital")
|
|
|
|
if request.method == "POST":
|
|
subsection_count = section.subsections.count()
|
|
if subsection_count > 0:
|
|
messages.error(request, f"Cannot delete section. {subsection_count} subsections are linked to it.")
|
|
return redirect("organizations:section_list")
|
|
|
|
section.delete()
|
|
messages.success(request, "Section deleted successfully.")
|
|
return redirect("organizations:section_list")
|
|
|
|
context = {
|
|
"section": section,
|
|
}
|
|
return render(request, "organizations/section_confirm_delete.html", context)
|
|
|
|
|
|
# ==================== Staff Subsection CRUD ====================
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def subsection_list(request):
|
|
"""Subsections list view"""
|
|
queryset = StaffSubsection.objects.select_related(
|
|
"section", "section__department", "section__department__hospital", "head"
|
|
)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if not user.is_px_admin() and user.hospital:
|
|
queryset = queryset.filter(section__department__hospital=user.hospital)
|
|
|
|
# Apply filters
|
|
section_filter = request.GET.get("section")
|
|
if section_filter:
|
|
queryset = queryset.filter(section_id=section_filter)
|
|
|
|
department_filter = request.GET.get("department")
|
|
if department_filter:
|
|
queryset = queryset.filter(section__department_id=department_filter)
|
|
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
# Search
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=search_query) | Q(name_ar__icontains=search_query) | Q(code__icontains=search_query)
|
|
)
|
|
|
|
# Ordering
|
|
queryset = queryset.order_by("section__name", "name")
|
|
|
|
# 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 sections and departments for filter
|
|
departments = Department.objects.filter(status="active")
|
|
sections = StaffSection.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
sections = sections.filter(department__hospital=user.hospital)
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"subsections": page_obj.object_list,
|
|
"sections": sections,
|
|
"departments": departments,
|
|
"filters": request.GET,
|
|
}
|
|
|
|
return render(request, "organizations/subsection_list.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def subsection_create(request):
|
|
"""Create subsection view"""
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to create subsections")
|
|
|
|
if request.method == "POST":
|
|
name = request.POST.get("name")
|
|
name_ar = request.POST.get("name_ar", "")
|
|
code = request.POST.get("code", "")
|
|
section_id = request.POST.get("section")
|
|
status = request.POST.get("status", "active")
|
|
head_id = request.POST.get("head")
|
|
|
|
if name and section_id:
|
|
section = get_object_or_404(StaffSection, pk=section_id)
|
|
|
|
# RBAC check
|
|
if not user.is_px_admin() and section.department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You can only create subsections in your hospital")
|
|
|
|
subsection = StaffSubsection.objects.create(
|
|
name=name,
|
|
name_ar=name_ar or name,
|
|
code=code,
|
|
section=section,
|
|
status=status,
|
|
head_id=head_id if head_id else None,
|
|
)
|
|
messages.success(request, "Subsection created successfully.")
|
|
return redirect("organizations:subsection_list")
|
|
|
|
# Get sections for dropdown
|
|
sections = StaffSection.objects.filter(status="active").select_related("department")
|
|
if not user.is_px_admin() and user.hospital:
|
|
sections = sections.filter(department__hospital=user.hospital)
|
|
|
|
context = {
|
|
"sections": sections,
|
|
}
|
|
return render(request, "organizations/subsection_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def subsection_update(request, pk):
|
|
"""Update subsection view"""
|
|
subsection = get_object_or_404(StaffSubsection, pk=pk)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to update subsections")
|
|
|
|
if not user.is_px_admin() and subsection.section.department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You can only update subsections in your hospital")
|
|
|
|
if request.method == "POST":
|
|
subsection.name = request.POST.get("name", subsection.name)
|
|
subsection.name_ar = request.POST.get("name_ar", "")
|
|
subsection.code = request.POST.get("code", "")
|
|
subsection.status = request.POST.get("status", subsection.status)
|
|
head_id = request.POST.get("head")
|
|
subsection.head_id = head_id if head_id else None
|
|
subsection.save()
|
|
|
|
messages.success(request, "Subsection updated successfully.")
|
|
return redirect("organizations:subsection_list")
|
|
|
|
# Get sections for dropdown
|
|
sections = StaffSection.objects.filter(status="active").select_related("department")
|
|
if not user.is_px_admin() and user.hospital:
|
|
sections = sections.filter(department__hospital=user.hospital)
|
|
|
|
context = {
|
|
"subsection": subsection,
|
|
"sections": sections,
|
|
}
|
|
return render(request, "organizations/subsection_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def subsection_delete(request, pk):
|
|
"""Delete subsection view"""
|
|
subsection = get_object_or_404(StaffSubsection, pk=pk)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to delete subsections")
|
|
|
|
if not user.is_px_admin() and subsection.section.department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You can only delete subsections in your hospital")
|
|
|
|
if request.method == "POST":
|
|
subsection.delete()
|
|
messages.success(request, "Subsection deleted successfully.")
|
|
return redirect("organizations:subsection_list")
|
|
|
|
context = {
|
|
"subsection": subsection,
|
|
}
|
|
return render(request, "organizations/subsection_confirm_delete.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def patient_detail(request, pk):
|
|
"""Patient detail view"""
|
|
patient = get_object_or_404(Patient.objects.select_related("primary_hospital"), pk=pk)
|
|
|
|
# Apply RBAC filters
|
|
user = request.user
|
|
if not user.is_px_admin() and patient.primary_hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to view this patient")
|
|
|
|
from apps.integrations.models import HISPatientVisit
|
|
from apps.surveys.models import SurveyInstance
|
|
|
|
tab = request.GET.get("tab", "visits")
|
|
|
|
his_visits = (
|
|
HISPatientVisit.objects.filter(patient=patient)
|
|
.select_related("hospital", "survey_instance", "primary_doctor_fk", "consultant_fk")
|
|
.order_by("-admit_date")[:20]
|
|
)
|
|
|
|
surveys = (
|
|
SurveyInstance.objects.filter(patient=patient)
|
|
.select_related("survey_template")
|
|
.prefetch_related("responses")
|
|
.order_by("-created_at")[:10]
|
|
)
|
|
|
|
complaints = patient.complaints.select_related("hospital", "department").order_by("-created_at")[:10]
|
|
|
|
inquiries = patient.inquiries.select_related("hospital", "department").order_by("-created_at")[:10]
|
|
|
|
stats = {
|
|
"visits": HISPatientVisit.objects.filter(patient=patient).count(),
|
|
"surveys": SurveyInstance.objects.filter(patient=patient).count(),
|
|
"complaints": patient.complaints.count(),
|
|
"inquiries": patient.inquiries.count(),
|
|
}
|
|
|
|
context = {
|
|
"patient": patient,
|
|
"tab": tab,
|
|
"his_visits": his_visits,
|
|
"surveys": surveys,
|
|
"complaints": complaints,
|
|
"inquiries": inquiries,
|
|
"stats": stats,
|
|
}
|
|
|
|
return render(request, "organizations/patient_detail.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def patient_visit_journey(request, patient_pk, visit_pk):
|
|
"""Patient visit journey timeline view with time analytics"""
|
|
import json
|
|
from apps.integrations.models import HISPatientVisit
|
|
|
|
patient = get_object_or_404(Patient.objects.select_related("primary_hospital"), pk=patient_pk)
|
|
visit = get_object_or_404(
|
|
HISPatientVisit.objects.prefetch_related("visit_events").select_related(
|
|
"hospital", "survey_instance", "primary_doctor_fk", "consultant_fk"
|
|
),
|
|
pk=visit_pk,
|
|
patient=patient,
|
|
)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and patient.primary_hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to view this visit")
|
|
|
|
timeline = list(visit.visit_events.all())
|
|
|
|
EVENT_CATEGORIES = {
|
|
"Registration": ["consultation", "registration", "triage", "admission"],
|
|
"Lab": ["lab", "sample"],
|
|
"Radiology": ["rad", "radiology"],
|
|
"Pharmacy": ["drug", "pharmacy"],
|
|
"Doctor": ["doctor", "procedure", "episode"],
|
|
}
|
|
|
|
def _classify_event(event_type):
|
|
et = event_type.lower()
|
|
for cat, keywords in EVENT_CATEGORIES.items():
|
|
if any(kw in et for kw in keywords):
|
|
return cat
|
|
return "Other"
|
|
|
|
def _fmt_duration(seconds):
|
|
if seconds is None:
|
|
return "-"
|
|
total_min = int(seconds // 60)
|
|
if total_min < 60:
|
|
return f"{total_min}m"
|
|
hours = total_min // 60
|
|
mins = total_min % 60
|
|
if mins == 0:
|
|
return f"{hours}h"
|
|
return f"{hours}h {mins}m"
|
|
|
|
timeline_with_durations = []
|
|
intervals = []
|
|
category_time = {}
|
|
|
|
for i, event in enumerate(timeline):
|
|
duration_seconds = None
|
|
duration_display = "-"
|
|
if i < len(timeline) - 1 and event.parsed_date and timeline[i + 1].parsed_date:
|
|
delta = timeline[i + 1].parsed_date - event.parsed_date
|
|
duration_seconds = delta.total_seconds()
|
|
duration_display = _fmt_duration(duration_seconds)
|
|
|
|
cat = _classify_event(event.event_type)
|
|
category_time[cat] = category_time.get(cat, 0) + max(duration_seconds, 0)
|
|
|
|
intervals.append({
|
|
"label": f"{event.event_type} → {timeline[i + 1].event_type}",
|
|
"short_label": event.event_type,
|
|
"minutes": round(max(duration_seconds, 0) / 60, 1),
|
|
})
|
|
|
|
timeline_with_durations.append({
|
|
"event": event,
|
|
"duration_seconds": duration_seconds,
|
|
"duration_display": duration_display,
|
|
})
|
|
|
|
first_dt = None
|
|
last_dt = None
|
|
for e in timeline:
|
|
if e.parsed_date:
|
|
if first_dt is None or e.parsed_date < first_dt:
|
|
first_dt = e.parsed_date
|
|
if last_dt is None or e.parsed_date > last_dt:
|
|
last_dt = e.parsed_date
|
|
|
|
total_duration_seconds = (last_dt - first_dt).total_seconds() if first_dt and last_dt else 0
|
|
total_duration_display = _fmt_duration(total_duration_seconds)
|
|
|
|
valid_durations = [iv["minutes"] for iv in intervals if iv["minutes"] > 0]
|
|
avg_step = sum(valid_durations) / len(valid_durations) if valid_durations else 0
|
|
longest_wait = max(valid_durations) if valid_durations else 0
|
|
longest_wait_display = _fmt_duration(longest_wait * 60)
|
|
avg_step_display = _fmt_duration(avg_step * 60)
|
|
|
|
bar_chart_data = []
|
|
for iv in intervals[:20]:
|
|
if iv["minutes"] > 0:
|
|
bar_chart_data.append({"x": iv["short_label"], "y": iv["minutes"]})
|
|
if not bar_chart_data:
|
|
bar_chart_data = [{"x": "No intervals", "y": 0.1}]
|
|
|
|
donut_labels = []
|
|
donut_series = []
|
|
donut_color_keys = []
|
|
for cat, seconds in category_time.items():
|
|
minutes = round(seconds / 60, 1)
|
|
if minutes > 0:
|
|
donut_labels.append(cat)
|
|
donut_series.append(minutes)
|
|
donut_color_keys.append(cat)
|
|
if not donut_labels:
|
|
donut_labels = ["No data"]
|
|
donut_series = [1]
|
|
donut_color_keys = ["Other"]
|
|
|
|
donut_chart_data = {
|
|
"labels": donut_labels,
|
|
"series": donut_series,
|
|
}
|
|
|
|
CATEGORY_COLORS = {
|
|
"Registration": "#3b82f6",
|
|
"Lab": "#8b5cf6",
|
|
"Radiology": "#f59e0b",
|
|
"Pharmacy": "#10b981",
|
|
"Doctor": "#06b6d4",
|
|
"Other": "#94a3b8",
|
|
}
|
|
donut_colors = [CATEGORY_COLORS.get(c, "#94a3b8") for c in donut_color_keys]
|
|
|
|
context = {
|
|
"patient": patient,
|
|
"visit": visit,
|
|
"timeline": timeline_with_durations,
|
|
"total_duration_display": total_duration_display,
|
|
"total_duration_seconds": total_duration_seconds,
|
|
"avg_step_display": avg_step_display,
|
|
"longest_wait_display": longest_wait_display,
|
|
"step_count": len(timeline),
|
|
"bar_chart_json": json.dumps(bar_chart_data),
|
|
"donut_chart_json": json.dumps(donut_chart_data),
|
|
"donut_colors_json": json.dumps(donut_colors),
|
|
}
|
|
return render(request, "organizations/patient_visit_journey.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def patient_create(request):
|
|
"""Create patient view"""
|
|
user = request.user
|
|
|
|
# Only PX Admins and Hospital Admins can create patients
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to create patients")
|
|
|
|
if request.method == "POST":
|
|
form = PatientForm(user, request.POST)
|
|
if form.is_valid():
|
|
patient = form.save()
|
|
messages.success(request, f"Patient {patient.get_full_name()} created successfully.")
|
|
return redirect("organizations:patient_detail", pk=patient.pk)
|
|
else:
|
|
form = PatientForm(user)
|
|
|
|
context = {
|
|
"form": form,
|
|
"title": _("Create Patient"),
|
|
}
|
|
|
|
return render(request, "organizations/patient_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def patient_update(request, pk):
|
|
"""Update patient view"""
|
|
patient = get_object_or_404(Patient, pk=pk)
|
|
user = request.user
|
|
|
|
# Apply RBAC filters
|
|
if not user.is_px_admin() and patient.primary_hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to edit this patient")
|
|
|
|
# Only PX Admins and Hospital Admins can update patients
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to edit patients")
|
|
|
|
if request.method == "POST":
|
|
form = PatientForm(user, request.POST, instance=patient)
|
|
if form.is_valid():
|
|
patient = form.save()
|
|
messages.success(request, f"Patient {patient.get_full_name()} updated successfully.")
|
|
return redirect("organizations:patient_detail", pk=patient.pk)
|
|
else:
|
|
form = PatientForm(user, instance=patient)
|
|
|
|
context = {
|
|
"form": form,
|
|
"patient": patient,
|
|
"title": _("Edit Patient"),
|
|
}
|
|
|
|
return render(request, "organizations/patient_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def patient_delete(request, pk):
|
|
"""Delete patient view"""
|
|
patient = get_object_or_404(Patient, pk=pk)
|
|
user = request.user
|
|
|
|
# Apply RBAC filters
|
|
if not user.is_px_admin() and patient.primary_hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("You don't have permission to delete this patient")
|
|
|
|
# Only PX Admins can delete patients
|
|
if not user.is_px_admin():
|
|
from django.http import HttpResponseForbidden
|
|
|
|
return HttpResponseForbidden("Only PX Admins can delete patients")
|
|
|
|
if request.method == "POST":
|
|
patient_name = patient.get_full_name()
|
|
patient.delete()
|
|
messages.success(request, f"Patient {patient_name} deleted successfully.")
|
|
return redirect("organizations:patient_list")
|
|
|
|
context = {
|
|
"patient": patient,
|
|
}
|
|
|
|
return render(request, "organizations/patient_confirm_delete.html", context)
|
|
|
|
|
|
@csrf_exempt
|
|
@login_required
|
|
@require_POST
|
|
def search_his_patient(request):
|
|
"""Search HIS by SSN or MobileNo, return patient demographics as JSON."""
|
|
import json
|
|
from django.http import JsonResponse
|
|
|
|
try:
|
|
data = json.loads(request.body or b"{}")
|
|
if not isinstance(data, dict):
|
|
data = {}
|
|
except (json.JSONDecodeError, TypeError):
|
|
data = {}
|
|
|
|
ssn = (data.get("ssn") or "").strip()
|
|
mobile_no = (data.get("mobile_no") or "").strip()
|
|
|
|
if not ssn and not mobile_no:
|
|
return JsonResponse({"error": "Provide SSN or Mobile number"}, status=400)
|
|
|
|
from apps.integrations.models import IntegrationConfig
|
|
from apps.integrations.services.his_client import HISClient
|
|
from django.conf import settings
|
|
|
|
config = IntegrationConfig.objects.filter(source_system__in=["his", "other"]).first()
|
|
|
|
if not config:
|
|
return JsonResponse({"error": "HIS integration not configured"}, status=400)
|
|
|
|
if settings.DEBUG:
|
|
config.api_url = request.build_absolute_uri("/api/integrations/test-his-data/")
|
|
|
|
client = HISClient(config)
|
|
patients = client.fetch_patient_by_identifier(ssn=ssn or None, mobile_no=mobile_no or None)
|
|
|
|
if patients is None:
|
|
return JsonResponse({"error": "Failed to connect to HIS"}, status=500)
|
|
|
|
if not patients:
|
|
return JsonResponse({"patients": [], "total": 0})
|
|
|
|
result = []
|
|
for p in patients:
|
|
result.append(
|
|
{
|
|
"patient_id": p.get("PatientID"),
|
|
"admission_id": p.get("AdmissionID"),
|
|
"name": p.get("PatientName", ""),
|
|
"ssn": p.get("SSN", ""),
|
|
"mobile_no": p.get("MobileNo", ""),
|
|
"gender": p.get("Gender", ""),
|
|
"nationality": p.get("PatientNationality", ""),
|
|
"dob": p.get("DOB", ""),
|
|
"patient_type": p.get("PatientType", ""),
|
|
"hospital_name": p.get("HospitalName", ""),
|
|
"hospital_id": p.get("HospitalID", ""),
|
|
"admit_date": p.get("AdmitDate", ""),
|
|
"discharge_date": p.get("DischargeDate", ""),
|
|
"reg_code": p.get("RegCode", ""),
|
|
"raw": p,
|
|
}
|
|
)
|
|
|
|
return JsonResponse({"patients": result, "total": len(result)})
|
|
|
|
|
|
@csrf_exempt
|
|
@login_required
|
|
@require_POST
|
|
def save_his_patient(request):
|
|
"""Save a patient from HIS data locally."""
|
|
import json
|
|
from django.http import JsonResponse
|
|
|
|
try:
|
|
data = json.loads(request.body or b"{}")
|
|
if not isinstance(data, dict):
|
|
data = {}
|
|
except (json.JSONDecodeError, TypeError):
|
|
data = {}
|
|
|
|
patient_data = data.get("patient_data")
|
|
if not patient_data:
|
|
return JsonResponse({"error": "No patient data provided"}, status=400)
|
|
|
|
from apps.integrations.models import IntegrationConfig
|
|
from apps.integrations.services.his_adapter import HISAdapter
|
|
|
|
config = IntegrationConfig.objects.filter(source_system__in=["his", "other"]).first()
|
|
|
|
if not config:
|
|
return JsonResponse({"error": "HIS integration not configured"}, status=400)
|
|
|
|
try:
|
|
hospital = HISAdapter.get_or_create_hospital(patient_data)
|
|
patient = HISAdapter.get_or_create_patient(patient_data, hospital)
|
|
visit = HISAdapter.save_patient_visit(
|
|
patient=patient,
|
|
hospital=hospital,
|
|
patient_data=patient_data,
|
|
visit_timeline=[],
|
|
is_visit_complete=False,
|
|
)
|
|
|
|
return JsonResponse(
|
|
{
|
|
"success": True,
|
|
"patient_id": str(patient.id),
|
|
"patient_name": patient.get_full_name(),
|
|
"visit_id": str(visit.id),
|
|
"admission_id": visit.admission_id,
|
|
}
|
|
)
|
|
except Exception as e:
|
|
return JsonResponse({"error": str(e)}, status=500)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
@require_POST
|
|
def send_complaint_link(request, pk):
|
|
"""Create a complaint session and send SMS link to the patient."""
|
|
from django.http import JsonResponse
|
|
from apps.complaints.models import PatientComplaintSession
|
|
from apps.notifications.services import NotificationService
|
|
|
|
patient = get_object_or_404(Patient.objects.select_related("primary_hospital"), pk=pk)
|
|
|
|
if not patient.phone:
|
|
return JsonResponse({"error": "Patient has no phone number"}, status=400)
|
|
|
|
session = PatientComplaintSession.objects.create(
|
|
patient=patient,
|
|
created_by=request.user,
|
|
)
|
|
|
|
base_url = getattr(settings, "SURVEY_BASE_URL", "")
|
|
link = f"{base_url}/complaints/patient/{session.token}/"
|
|
|
|
message = (
|
|
f"Dear {patient.first_name}, we value your feedback. "
|
|
f"If you have any concerns about your recent visit, please submit them here: {link}"
|
|
)
|
|
|
|
NotificationService.send_sms(phone=patient.phone, message=message)
|
|
|
|
return JsonResponse(
|
|
{
|
|
"success": True,
|
|
"token": session.token,
|
|
"link": link,
|
|
"phone": patient.phone,
|
|
}
|
|
)
|
|
|
|
|
|
|
|
@login_required
|
|
def department_list(request):
|
|
from .models import Department
|
|
from apps.organizations.models import Staff
|
|
from django.db.models import Count, Q
|
|
|
|
user = request.user
|
|
selected_hospital = getattr(request, "tenant_hospital", None)
|
|
|
|
queryset = Department.objects.select_related("hospital", "manager").filter(status="active")
|
|
|
|
if user.is_px_admin():
|
|
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(
|
|
Q(id=user.department.id) | Q(parent=user.department)
|
|
)
|
|
elif user.is_director():
|
|
directed_depts = user.get_directed_departments()
|
|
if directed_depts.exists():
|
|
queryset = queryset.filter(id__in=directed_depts)
|
|
else:
|
|
queryset = queryset.none()
|
|
elif user.is_champion() and user.department and not user.is_px_employee() and not user.is_px_management():
|
|
queryset = queryset.filter(id=user.department.id)
|
|
elif user.is_basic_staff() and user.department:
|
|
queryset = queryset.filter(id=user.department.id)
|
|
elif user.hospital:
|
|
queryset = queryset.filter(hospital=user.hospital)
|
|
else:
|
|
queryset = queryset.none()
|
|
|
|
search = request.GET.get("search", "").strip()
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=search)
|
|
| Q(name_en__icontains=search)
|
|
| Q(name_ar__icontains=search)
|
|
| Q(code__icontains=search)
|
|
)
|
|
|
|
category_filter = request.GET.get("category")
|
|
if category_filter:
|
|
queryset = queryset.filter(category=category_filter)
|
|
|
|
queryset = queryset.annotate(
|
|
staff_count=Count("staff", filter=Q(staff__status="active"), distinct=True),
|
|
open_complaints=Count(
|
|
"complaints",
|
|
filter=Q(complaints__status__in=["open", "in_progress"]),
|
|
distinct=True,
|
|
),
|
|
pending_inquiries=Count(
|
|
"inquiries",
|
|
filter=Q(inquiries__status__in=["open", "in_progress"]),
|
|
distinct=True,
|
|
),
|
|
open_observations=Count(
|
|
"assigned_observations",
|
|
filter=Q(assigned_observations__status__in=["new", "triaged", "in_progress"]),
|
|
distinct=True,
|
|
),
|
|
).order_by("name")
|
|
|
|
total_count = queryset.count()
|
|
active_count = queryset.filter(status="active").count()
|
|
with_managers = queryset.filter(manager__isnull=False).count()
|
|
total_staff = sum(d.staff_count for d in queryset)
|
|
|
|
hospitals = Hospital.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
hospitals = hospitals.filter(id=user.hospital.id)
|
|
|
|
context = {
|
|
"departments": queryset,
|
|
"total_count": total_count,
|
|
"active_count": active_count,
|
|
"with_managers": with_managers,
|
|
"total_staff": total_staff,
|
|
"hospitals": hospitals,
|
|
"search": search,
|
|
"category_filter": category_filter,
|
|
"categories": DepartmentCategory.choices,
|
|
"can_create": user.is_px_admin() or user.is_hospital_admin(),
|
|
}
|
|
return render(request, "organizations/department_list.html", context)
|
|
|
|
|
|
@login_required
|
|
def department_detail(request, pk):
|
|
from .models import Department, Staff
|
|
from apps.complaints.models import Complaint, ComplaintInvolvedStaff, Inquiry
|
|
from apps.observations.models import Observation
|
|
from apps.appreciation.models import Appreciation
|
|
from apps.feedback.models import Feedback, FeedbackType
|
|
from django.db.models import Q
|
|
from collections import defaultdict
|
|
|
|
department = get_object_or_404(
|
|
Department.objects.select_related(
|
|
"hospital", "manager", "parent",
|
|
"champion", "manager", "manager_2nd", "manager_3rd",
|
|
"deputy_manager", "supervisor", "deputy_supervisor",
|
|
),
|
|
pk=pk,
|
|
)
|
|
|
|
user = request.user
|
|
if not (
|
|
user.is_px_admin()
|
|
or (user.is_hospital_admin() and user.hospital == department.hospital)
|
|
or (
|
|
(user.is_champion() or user.is_department_manager())
|
|
and user.department == department
|
|
)
|
|
or (user.is_basic_staff() and user.department == department)
|
|
or (user.is_director() and user.get_directed_departments().filter(id=department.id).exists())
|
|
or (user.hospital == department.hospital and not user.is_source_user() and not user.is_basic_staff() and not user.is_department_manager() and not user.is_director())
|
|
):
|
|
messages.error(request, _("You don't have permission to view this department."))
|
|
return redirect("organizations:department_list")
|
|
|
|
staff_list = (
|
|
Staff.objects.filter(department=department, status="active")
|
|
.select_related("user", "department")
|
|
.order_by("-is_head", "first_name")
|
|
)
|
|
staff_head = staff_list.filter(is_head=True).first()
|
|
|
|
active_staff_ids = list(staff_list.values_list("pk", flat=True))
|
|
primary_pairs = set(
|
|
Complaint.objects.filter(staff_id__in=active_staff_ids).values_list("staff_id", "pk")
|
|
)
|
|
involved_pairs = set(
|
|
ComplaintInvolvedStaff.objects.filter(staff_id__in=active_staff_ids).values_list("staff_id", "complaint_id")
|
|
)
|
|
staff_complaint_counts = defaultdict(int)
|
|
for staff_id, _complaint_id in primary_pairs | involved_pairs:
|
|
staff_complaint_counts[staff_id] += 1
|
|
|
|
complaints = Complaint.objects.filter(
|
|
Q(department=department, sent_to_department=True) | Q(involved_departments__department=department, involved_departments__sent=True)
|
|
).select_related(
|
|
"patient", "assigned_to", "category", "domain", "subcategory_obj", "classification_obj", "staff"
|
|
).prefetch_related(
|
|
"involved_staff__staff"
|
|
).distinct().order_by("-created_at")
|
|
|
|
inquiry_direction = request.GET.get("inquiry_direction", "incoming")
|
|
sent_away = (
|
|
Q(department=department)
|
|
& Q(transferred_to_department__isnull=False)
|
|
& ~Q(transferred_to_department=department)
|
|
)
|
|
incoming_inquiries_qs = Inquiry.objects.filter(
|
|
Q(department=department, sent_to_department=True)
|
|
| Q(outgoing_department=department)
|
|
| Q(transferred_to_department=department)
|
|
).exclude(sent_away)
|
|
outgoing_inquiries_qs = Inquiry.objects.filter(sent_away)
|
|
|
|
if inquiry_direction == "outgoing":
|
|
inquiries = outgoing_inquiries_qs
|
|
else:
|
|
inquiries = incoming_inquiries_qs
|
|
inquiries = inquiries.select_related("assigned_to", "patient", "department", "outgoing_department", "transferred_to_department").order_by("-created_at")
|
|
|
|
incoming_count = incoming_inquiries_qs.count()
|
|
outgoing_count = outgoing_inquiries_qs.count()
|
|
all_inquiries_count = incoming_count + outgoing_count
|
|
|
|
observations = Observation.objects.filter(
|
|
assigned_department=department, sent_to_department=True
|
|
).select_related("category", "assigned_to").order_by("-created_at")
|
|
|
|
complaint_status_filter = request.GET.get("complaint_status")
|
|
if complaint_status_filter:
|
|
complaints = complaints.filter(status=complaint_status_filter)
|
|
|
|
inquiry_status_filter = request.GET.get("inquiry_status")
|
|
if inquiry_status_filter:
|
|
inquiries = inquiries.filter(status=inquiry_status_filter)
|
|
|
|
observation_status_filter = request.GET.get("observation_status")
|
|
if observation_status_filter:
|
|
observations = observations.filter(status=observation_status_filter)
|
|
|
|
appreciations = Appreciation.objects.filter(
|
|
department=department
|
|
).select_related("sender", "category").order_by("-created_at")
|
|
|
|
suggestions = Feedback.objects.filter(
|
|
department=department, feedback_type=FeedbackType.SUGGESTION
|
|
).select_related("assigned_to", "staff").order_by("-created_at")
|
|
|
|
# QI Projects linked to this department (templates excluded)
|
|
from apps.projects.models import QIProject
|
|
|
|
qi_projects = (
|
|
QIProject.objects.filter(departments=department, is_template=False)
|
|
.select_related("hospital", "project_lead")
|
|
.prefetch_related("departments", "team_members")
|
|
.order_by("-created_at")
|
|
)
|
|
|
|
appreciation_status_filter = request.GET.get("appreciation_status")
|
|
if appreciation_status_filter:
|
|
appreciations = appreciations.filter(status=appreciation_status_filter)
|
|
|
|
suggestion_status_filter = request.GET.get("suggestion_status")
|
|
if suggestion_status_filter:
|
|
suggestions = suggestions.filter(status=suggestion_status_filter)
|
|
|
|
active_tab = request.GET.get("tab", "analytics")
|
|
|
|
stats = {
|
|
"staff_count": staff_list.count(),
|
|
"open_complaints": complaints.filter(status__in=["open", "in_progress"]).count(),
|
|
"pending_inquiries": inquiries.filter(status__in=["open", "in_progress"]).count(),
|
|
"open_observations": observations.filter(status__in=["new", "triaged", "in_progress"]).count(),
|
|
"total_complaints": complaints.count(),
|
|
"total_inquiries": inquiries.count(),
|
|
"total_observations": observations.count(),
|
|
"total_appreciations": appreciations.count(),
|
|
"total_suggestions": suggestions.count(),
|
|
"total_projects": qi_projects.count(),
|
|
}
|
|
|
|
search_query = request.GET.get("search", "").strip()
|
|
if search_query:
|
|
staff_list = staff_list.filter(
|
|
Q(first_name__icontains=search_query)
|
|
| Q(last_name__icontains=search_query)
|
|
| Q(employee_id__icontains=search_query)
|
|
)
|
|
complaints = complaints.filter(
|
|
Q(reference_number__icontains=search_query)
|
|
| Q(title__icontains=search_query)
|
|
)
|
|
inquiries = inquiries.filter(
|
|
Q(reference_number__icontains=search_query)
|
|
| Q(subject__icontains=search_query)
|
|
| Q(contact_name__icontains=search_query)
|
|
)
|
|
observations = observations.filter(
|
|
Q(tracking_code__icontains=search_query)
|
|
| Q(title__icontains=search_query)
|
|
)
|
|
|
|
assignable_staff = Staff.objects.filter(
|
|
department=department,
|
|
status="active",
|
|
user__isnull=False,
|
|
).exclude(email="").order_by("first_name", "last_name")
|
|
|
|
managers = Staff.objects.filter(
|
|
department=department, status="active"
|
|
).order_by("first_name", "last_name")
|
|
|
|
pending_actions = []
|
|
from apps.complaints.models import ComplaintExplanation, ComplaintInvolvedDepartment, ChampionInvestigation, InvestigationResponse
|
|
from django.utils import timezone as dj_tz
|
|
|
|
# 1. Complaint Department Responses (new)
|
|
pending_complaint_dept_responses = ComplaintInvolvedDepartment.objects.filter(
|
|
department=department,
|
|
forwarded_at__isnull=False,
|
|
sent=True,
|
|
).filter(
|
|
Q(response_submitted=False, acceptance_status="pending")
|
|
| Q(acceptance_status="not_acceptable")
|
|
| Q(manager_review_status="rejected")
|
|
).select_related("complaint").order_by("-forwarded_at")
|
|
for pc in pending_complaint_dept_responses:
|
|
is_rejected = pc.acceptance_status == "not_acceptable"
|
|
needs_resubmit = is_rejected or pc.manager_review_status == "rejected"
|
|
explanation_url = "#"
|
|
explanation = ComplaintExplanation.objects.filter(
|
|
complaint_id=pc.complaint_id,
|
|
staff__department=department,
|
|
).first()
|
|
if not explanation and department.champion:
|
|
import secrets as _secrets
|
|
explanation, _created = ComplaintExplanation.objects.get_or_create(
|
|
complaint=pc.complaint,
|
|
staff=department.champion,
|
|
defaults={
|
|
"token": _secrets.token_urlsafe(32),
|
|
"explanation": "",
|
|
"is_used": False,
|
|
},
|
|
)
|
|
if explanation:
|
|
if explanation.is_used and needs_resubmit:
|
|
import secrets
|
|
explanation.is_used = False
|
|
explanation.token = secrets.token_urlsafe(32)
|
|
explanation.save(update_fields=["is_used", "token"])
|
|
if not explanation.is_used:
|
|
explanation_url = reverse(
|
|
"complaints:complaint_explanation_form",
|
|
kwargs={"complaint_id": pc.complaint_id, "token": explanation.token},
|
|
)
|
|
pending_actions.append({
|
|
"type": "complaint_department_response",
|
|
"type_label": _("Complaint Response (Re-submit)") if is_rejected else _("Complaint Response"),
|
|
"reference": pc.complaint.reference_number or str(pc.complaint.id),
|
|
"subject": pc.complaint.title or _("No title"),
|
|
"sla_due_at": None,
|
|
"is_overdue": False,
|
|
"url": "#",
|
|
"explanation_url": explanation_url,
|
|
"badge_color": "red" if is_rejected else "orange",
|
|
"item_id": str(pc.pk),
|
|
"item_type": "complaint_involved_department",
|
|
"complaint_id": str(pc.complaint_id),
|
|
"department_name": pc.department.name,
|
|
})
|
|
|
|
if (
|
|
user.is_department_manager()
|
|
or user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or user.is_px_management()
|
|
or user.is_px_employee()
|
|
):
|
|
pending_manager_reviews = ComplaintInvolvedDepartment.objects.filter(
|
|
department=department,
|
|
response_submitted=True,
|
|
manager_review_status="pending",
|
|
).select_related("complaint").order_by("-response_submitted_at")
|
|
for mr in pending_manager_reviews:
|
|
pending_actions.append({
|
|
"type": "manager_review",
|
|
"type_label": _("Manager Review"),
|
|
"reference": mr.complaint.reference_number or str(mr.complaint.id),
|
|
"subject": mr.complaint.title or _("No title"),
|
|
"sla_due_at": None,
|
|
"is_overdue": False,
|
|
"url": reverse("organizations:department_manager_review", kwargs={"pk": department.pk, "idept_pk": mr.pk}),
|
|
"badge_color": "blue",
|
|
"item_id": str(mr.pk),
|
|
"item_type": "manager_review",
|
|
"complaint_id": str(mr.complaint_id),
|
|
"department_name": mr.department.name,
|
|
})
|
|
|
|
# 2. Complaint Explanations (staff-level) — only for THIS department's staff
|
|
# and only for complaints not already covered by a CID-based entry above.
|
|
already_pending_complaint_ids = set(
|
|
str(a.get("complaint_id")) for a in pending_actions if a.get("complaint_id")
|
|
)
|
|
pending_explanations = ComplaintExplanation.objects.filter(
|
|
staff__department=department,
|
|
is_used=False,
|
|
).exclude(complaint_id__in=already_pending_complaint_ids).select_related(
|
|
"complaint", "staff"
|
|
).order_by("sla_due_at")
|
|
for pe in pending_explanations:
|
|
pending_actions.append({
|
|
"type": "complaint_explanation",
|
|
"type_label": _("Complaint Explanation"),
|
|
"reference": pe.complaint.reference_number or str(pe.complaint.id),
|
|
"subject": pe.complaint.title,
|
|
"sla_due_at": pe.sla_due_at,
|
|
"is_overdue": pe.sla_due_at and pe.sla_due_at < dj_tz.now(),
|
|
"url": "/complaints/{}/explain/{}/".format(pe.complaint_id, pe.token),
|
|
"badge_color": "orange",
|
|
})
|
|
|
|
pending_observations = Observation.objects.filter(
|
|
assigned_department=department,
|
|
sent_to_department=True,
|
|
forwarded_to_dept_at__isnull=False,
|
|
department_responded_at__isnull=True,
|
|
).order_by("dept_response_sla_due_at")
|
|
for obs in pending_observations:
|
|
pending_actions.append({
|
|
"type": "observation_response",
|
|
"type_label": _("Observation Response"),
|
|
"reference": obs.tracking_code or str(obs.id),
|
|
"subject": obs.description[:80] if obs.description else "",
|
|
"sla_due_at": obs.dept_response_sla_due_at,
|
|
"is_overdue": obs.dept_response_sla_due_at and obs.dept_response_sla_due_at < dj_tz.now(),
|
|
"url": "/observations/{}/department-response/".format(obs.pk),
|
|
"badge_color": "purple",
|
|
"item_id": str(obs.pk),
|
|
"existing_en": obs.department_response_en or "",
|
|
"existing_ar": obs.department_response_ar or "",
|
|
})
|
|
|
|
pending_inquiries = Inquiry.objects.filter(
|
|
Q(outgoing_department=department) | Q(department=department, sent_to_department=True),
|
|
transferred_at__isnull=False,
|
|
status__in=["open", "in_progress"],
|
|
).order_by("dept_response_sla_due_at")
|
|
for inq in pending_inquiries:
|
|
pending_actions.append({
|
|
"type": "inquiry_response",
|
|
"type_label": _("Inquiry to Handle"),
|
|
"reference": inq.reference_number or str(inq.id),
|
|
"subject": inq.subject or inq.message[:80] if inq.message else "",
|
|
"sla_due_at": inq.dept_response_sla_due_at,
|
|
"is_overdue": inq.dept_response_sla_due_at and inq.dept_response_sla_due_at < dj_tz.now(),
|
|
"url": "/organizations/departments/{}/inquiries/{}/".format(department.pk, inq.pk),
|
|
"badge_color": "cyan",
|
|
"item_id": str(inq.pk),
|
|
"existing_en": inq.department_response_en or "",
|
|
"existing_ar": inq.department_response_ar or "",
|
|
})
|
|
|
|
# Merge routed complaints into pending_actions (unified table)
|
|
_existing_complaint_ids = {
|
|
str(a.get("complaint_id")) for a in pending_actions if a.get("complaint_id")
|
|
}
|
|
pending_routing_involvements = list(
|
|
ComplaintInvolvedDepartment.objects.filter(
|
|
department=department,
|
|
sent=True,
|
|
response_submitted=False,
|
|
routing_status="sent",
|
|
).select_related("complaint", "complaint__department", "department").order_by("-sent_at")[:10]
|
|
)
|
|
for inv in pending_routing_involvements:
|
|
if str(inv.complaint_id) not in _existing_complaint_ids:
|
|
pending_actions.append({
|
|
"type": "complaint_department_response",
|
|
"type_label": _("Complaint Response"),
|
|
"reference": inv.complaint.reference_number or str(inv.complaint.id),
|
|
"subject": inv.complaint.title or _("No title"),
|
|
"complaint_id": str(inv.complaint_id),
|
|
"item_id": str(inv.id),
|
|
"routing_inv_id": str(inv.id),
|
|
"sla_due_at": None,
|
|
"is_overdue": False,
|
|
"url": "#",
|
|
"explanation_url": "#",
|
|
"badge_color": "blue",
|
|
})
|
|
_existing_complaint_ids.add(str(inv.complaint_id))
|
|
|
|
# Standards for this department (global + department-specific)
|
|
from apps.standards.models import Standard, StandardCompliance
|
|
from django.db.models import Count
|
|
|
|
dept_standards = (
|
|
Standard.objects.filter(is_active=True, departments=department)
|
|
.select_related("source", "category", "activity_type")
|
|
.order_by("source", "category", "order_within_category", "code")
|
|
)
|
|
standards_data = []
|
|
for std in dept_standards:
|
|
compliance = StandardCompliance.objects.filter(
|
|
department=department, standard=std
|
|
).first()
|
|
standards_data.append({
|
|
"standard": std,
|
|
"compliance": compliance,
|
|
"attachment_count": compliance.attachments.count() if compliance else 0,
|
|
})
|
|
|
|
context = {
|
|
"department": department,
|
|
"staff_list": staff_list,
|
|
"staff_head": staff_head,
|
|
"staff_complaint_counts": staff_complaint_counts,
|
|
"complaints": complaints[:5],
|
|
"inquiries": inquiries[:5],
|
|
"inquiry_direction": inquiry_direction,
|
|
"incoming_count": incoming_count,
|
|
"outgoing_count": outgoing_count,
|
|
"all_inquiries_count": all_inquiries_count,
|
|
"observations": observations[:5],
|
|
"appreciations": appreciations[:50],
|
|
"suggestions": suggestions[:50],
|
|
"projects": qi_projects[:10],
|
|
"stats": stats,
|
|
"active_tab": active_tab,
|
|
"assignable_staff": assignable_staff,
|
|
"managers": managers,
|
|
"standards_data": standards_data,
|
|
"org_sections": Section.objects.filter(department=department).select_related("champion", "supervisor", "deputy_supervisor").order_by("name_en"),
|
|
"role_data": (lambda d: [
|
|
{"field": "champion", "label": "Champion", "staff": d.champion, "readonly": False},
|
|
{"field": "manager", "label": "Manager", "staff": d.manager, "readonly": False},
|
|
] + [
|
|
{
|
|
"field": field_name,
|
|
"label": role_label,
|
|
"staff": getattr(d, field_name, None),
|
|
"readonly": False,
|
|
}
|
|
for field_name, role_label in Department.ROLE_FIELDS
|
|
if field_name != "champion"
|
|
])(department),
|
|
"search_query": search_query,
|
|
"complaint_status_filter": complaint_status_filter,
|
|
"inquiry_status_filter": inquiry_status_filter,
|
|
"observation_status_filter": observation_status_filter,
|
|
"can_assign": user.is_px_admin() or user.is_hospital_admin() or user.is_department_manager(),
|
|
"can_manage_roles": user.is_px_admin() or user.is_px_employee() or user.is_hospital_admin(),
|
|
"can_edit": user.is_px_admin() or (user.is_hospital_admin() and user.hospital == department.hospital),
|
|
"can_respond": (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or (user.is_department_manager() and user.department == department)
|
|
or (user.is_champion() and user.department == department)
|
|
),
|
|
"pending_actions": pending_actions,
|
|
"pending_actions_count": len(pending_actions),
|
|
"active_investigations": ChampionInvestigation.objects.filter(
|
|
involved_department__department=department,
|
|
status__in=["questions_sent", "answers_received"],
|
|
).select_related("complaint", "champion", "explanation").prefetch_related(
|
|
"responses__staff", "questions"
|
|
),
|
|
"my_pending_responses": (
|
|
InvestigationResponse.objects.filter(
|
|
staff__user=user,
|
|
is_completed=False,
|
|
investigation__involved_department__department=department,
|
|
).select_related("staff", "investigation__complaint", "investigation__champion")
|
|
if hasattr(user, "staff_profile") and user.staff_profile else []
|
|
),
|
|
"my_assigned_complaints": Complaint.objects.filter(
|
|
assigned_to=user,
|
|
status__in=["open", "in_progress"],
|
|
involved_departments__department=department,
|
|
).distinct().select_related("department", "assigned_to")[:5],
|
|
"pending_routing_involvements": pending_routing_involvements,
|
|
"hospital_departments": (
|
|
Department.objects.filter(hospital=department.hospital, status="active")
|
|
.filter(Q(champion__isnull=False) | Q(manager__isnull=False))
|
|
.order_by("name")
|
|
),
|
|
}
|
|
return render(request, "organizations/department_detail.html", context)
|
|
|
|
def _check_department_access(user, department):
|
|
if user.is_px_admin():
|
|
return True
|
|
if user.is_hospital_admin() and user.hospital == department.hospital:
|
|
return True
|
|
if department.manager_id == user.id:
|
|
return True
|
|
if user.is_champion() and user.department == department:
|
|
return True
|
|
if user.is_department_manager() and user.department == department:
|
|
return True
|
|
if user.is_basic_staff() and user.department == department:
|
|
return True
|
|
if user.is_director() and user.get_directed_departments().filter(id=department.id).exists():
|
|
return True
|
|
if (
|
|
user.hospital == department.hospital
|
|
and not user.is_source_user()
|
|
and not user.is_basic_staff()
|
|
and not user.is_department_manager()
|
|
and not user.is_director()
|
|
):
|
|
return True
|
|
return False
|
|
|
|
|
|
@login_required
|
|
def department_complaints_list(request, pk):
|
|
from .models import Department
|
|
from apps.complaints.models import Complaint, ComplaintInvolvedDepartment
|
|
from django.core.paginator import Paginator
|
|
|
|
department = get_object_or_404(
|
|
Department.objects.select_related("hospital"), pk=pk
|
|
)
|
|
user = request.user
|
|
if not _check_department_access(user, department):
|
|
messages.error(request, _("You don't have permission to view this department."))
|
|
return redirect("organizations:department_list")
|
|
|
|
queryset = Complaint.objects.filter(
|
|
Q(department=department, sent_to_department=True) | Q(involved_departments__department=department, involved_departments__sent=True)
|
|
).select_related(
|
|
"patient", "assigned_to", "category", "domain", "subcategory_obj",
|
|
"classification_obj", "staff",
|
|
).prefetch_related(
|
|
"involved_staff__staff", "involved_departments__department",
|
|
).distinct().order_by("-created_at")
|
|
|
|
from django.db.models import Count
|
|
base_filter = Q(department=department, sent_to_department=True) | Q(involved_departments__department=department, involved_departments__sent=True)
|
|
status_counts = dict(
|
|
Complaint.objects.filter(base_filter)
|
|
.values("status")
|
|
.annotate(count=Count("pk", distinct=True))
|
|
.values_list("status", "count")
|
|
)
|
|
|
|
status_filter = request.GET.get("status")
|
|
severity_filter = request.GET.get("severity", "")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
if severity_filter:
|
|
queryset = queryset.filter(severity=severity_filter)
|
|
|
|
search_query = request.GET.get("search", "").strip()
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(reference_number__icontains=search_query) | Q(title__icontains=search_query)
|
|
)
|
|
|
|
date_from = request.GET.get("date_from")
|
|
date_to = request.GET.get("date_to")
|
|
if date_from:
|
|
queryset = queryset.filter(created_at__date__gte=date_from)
|
|
if date_to:
|
|
queryset = queryset.filter(created_at__date__lte=date_to)
|
|
|
|
page_size = int(request.GET.get("page_size", 25))
|
|
paginator = Paginator(queryset, page_size)
|
|
page_obj = paginator.get_page(request.GET.get("page", 1))
|
|
|
|
can_respond = (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or (user.is_department_manager() and user.department == department)
|
|
or (user.is_champion() and user.department == department)
|
|
)
|
|
|
|
complaint_dept_map = {}
|
|
if can_respond:
|
|
complaint_ids = [c.pk for c in page_obj.object_list]
|
|
for idept in ComplaintInvolvedDepartment.objects.filter(
|
|
department=department, complaint_id__in=complaint_ids, sent=True, response_submitted=False
|
|
):
|
|
complaint_dept_map[idept.complaint_id] = idept
|
|
for idept in ComplaintInvolvedDepartment.objects.filter(
|
|
department=department, complaint_id__in=complaint_ids, sent=True,
|
|
response_submitted=True, manager_review_status="pending",
|
|
):
|
|
if idept.complaint_id not in complaint_dept_map:
|
|
complaint_dept_map[idept.complaint_id] = idept
|
|
|
|
context = {
|
|
"department": department,
|
|
"page_obj": page_obj,
|
|
"complaints": page_obj,
|
|
"status_filter": status_filter,
|
|
"severity_filter": severity_filter or "",
|
|
"search_query": search_query,
|
|
"date_from": date_from or "",
|
|
"date_to": date_to or "",
|
|
"can_respond": can_respond,
|
|
"complaint_dept_map": complaint_dept_map,
|
|
"status_choices": Complaint.Status.choices if hasattr(Complaint, 'Status') else [],
|
|
"list_stats": status_counts,
|
|
"status_labels": {"open": _("Open"), "in_progress": _("In Progress"), "resolved": _("Resolved"), "closed": _("Closed"), "contact_no_response": _("Contacted - No Response")},
|
|
}
|
|
return render(request, "organizations/department_complaints.html", context)
|
|
|
|
|
|
@login_required
|
|
def department_inquiries_list(request, pk):
|
|
from .models import Department
|
|
from apps.complaints.models import Inquiry
|
|
from django.core.paginator import Paginator
|
|
|
|
department = get_object_or_404(
|
|
Department.objects.select_related("hospital"), pk=pk
|
|
)
|
|
user = request.user
|
|
if not _check_department_access(user, department):
|
|
messages.error(request, _("You don't have permission to view this department."))
|
|
return redirect("organizations:department_list")
|
|
|
|
direction = request.GET.get("direction", "incoming")
|
|
|
|
# Direction split (department POV):
|
|
# outgoing = this department transferred the inquiry away to another department
|
|
# incoming = inquiries this department currently handles (owned & not sent away, or transferred TO it)
|
|
sent_away = (
|
|
Q(department=department)
|
|
& Q(transferred_to_department__isnull=False)
|
|
& ~Q(transferred_to_department=department)
|
|
)
|
|
if direction == "outgoing":
|
|
queryset = Inquiry.objects.filter(sent_away)
|
|
else:
|
|
queryset = Inquiry.objects.filter(
|
|
Q(department=department, sent_to_department=True)
|
|
| Q(outgoing_department=department)
|
|
| Q(transferred_to_department=department)
|
|
).exclude(sent_away)
|
|
|
|
queryset = queryset.select_related("assigned_to", "patient").order_by("-created_at")
|
|
|
|
from django.db.models import Count
|
|
list_stats = queryset.values("status").annotate(count=Count("pk"))
|
|
status_counts = {s["status"]: s["count"] for s in list_stats}
|
|
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
priority_filter = request.GET.get("priority")
|
|
if priority_filter:
|
|
queryset = queryset.filter(priority=priority_filter)
|
|
|
|
search_query = request.GET.get("search", "").strip()
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(reference_number__icontains=search_query)
|
|
| Q(subject__icontains=search_query)
|
|
| Q(contact_name__icontains=search_query)
|
|
)
|
|
|
|
date_from = request.GET.get("date_from")
|
|
date_to = request.GET.get("date_to")
|
|
if date_from:
|
|
queryset = queryset.filter(created_at__date__gte=date_from)
|
|
if date_to:
|
|
queryset = queryset.filter(created_at__date__lte=date_to)
|
|
|
|
can_respond = (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or (user.is_department_manager() and user.department == department)
|
|
or (user.is_champion() and user.department == department)
|
|
)
|
|
|
|
page_size = int(request.GET.get("page_size", 25))
|
|
paginator = Paginator(queryset, page_size)
|
|
page_obj = paginator.get_page(request.GET.get("page", 1))
|
|
|
|
context = {
|
|
"department": department,
|
|
"page_obj": page_obj,
|
|
"inquiries": page_obj,
|
|
"status_filter": status_filter,
|
|
"priority_filter": priority_filter or "",
|
|
"search_query": search_query,
|
|
"date_from": date_from or "",
|
|
"date_to": date_to or "",
|
|
"can_respond": can_respond,
|
|
"list_stats": status_counts,
|
|
"direction": direction,
|
|
"status_labels": {"open": _("Open"), "in_progress": _("In Progress"), "resolved": _("Resolved"), "closed": _("Closed"), "contact_no_response": _("Contacted - No Response")},
|
|
}
|
|
return render(request, "organizations/department_inquiries.html", context)
|
|
|
|
|
|
@login_required
|
|
def department_observations_list(request, pk):
|
|
from .models import Department
|
|
from apps.observations.models import Observation
|
|
from django.core.paginator import Paginator
|
|
|
|
department = get_object_or_404(
|
|
Department.objects.select_related("hospital"), pk=pk
|
|
)
|
|
user = request.user
|
|
if not _check_department_access(user, department):
|
|
messages.error(request, _("You don't have permission to view this department."))
|
|
return redirect("organizations:department_list")
|
|
|
|
queryset = Observation.objects.filter(
|
|
assigned_department=department, sent_to_department=True
|
|
).select_related("category", "assigned_to").order_by("-created_at")
|
|
|
|
from django.db.models import Count
|
|
list_stats = queryset.values("status").annotate(count=Count("pk"))
|
|
status_counts = {s["status"]: s["count"] for s in list_stats}
|
|
|
|
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)
|
|
|
|
search_query = request.GET.get("search", "").strip()
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(tracking_code__icontains=search_query) | Q(title__icontains=search_query)
|
|
)
|
|
|
|
date_from = request.GET.get("date_from")
|
|
date_to = request.GET.get("date_to")
|
|
if date_from:
|
|
queryset = queryset.filter(created_at__date__gte=date_from)
|
|
if date_to:
|
|
queryset = queryset.filter(created_at__date__lte=date_to)
|
|
|
|
can_respond = (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or (user.is_department_manager() and user.department == department)
|
|
or (user.is_champion() and user.department == department)
|
|
)
|
|
|
|
page_size = int(request.GET.get("page_size", 25))
|
|
paginator = Paginator(queryset, page_size)
|
|
page_obj = paginator.get_page(request.GET.get("page", 1))
|
|
|
|
context = {
|
|
"department": department,
|
|
"page_obj": page_obj,
|
|
"observations": page_obj,
|
|
"status_filter": status_filter,
|
|
"severity_filter": severity_filter or "",
|
|
"search_query": search_query,
|
|
"date_from": date_from or "",
|
|
"date_to": date_to or "",
|
|
"can_respond": can_respond,
|
|
"list_stats": status_counts,
|
|
"status_labels": {"new": _("New"), "triaged": _("Triaged"), "in_progress": _("In Progress"), "resolved": _("Resolved"), "closed": _("Closed")},
|
|
}
|
|
return render(request, "organizations/department_observations.html", context)
|
|
|
|
|
|
@login_required
|
|
def department_complaint_detail(request, pk, cpk):
|
|
from .models import Department
|
|
from apps.complaints.models import Complaint, ComplaintInvolvedDepartment
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
department = get_object_or_404(
|
|
Department.objects.select_related("hospital"), pk=pk
|
|
)
|
|
if not _check_department_access(request.user, department):
|
|
messages.error(request, _("You don't have permission to view this department."))
|
|
return redirect("organizations:department_list")
|
|
|
|
complaint = get_object_or_404(
|
|
Complaint.objects.select_related(
|
|
"patient", "hospital", "department", "staff", "assigned_to",
|
|
"category", "domain", "subcategory_obj", "classification_obj",
|
|
"source", "legacy_location", "legacy_main_section", "legacy_subsection",
|
|
).prefetch_related(
|
|
"involved_staff__staff", "involved_departments__department",
|
|
),
|
|
pk=cpk,
|
|
)
|
|
|
|
belongs = (
|
|
(complaint.department_id == department.pk and complaint.sent_to_department)
|
|
or complaint.involved_departments.filter(department=department, sent=True).exists()
|
|
)
|
|
if not belongs:
|
|
messages.error(request, _("This complaint does not belong to this department."))
|
|
return redirect("organizations:department_complaints_list", pk=pk)
|
|
|
|
user = request.user
|
|
involved_dept = complaint.involved_departments.filter(department=department, sent=True).first()
|
|
can_respond = (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or (user.is_department_manager() and user.department == department)
|
|
or (user.is_champion() and user.department == department)
|
|
) and involved_dept and not involved_dept.response_submitted
|
|
|
|
can_manager_review = (
|
|
(user.is_department_manager() and user.department == department)
|
|
or user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
) and involved_dept and involved_dept.response_submitted and involved_dept.manager_review_status == "pending"
|
|
|
|
taxonomy = []
|
|
if complaint.domain:
|
|
taxonomy.append(complaint.domain.get_localized_name())
|
|
if complaint.category:
|
|
taxonomy.append(complaint.category.get_localized_name())
|
|
if complaint.subcategory_obj:
|
|
taxonomy.append(complaint.subcategory_obj.get_localized_name())
|
|
if complaint.classification_obj:
|
|
taxonomy.append(complaint.classification_obj.get_localized_name())
|
|
|
|
staff_list = []
|
|
for inv in complaint.involved_staff.all():
|
|
if inv.staff:
|
|
staff_list.append(inv.staff.get_localized_name())
|
|
if not staff_list and complaint.staff:
|
|
staff_list.append(complaint.staff.get_localized_name())
|
|
|
|
location_parts = []
|
|
if complaint.legacy_location:
|
|
location_parts.append(str(complaint.legacy_location))
|
|
if complaint.legacy_main_section:
|
|
location_parts.append(str(complaint.legacy_main_section))
|
|
if complaint.legacy_subsection:
|
|
location_parts.append(str(complaint.legacy_subsection))
|
|
|
|
complaint_ct = ContentType.objects.get_for_model(Complaint)
|
|
notes = complaint.notes.select_related("created_by").order_by("-created_at")
|
|
|
|
explanation_url = None
|
|
if can_respond and involved_dept:
|
|
from apps.complaints.models import ComplaintExplanation
|
|
explanation = ComplaintExplanation.objects.filter(
|
|
complaint=complaint,
|
|
staff__department=department,
|
|
).first()
|
|
if not explanation and department.champion:
|
|
import secrets as _secrets
|
|
explanation = ComplaintExplanation.objects.create(
|
|
complaint=complaint,
|
|
staff=department.champion,
|
|
token=_secrets.token_urlsafe(32),
|
|
explanation="",
|
|
is_used=False,
|
|
)
|
|
if explanation:
|
|
needs_resubmit = (
|
|
involved_dept.acceptance_status == "not_acceptable"
|
|
or involved_dept.manager_review_status == "rejected"
|
|
)
|
|
if explanation.is_used and needs_resubmit:
|
|
import secrets
|
|
explanation.is_used = False
|
|
explanation.token = secrets.token_urlsafe(32)
|
|
explanation.save(update_fields=["is_used", "token"])
|
|
if not explanation.is_used:
|
|
explanation_url = reverse(
|
|
"complaints:complaint_explanation_form",
|
|
kwargs={"complaint_id": complaint.id, "token": explanation.token},
|
|
)
|
|
|
|
from apps.complaints.workflow_log import build_workflow_log
|
|
|
|
context = {
|
|
"department": department,
|
|
"complaint": complaint,
|
|
"taxonomy": taxonomy,
|
|
"staff_list": staff_list,
|
|
"location_str": " > ".join(location_parts) if location_parts else "",
|
|
"can_respond": can_respond,
|
|
"can_manager_review": can_manager_review,
|
|
"involved_dept": involved_dept,
|
|
"explanation_url": explanation_url,
|
|
"notes": notes,
|
|
"content_type_id": complaint_ct.pk,
|
|
"object_id": complaint.pk,
|
|
"workflow_log": build_workflow_log(complaint),
|
|
}
|
|
return render(request, "organizations/department_complaint_detail.html", context)
|
|
|
|
|
|
@login_required
|
|
def department_inquiry_detail(request, pk, ipk):
|
|
from .models import Department
|
|
from apps.complaints.models import Inquiry
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
department = get_object_or_404(
|
|
Department.objects.select_related("hospital"), pk=pk
|
|
)
|
|
if not _check_department_access(request.user, department):
|
|
messages.error(request, _("You don't have permission to view this department."))
|
|
return redirect("organizations:department_list")
|
|
|
|
inquiry = get_object_or_404(
|
|
Inquiry.objects.select_related(
|
|
"patient", "hospital", "department", "assigned_to",
|
|
"outgoing_department", "legacy_location", "legacy_main_section", "legacy_subsection",
|
|
"department_responded_by",
|
|
),
|
|
pk=ipk,
|
|
)
|
|
|
|
belongs = (
|
|
(inquiry.department_id == department.pk and inquiry.sent_to_department)
|
|
or inquiry.outgoing_department_id == department.pk
|
|
)
|
|
if not belongs:
|
|
messages.error(request, _("This inquiry does not belong to this department."))
|
|
return redirect("organizations:department_inquiries_list", pk=pk)
|
|
|
|
user = request.user
|
|
can_respond = (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or (user.is_department_manager() and user.department_id == inquiry.handling_department_id)
|
|
or (user.is_champion() and user.department_id == inquiry.handling_department_id)
|
|
)
|
|
|
|
location_parts = []
|
|
if inquiry.legacy_location:
|
|
location_parts.append(str(inquiry.legacy_location))
|
|
if inquiry.legacy_main_section:
|
|
location_parts.append(str(inquiry.legacy_main_section))
|
|
if inquiry.legacy_subsection:
|
|
location_parts.append(str(inquiry.legacy_subsection))
|
|
|
|
inquiry_ct = ContentType.objects.get_for_model(Inquiry)
|
|
notes = inquiry.notes.select_related("created_by").order_by("-created_at")
|
|
|
|
departments = (
|
|
Department.objects.filter(hospital=department.hospital, status="active")
|
|
.exclude(pk=department.pk)
|
|
.order_by("name_en")
|
|
)
|
|
users = User.objects.filter(hospital=department.hospital, is_active=True)
|
|
|
|
context = {
|
|
"department": department,
|
|
"inquiry": inquiry,
|
|
"location_str": " > ".join(location_parts) if location_parts else "",
|
|
"can_respond": can_respond,
|
|
"notes": notes,
|
|
"departments": departments,
|
|
"users": users,
|
|
"content_type_id": inquiry_ct.pk,
|
|
"object_id": inquiry.pk,
|
|
}
|
|
return render(request, "organizations/department_inquiry_detail.html", context)
|
|
|
|
|
|
@login_required
|
|
def department_observation_detail(request, pk, opk):
|
|
from .models import Department
|
|
from apps.observations.models import Observation
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.utils import timezone
|
|
|
|
department = get_object_or_404(
|
|
Department.objects.select_related("hospital"), pk=pk
|
|
)
|
|
if not _check_department_access(request.user, department):
|
|
messages.error(request, _("You don't have permission to view this department."))
|
|
return redirect("organizations:department_list")
|
|
|
|
observation = get_object_or_404(
|
|
Observation.objects.select_related(
|
|
"category", "assigned_department", "assigned_to",
|
|
"legacy_location", "legacy_main_section", "legacy_subsection", "hospital",
|
|
"department_responded_by",
|
|
),
|
|
pk=opk,
|
|
)
|
|
|
|
if observation.assigned_department_id != department.pk or not observation.sent_to_department:
|
|
messages.error(request, _("This observation does not belong to this department."))
|
|
return redirect("organizations:department_observations_list", pk=pk)
|
|
|
|
user = request.user
|
|
can_respond = (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or (user.is_department_manager() and user.department == department)
|
|
or (user.is_champion() and user.department == department)
|
|
)
|
|
|
|
obs_ct = ContentType.objects.get_for_model(Observation)
|
|
notes = observation.notes.select_related("created_by").order_by("-created_at")
|
|
|
|
context = {
|
|
"department": department,
|
|
"observation": observation,
|
|
"can_respond": can_respond,
|
|
"notes": notes,
|
|
"now": timezone.now(),
|
|
"content_type_id": obs_ct.pk,
|
|
"object_id": observation.pk,
|
|
}
|
|
return render(request, "organizations/department_observation_detail.html", context)
|
|
|
|
|
|
@login_required
|
|
def department_staff_detail(request, pk, spk):
|
|
from .models import Department
|
|
from apps.complaints.models import Complaint, ComplaintInvolvedStaff
|
|
from django.db.models import Q
|
|
|
|
department = get_object_or_404(
|
|
Department.objects.select_related("hospital"), pk=pk
|
|
)
|
|
if not _check_department_access(request.user, department):
|
|
messages.error(request, _("You don't have permission to view this department."))
|
|
return redirect("organizations:department_list")
|
|
|
|
staff = get_object_or_404(
|
|
Staff.objects.select_related("user", "department", "section_fk", "subsection_fk", "report_to"),
|
|
pk=spk,
|
|
department=department,
|
|
status="active",
|
|
)
|
|
|
|
complaint_q = Q(
|
|
Q(department=department, sent_to_department=True)
|
|
| Q(involved_departments__department=department, involved_departments__sent=True)
|
|
)
|
|
|
|
direct_complaints = Complaint.objects.filter(
|
|
staff=staff, sent_to_department=True,
|
|
).filter(
|
|
Q(department=department) | Q(involved_departments__department=department),
|
|
).select_related("patient", "assigned_to", "category").distinct()
|
|
|
|
involved_records = ComplaintInvolvedStaff.objects.filter(
|
|
staff=staff,
|
|
complaint__sent_to_department=True,
|
|
).filter(
|
|
Q(complaint__department=department) | Q(complaint__involved_departments__department=department),
|
|
).select_related("complaint__patient", "complaint__assigned_to", "complaint__category").distinct()
|
|
|
|
complaint_rows = []
|
|
seen_ids = set()
|
|
for c in direct_complaints:
|
|
complaint_rows.append({"complaint": c, "role": _("Primary")})
|
|
seen_ids.add(c.id)
|
|
for inv in involved_records:
|
|
if inv.complaint_id not in seen_ids:
|
|
complaint_rows.append({"complaint": inv.complaint, "role": inv.get_role_display()})
|
|
seen_ids.add(inv.complaint_id)
|
|
complaint_rows.sort(key=lambda r: r["complaint"].created_at, reverse=True)
|
|
|
|
all_complaints = [r["complaint"] for r in complaint_rows]
|
|
complaint_counts = {
|
|
"total": len(all_complaints),
|
|
"open": sum(1 for c in all_complaints if c.status == "open"),
|
|
"in_progress": sum(1 for c in all_complaints if c.status == "in_progress"),
|
|
"resolved": sum(1 for c in all_complaints if c.status in ("resolved", "partially_resolved")),
|
|
"closed": sum(1 for c in all_complaints if c.status == "closed"),
|
|
}
|
|
|
|
direct_reports = Staff.objects.filter(
|
|
report_to=staff, department=department, status="active",
|
|
).order_by("first_name")
|
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from apps.appreciation.models import Appreciation
|
|
|
|
recipient_ctypes = [ContentType.objects.get_for_model(Staff)]
|
|
recipient_ids = [staff.pk]
|
|
if staff.user:
|
|
recipient_ctypes.append(ContentType.objects.get_for_model(staff.user))
|
|
recipient_ids.append(staff.user.pk)
|
|
|
|
appreciations = Appreciation.objects.filter(
|
|
department=department,
|
|
recipient_content_type__in=recipient_ctypes,
|
|
recipient_object_id__in=recipient_ids,
|
|
).select_related("sender", "category").order_by("-created_at")
|
|
|
|
context = {
|
|
"department": department,
|
|
"staff": staff,
|
|
"complaint_rows": complaint_rows,
|
|
"complaint_counts": complaint_counts,
|
|
"direct_reports": direct_reports,
|
|
"appreciations": appreciations,
|
|
"can_edit_staff": (
|
|
request.user.is_px_admin()
|
|
or (request.user.is_hospital_admin() and request.user.hospital == department.hospital)
|
|
or (request.user.is_champion() and request.user.department == department)
|
|
or (request.user.is_department_manager() and request.user.department == department)
|
|
),
|
|
}
|
|
return render(request, "organizations/department_staff_detail.html", context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def department_staff_update(request, pk, spk):
|
|
from .models import Department
|
|
|
|
department = get_object_or_404(Department.objects.select_related("hospital"), pk=pk)
|
|
user = request.user
|
|
if not (
|
|
user.is_px_admin()
|
|
or (user.is_hospital_admin() and user.hospital == department.hospital)
|
|
or (user.is_champion() and user.department == department)
|
|
or (user.is_department_manager() and user.department == department)
|
|
):
|
|
messages.error(request, _("You don't have permission to update staff info."))
|
|
return redirect("organizations:department_staff_detail", pk=pk, spk=spk)
|
|
|
|
staff = get_object_or_404(
|
|
Staff.objects.select_related("department"),
|
|
pk=spk,
|
|
department=department,
|
|
status="active",
|
|
)
|
|
|
|
EDITABLE_FIELDS = [
|
|
"first_name", "last_name", "first_name_ar", "last_name_ar",
|
|
"job_title", "job_title_ar", "email", "phone",
|
|
"specialization", "license_number", "gender",
|
|
]
|
|
|
|
changed = []
|
|
for field in EDITABLE_FIELDS:
|
|
value = request.POST.get(field, "").strip()
|
|
old = getattr(staff, field) or ""
|
|
if field in ("first_name", "last_name"):
|
|
if not value:
|
|
messages.error(request, _(f"{field.replace('_', ' ').title()} is required."))
|
|
return redirect("organizations:department_staff_detail", pk=pk, spk=spk)
|
|
setattr(staff, field, value)
|
|
if value != old:
|
|
changed.append(field)
|
|
|
|
if changed:
|
|
staff.save(update_fields=changed)
|
|
messages.success(request, _("Staff info updated successfully."))
|
|
else:
|
|
messages.info(request, _("No changes detected."))
|
|
|
|
return redirect("organizations:department_staff_detail", pk=pk, spk=spk)
|
|
|
|
|
|
@login_required
|
|
def department_record_complaint(request, pk):
|
|
from django.http import JsonResponse
|
|
from apps.complaints.models import Complaint
|
|
|
|
complaint = get_object_or_404(
|
|
Complaint.objects.select_related(
|
|
"hospital", "department", "assigned_to", "category", "domain",
|
|
"subcategory_obj", "classification_obj", "legacy_location", "legacy_main_section",
|
|
"legacy_subsection", "patient", "staff", "source",
|
|
).prefetch_related("involved_staff__staff"),
|
|
pk=pk,
|
|
)
|
|
|
|
department = get_object_or_404(Department, pk=pk if False else complaint.department_id)
|
|
user = request.user
|
|
if not (
|
|
user.is_px_admin()
|
|
or (user.is_hospital_admin() and user.hospital == complaint.hospital)
|
|
or (user.is_champion() and user.department_id == complaint.department_id)
|
|
or (user.is_department_manager() and user.department_id == complaint.department_id)
|
|
or (user.is_basic_staff() and user.department_id == complaint.department_id)
|
|
or (user.is_director() and user.get_directed_departments().filter(id=complaint.department_id).exists())
|
|
):
|
|
return JsonResponse({"error": "Access denied"}, status=403)
|
|
|
|
taxonomy = []
|
|
if complaint.domain:
|
|
taxonomy.append(complaint.domain.get_localized_name())
|
|
if complaint.category:
|
|
taxonomy.append(complaint.category.get_localized_name())
|
|
if complaint.subcategory_obj:
|
|
taxonomy.append(complaint.subcategory_obj.get_localized_name())
|
|
if complaint.classification_obj:
|
|
taxonomy.append(complaint.classification_obj.get_localized_name())
|
|
|
|
staff_list = []
|
|
for inv in complaint.involved_staff.all():
|
|
staff_list.append(inv.staff.get_localized_name() if inv.staff else "-")
|
|
if not staff_list and complaint.staff:
|
|
staff_list.append(complaint.staff.get_localized_name())
|
|
|
|
location_parts = []
|
|
if complaint.legacy_location:
|
|
location_parts.append(str(complaint.legacy_location))
|
|
if complaint.legacy_main_section:
|
|
location_parts.append(str(complaint.legacy_main_section))
|
|
if complaint.legacy_subsection:
|
|
location_parts.append(str(complaint.legacy_subsection))
|
|
|
|
data = {
|
|
"type": "complaint",
|
|
"reference": complaint.reference_number or "",
|
|
"title": complaint.title or "",
|
|
"description": complaint.description or "",
|
|
"status": complaint.status,
|
|
"status_display": complaint.get_status_display(),
|
|
"severity": complaint.severity,
|
|
"severity_display": complaint.get_severity_display(),
|
|
"department": complaint.department.get_localized_name() if complaint.department else "-",
|
|
"assigned_to": complaint.assigned_to.get_full_name() if complaint.assigned_to else "-",
|
|
"patient": complaint.patient.get_full_name() if complaint.patient else (complaint.patient_name or "-"),
|
|
"patient_mrn": complaint.patient.mrn if complaint.patient else "-",
|
|
"staff": staff_list,
|
|
"taxonomy": taxonomy,
|
|
"location": " > ".join(location_parts) if location_parts else "-",
|
|
"source": complaint.source.name_en if complaint.source else "-",
|
|
"created_at": complaint.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
"due_at": complaint.due_at.strftime("%Y-%m-%d %H:%M") if complaint.due_at else None,
|
|
"is_overdue": complaint.is_overdue,
|
|
"escalated": bool(complaint.escalated_at),
|
|
"expected_result": complaint.expected_result or "",
|
|
"ai_brief": complaint.ai_brief_en or "",
|
|
}
|
|
return JsonResponse(data)
|
|
|
|
|
|
@login_required
|
|
def department_record_inquiry(request, pk):
|
|
from django.http import JsonResponse
|
|
from apps.complaints.models import Inquiry
|
|
|
|
inquiry = get_object_or_404(
|
|
Inquiry.objects.select_related(
|
|
"hospital", "department", "assigned_to", "legacy_location", "legacy_main_section",
|
|
"legacy_subsection", "outgoing_department",
|
|
),
|
|
pk=pk,
|
|
)
|
|
|
|
user = request.user
|
|
dept_id = inquiry.department_id or inquiry.outgoing_department_id
|
|
if not (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or user.is_px_management()
|
|
or user.is_px_employee()
|
|
or (user.is_champion() and user.department and (user.department_id == inquiry.department_id or user.department_id == inquiry.outgoing_department_id))
|
|
or user.is_department_manager()
|
|
or user.is_director()
|
|
):
|
|
return JsonResponse({"error": "Access denied"}, status=403)
|
|
|
|
location_parts = []
|
|
if inquiry.legacy_location:
|
|
location_parts.append(str(inquiry.legacy_location))
|
|
if inquiry.legacy_main_section:
|
|
location_parts.append(str(inquiry.legacy_main_section))
|
|
if inquiry.legacy_subsection:
|
|
location_parts.append(str(inquiry.legacy_subsection))
|
|
|
|
data = {
|
|
"type": "inquiry",
|
|
"reference": inquiry.reference_number or "",
|
|
"title": inquiry.subject or "",
|
|
"description": inquiry.message or "",
|
|
"status": inquiry.status,
|
|
"status_display": inquiry.get_status_display(),
|
|
"department": inquiry.department.get_localized_name() if inquiry.department else "-",
|
|
"assigned_to": inquiry.assigned_to.get_full_name() if inquiry.assigned_to else "-",
|
|
"contact_name": inquiry.contact_name or "-",
|
|
"contact_phone": inquiry.contact_phone or "-",
|
|
"contact_email": inquiry.contact_email or "-",
|
|
"category": inquiry.get_category_display() if inquiry.category else "-",
|
|
"location": " > ".join(location_parts) if location_parts else "-",
|
|
"created_at": inquiry.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
"due_at": inquiry.due_at.strftime("%Y-%m-%d %H:%M") if hasattr(inquiry, 'due_at') and inquiry.due_at else None,
|
|
"ai_summary": inquiry.short_description_en if hasattr(inquiry, 'short_description_en') else "",
|
|
"department_response_en": inquiry.department_response_en or "",
|
|
"department_response_ar": inquiry.department_response_ar or "",
|
|
"department_responded_at": inquiry.department_responded_at.strftime("%Y-%m-%d %H:%M") if inquiry.department_responded_at else None,
|
|
"priority_display": inquiry.get_priority_display() if hasattr(inquiry, 'priority') and inquiry.priority else "",
|
|
"contact_status_display": inquiry.get_contact_status_display() if inquiry.contact_status else "",
|
|
"response": inquiry.response or inquiry.response_en or "",
|
|
"responded_at": inquiry.responded_at.strftime("%Y-%m-%d %H:%M") if inquiry.responded_at else None,
|
|
"responded_by": inquiry.responded_by.get_full_name() if inquiry.responded_by else None,
|
|
"resolved_at": inquiry.resolved_at.strftime("%Y-%m-%d %H:%M") if inquiry.resolved_at else None,
|
|
"resolved_by": inquiry.resolved_by.get_full_name() if inquiry.resolved_by else None,
|
|
"outgoing_department": inquiry.outgoing_department.get_localized_name() if inquiry.outgoing_department else None,
|
|
}
|
|
return JsonResponse(data)
|
|
|
|
|
|
@login_required
|
|
def department_record_observation(request, pk):
|
|
from django.http import JsonResponse
|
|
from apps.observations.models import Observation
|
|
|
|
observation = get_object_or_404(
|
|
Observation.objects.select_related(
|
|
"hospital", "assigned_department", "assigned_to", "category",
|
|
"legacy_location", "legacy_main_section", "legacy_subsection",
|
|
),
|
|
pk=pk,
|
|
)
|
|
|
|
user = request.user
|
|
dept_id = observation.assigned_department_id
|
|
if not (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or user.is_px_management()
|
|
or user.is_px_employee()
|
|
or (user.is_champion() and user.department and user.department_id == observation.assigned_department_id)
|
|
or user.is_department_manager()
|
|
or user.is_director()
|
|
):
|
|
return JsonResponse({"error": "Access denied"}, status=403)
|
|
|
|
location_parts = []
|
|
if observation.legacy_location:
|
|
location_parts.append(str(observation.legacy_location))
|
|
if observation.legacy_main_section:
|
|
location_parts.append(str(observation.legacy_main_section))
|
|
if observation.legacy_subsection:
|
|
location_parts.append(str(observation.legacy_subsection))
|
|
|
|
reporter = ""
|
|
if observation.is_anonymous:
|
|
reporter = str(_("Anonymous"))
|
|
elif observation.reporter_name:
|
|
reporter = observation.reporter_name
|
|
|
|
data = {
|
|
"type": "observation",
|
|
"reference": observation.tracking_code or "",
|
|
"title": observation.title or "",
|
|
"description": observation.description or "",
|
|
"status": observation.status,
|
|
"status_display": observation.get_status_display(),
|
|
"severity": observation.severity,
|
|
"severity_display": observation.get_severity_display(),
|
|
"department": observation.assigned_department.get_localized_name() if observation.assigned_department else "-",
|
|
"assigned_to": observation.assigned_to.get_full_name() if observation.assigned_to else "-",
|
|
"category": observation.category.get_localized_name() if observation.category else "-",
|
|
"location": " > ".join(location_parts) if location_parts else "-",
|
|
"reporter": reporter,
|
|
"is_anonymous": observation.is_anonymous,
|
|
"created_at": observation.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
"due_at": observation.due_at.strftime("%Y-%m-%d %H:%M") if observation.due_at else None,
|
|
"is_overdue": observation.is_overdue if hasattr(observation, 'is_overdue') else False,
|
|
"ai_summary": observation.short_description_en if hasattr(observation, 'short_description_en') else "",
|
|
}
|
|
return JsonResponse(data)
|
|
|
|
|
|
@login_required
|
|
def department_record_suggestion(request, pk):
|
|
from django.http import JsonResponse
|
|
from apps.feedback.models import Feedback
|
|
|
|
feedback = get_object_or_404(
|
|
Feedback.objects.select_related(
|
|
"hospital", "department", "assigned_to", "staff",
|
|
"legacy_location", "legacy_main_section", "legacy_subsection",
|
|
),
|
|
pk=pk,
|
|
)
|
|
|
|
user = request.user
|
|
if not (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or user.is_px_management()
|
|
or user.is_px_employee()
|
|
or (user.is_champion() and user.department and user.department_id == feedback.department_id)
|
|
or user.is_department_manager()
|
|
or user.is_director()
|
|
):
|
|
return JsonResponse({"error": "Access denied"}, status=403)
|
|
|
|
location_parts = []
|
|
if feedback.legacy_location:
|
|
location_parts.append(str(feedback.legacy_location))
|
|
if feedback.legacy_main_section:
|
|
location_parts.append(str(feedback.legacy_main_section))
|
|
if feedback.legacy_subsection:
|
|
location_parts.append(str(feedback.legacy_subsection))
|
|
|
|
data = {
|
|
"type": "suggestion",
|
|
"reference": str(feedback.id)[:8].upper() if feedback.id else "",
|
|
"title": feedback.title or "",
|
|
"description": feedback.message or "",
|
|
"status": feedback.status,
|
|
"status_display": feedback.get_status_display(),
|
|
"priority": feedback.priority,
|
|
"priority_display": feedback.get_priority_display(),
|
|
"department": feedback.department.get_localized_name() if feedback.department else "-",
|
|
"assigned_to": feedback.assigned_to.get_full_name() if feedback.assigned_to else "-",
|
|
"category": feedback.get_category_display() if feedback.category else "-",
|
|
"contact_name": feedback.contact_name if not feedback.is_anonymous else str(_("Anonymous")),
|
|
"location": " > ".join(location_parts) if location_parts else "-",
|
|
"created_at": feedback.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
"sentiment": feedback.get_sentiment_display() if hasattr(feedback, 'get_sentiment_display') else "",
|
|
}
|
|
return JsonResponse(data)
|
|
|
|
|
|
@login_required
|
|
def department_record_project(request, pk):
|
|
"""AJAX endpoint returning a QI project summary for the department-detail modal."""
|
|
from django.http import JsonResponse
|
|
from apps.projects.models import QIProject
|
|
|
|
project = get_object_or_404(
|
|
QIProject.objects.select_related("hospital", "project_lead").prefetch_related(
|
|
"departments", "team_members", "tasks"
|
|
),
|
|
pk=pk,
|
|
is_template=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()
|
|
or user.is_department_manager()
|
|
or user.is_director()
|
|
or (
|
|
user.is_champion()
|
|
and project.departments.filter(champion_id=user.staff_profile.id).exists()
|
|
)
|
|
):
|
|
return JsonResponse({"error": "Access denied"}, status=403)
|
|
|
|
tasks_total = project.tasks.count()
|
|
tasks_completed = project.tasks.filter(status="completed").count()
|
|
tasks_pending = project.tasks.exclude(status__in=["completed", "cancelled"]).count()
|
|
|
|
data = {
|
|
"type": "project",
|
|
"reference": str(project.id)[:8].upper(),
|
|
"title": project.name or "",
|
|
"description": project.description or "",
|
|
"status": project.status,
|
|
"status_display": project.get_status_display(),
|
|
"lead": project.project_lead.get_full_name() if project.project_lead else "-",
|
|
"departments": [d.get_localized_name() for d in project.departments.all()],
|
|
"team_count": project.team_members.count(),
|
|
"tasks_total": tasks_total,
|
|
"tasks_completed": tasks_completed,
|
|
"tasks_pending": tasks_pending,
|
|
"start_date": project.start_date.strftime("%Y-%m-%d") if project.start_date else "-",
|
|
"target_completion_date": project.target_completion_date.strftime("%Y-%m-%d") if project.target_completion_date else "-",
|
|
"created_at": project.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
"detail_url": reverse("projects:project_detail", args=[project.pk]),
|
|
}
|
|
return JsonResponse(data)
|
|
|
|
|
|
@login_required
|
|
def department_record_appreciation(request, pk):
|
|
from django.http import JsonResponse
|
|
from apps.appreciation.models import Appreciation
|
|
|
|
appreciation = get_object_or_404(
|
|
Appreciation.objects.select_related(
|
|
"hospital", "department", "sender", "category",
|
|
"legacy_location", "legacy_main_section", "legacy_subsection",
|
|
),
|
|
pk=pk,
|
|
)
|
|
|
|
user = request.user
|
|
if not (
|
|
user.is_px_admin()
|
|
or user.is_hospital_admin()
|
|
or user.is_px_management()
|
|
or user.is_px_employee()
|
|
or (user.is_champion() and user.department and user.department_id == appreciation.department_id)
|
|
or user.is_department_manager()
|
|
or user.is_director()
|
|
):
|
|
return JsonResponse({"error": "Access denied"}, status=403)
|
|
|
|
location_parts = []
|
|
if appreciation.legacy_location:
|
|
location_parts.append(str(appreciation.legacy_location))
|
|
if appreciation.legacy_main_section:
|
|
location_parts.append(str(appreciation.legacy_main_section))
|
|
if appreciation.legacy_subsection:
|
|
location_parts.append(str(appreciation.legacy_subsection))
|
|
|
|
data = {
|
|
"type": "appreciation",
|
|
"reference": str(appreciation.id)[:8].upper() if appreciation.id else "",
|
|
"title": "",
|
|
"description": appreciation.message_en or "",
|
|
"description_ar": appreciation.message_ar or "",
|
|
"status": appreciation.status,
|
|
"status_display": appreciation.get_status_display(),
|
|
"department": appreciation.department.get_localized_name() if appreciation.department else "-",
|
|
"sender": appreciation.sender.get_full_name() if appreciation.sender and not appreciation.is_anonymous else str(_("Anonymous")),
|
|
"recipient": appreciation.get_recipient_name(),
|
|
"category": appreciation.category.name_en if appreciation.category else "-",
|
|
"category_color": appreciation.category.color if appreciation.category else "",
|
|
"location": " > ".join(location_parts) if location_parts else "-",
|
|
"visibility": appreciation.get_visibility_display(),
|
|
"is_anonymous": appreciation.is_anonymous,
|
|
"created_at": appreciation.created_at.strftime("%Y-%m-%d %H:%M"),
|
|
"sent_at": appreciation.sent_at.strftime("%Y-%m-%d %H:%M") if appreciation.sent_at else None,
|
|
"acknowledged_at": appreciation.acknowledged_at.strftime("%Y-%m-%d %H:%M") if appreciation.acknowledged_at else None,
|
|
}
|
|
return JsonResponse(data)
|
|
|
|
|
|
|
|
@login_required
|
|
def department_analytics_api(request, pk):
|
|
from .models import Department
|
|
from apps.complaints.models import Complaint, ComplaintInvolvedStaff, ComplaintUpdate
|
|
from apps.physicians.models import PhysicianMonthlyRating
|
|
from apps.px_action_center.models import PXAction
|
|
from django.db.models import Count, Avg, Sum, Q, F
|
|
from django.db.models.functions import TruncMonth
|
|
from django.utils import timezone
|
|
from django.http import JsonResponse
|
|
from datetime import timedelta
|
|
import traceback
|
|
|
|
try:
|
|
department = get_object_or_404(Department, pk=pk)
|
|
|
|
user = request.user
|
|
if not (
|
|
user.is_px_admin()
|
|
or (user.is_hospital_admin() and user.hospital == department.hospital)
|
|
or (user.is_department_manager() and user.department in [department, department.parent])
|
|
or (
|
|
(user.is_champion() or user.is_department_manager())
|
|
and user.department == department
|
|
)
|
|
or (user.is_basic_staff() and user.department == department)
|
|
or (user.hospital == department.hospital and not user.is_source_user() and not user.is_basic_staff())
|
|
):
|
|
return JsonResponse({"error": "Permission denied"}, status=403)
|
|
|
|
now = timezone.now()
|
|
twelve_months_ago = now - timedelta(days=365)
|
|
six_months_ago = now - timedelta(days=180)
|
|
|
|
complaints_qs = Complaint.objects.filter(
|
|
Q(department=department, sent_to_department=True) | Q(involved_departments__department=department, involved_departments__sent=True)
|
|
).distinct()
|
|
|
|
complaint_ids = complaints_qs.values_list("pk", flat=True)
|
|
complaints_clean = Complaint.objects.filter(pk__in=complaint_ids)
|
|
|
|
# 1. Complaint Trend (last 12 months)
|
|
complaint_trend_raw = (
|
|
complaints_clean.filter(created_at__gte=twelve_months_ago)
|
|
.annotate(month=TruncMonth("created_at"))
|
|
.values("month")
|
|
.annotate(count=Count("pk"))
|
|
.order_by("month")
|
|
)
|
|
complaint_trend = [
|
|
{"period": item["month"].strftime("%Y-%m"), "count": item["count"]}
|
|
for item in complaint_trend_raw
|
|
]
|
|
|
|
# 2. Complaint Status Distribution
|
|
status_raw = complaints_clean.values("status").annotate(count=Count("pk"))
|
|
status_labels = {
|
|
"open": "Open",
|
|
"in_progress": "In Progress",
|
|
"partially_resolved": "Partially Resolved",
|
|
"resolved": "Resolved",
|
|
"closed": "Closed",
|
|
"cancelled": "Cancelled",
|
|
"contacted": "Contacted",
|
|
"contacted_no_response": "No Response",
|
|
}
|
|
complaint_status = [
|
|
{"status": status_labels.get(s["status"], s["status"]), "count": s["count"]}
|
|
for s in status_raw
|
|
if s["count"] > 0
|
|
]
|
|
|
|
# 3. Physician Rating Trend (last 6 months)
|
|
physician_ratings_qs = PhysicianMonthlyRating.objects.filter(
|
|
staff__department=department, staff__physician=True
|
|
)
|
|
rating_trend_raw = []
|
|
for i in range(5, -1, -1):
|
|
m = now.month - i
|
|
y = now.year
|
|
if m <= 0:
|
|
m += 12
|
|
y -= 1
|
|
period_data = physician_ratings_qs.filter(year=y, month=m).aggregate(
|
|
avg=Avg("average_rating"), surveys=Sum("total_surveys")
|
|
)
|
|
rating_trend_raw.append(
|
|
{
|
|
"period": f"{y}-{m:02d}",
|
|
"average_rating": round(float(period_data["avg"] or 0), 1),
|
|
"total_surveys": period_data["surveys"] or 0,
|
|
}
|
|
)
|
|
|
|
# 4. Top Physicians (current month or most recent with data)
|
|
current_month_ratings = physician_ratings_qs.filter(
|
|
year=now.year, month=now.month
|
|
).select_related("staff")
|
|
if not current_month_ratings.exists():
|
|
latest = physician_ratings_qs.order_by("-year", "-month").first()
|
|
if latest:
|
|
current_month_ratings = physician_ratings_qs.filter(
|
|
year=latest.year, month=latest.month
|
|
).select_related("staff")
|
|
|
|
top_physicians_raw = current_month_ratings.order_by("-average_rating", "-total_surveys")[:10]
|
|
top_physicians = [
|
|
{
|
|
"name": r.staff.get_localized_name(),
|
|
"rating": round(float(r.average_rating), 1),
|
|
"surveys": r.total_surveys,
|
|
}
|
|
for r in top_physicians_raw
|
|
]
|
|
|
|
# 5. Actions Overview
|
|
actions_qs = PXAction.objects.filter(department=department)
|
|
actions_status_raw = actions_qs.values("status").annotate(count=Count("pk"))
|
|
actions_status_labels = {
|
|
"open": "Open",
|
|
"in_progress": "In Progress",
|
|
"pending_approval": "Pending Approval",
|
|
"approved": "Approved",
|
|
"closed": "Closed",
|
|
"cancelled": "Cancelled",
|
|
}
|
|
actions_overview = [
|
|
{"status": actions_status_labels.get(a["status"], a["status"]), "count": a["count"]}
|
|
for a in actions_status_raw
|
|
if a["count"] > 0
|
|
]
|
|
|
|
# 6. Satisfaction Distribution
|
|
satisfaction_raw = complaints_clean.exclude(satisfaction__isnull=True).exclude(satisfaction="").values("satisfaction").annotate(count=Count("pk"))
|
|
satisfaction_labels = {
|
|
"satisfied": "Satisfied",
|
|
"neutral": "Neutral",
|
|
"dissatisfied": "Dissatisfied",
|
|
"no_response": "No Response",
|
|
"escalated": "Escalated",
|
|
}
|
|
satisfaction = [
|
|
{"label": satisfaction_labels.get(s["satisfaction"], s["satisfaction"]), "count": s["count"]}
|
|
for s in satisfaction_raw
|
|
if s["count"] > 0
|
|
]
|
|
|
|
# 7. Complaint Severity Distribution
|
|
severity_raw = complaints_clean.values("severity").annotate(count=Count("pk"))
|
|
severity_labels = {
|
|
"low": "Low",
|
|
"medium": "Medium",
|
|
"high": "High",
|
|
"critical": "Critical",
|
|
}
|
|
severity_order = ["low", "medium", "high", "critical"]
|
|
complaint_severity = []
|
|
for sev_key in severity_order:
|
|
count = next((s["count"] for s in severity_raw if s["severity"] == sev_key), 0)
|
|
if count > 0:
|
|
complaint_severity.append(
|
|
{"severity": severity_labels.get(sev_key, sev_key), "count": count}
|
|
)
|
|
|
|
# 8. Top Categories
|
|
categories_raw = (
|
|
complaints_clean.filter(category__isnull=False)
|
|
.values("category__name_en")
|
|
.annotate(count=Count("pk"))
|
|
.order_by("-count")[:5]
|
|
)
|
|
top_categories = [
|
|
{"category": c["category__name_en"] or "Unknown", "count": c["count"]}
|
|
for c in categories_raw
|
|
]
|
|
|
|
# KPIs
|
|
total_complaints = complaints_clean.count()
|
|
resolved_count = complaints_clean.filter(status__in=["resolved", "closed"]).count()
|
|
resolution_rate = round((resolved_count / total_complaints * 100) if total_complaints else 0, 1)
|
|
|
|
current_rating = physician_ratings_qs.filter(year=now.year, month=now.month).aggregate(
|
|
avg=Avg("average_rating")
|
|
)
|
|
avg_physician_rating = round(float(current_rating["avg"] or 0), 1)
|
|
|
|
open_actions = actions_qs.filter(status__in=["open", "in_progress"]).count()
|
|
|
|
total_satisfaction = sum(s["count"] for s in satisfaction)
|
|
satisfied_count = next((s["count"] for s in satisfaction if s["label"] == "Satisfied"), 0)
|
|
satisfaction_rate = round((satisfied_count / total_satisfaction * 100) if total_satisfaction else 0, 1)
|
|
|
|
reopened_count = complaints_clean.filter(reopened_from__isnull=False).count()
|
|
reassigned_count = ComplaintUpdate.objects.filter(
|
|
complaint__in=complaints_clean, update_type="assignment"
|
|
).distinct().count()
|
|
|
|
return JsonResponse(
|
|
{
|
|
"complaint_trend": complaint_trend,
|
|
"complaint_status": complaint_status,
|
|
"physician_rating_trend": rating_trend_raw,
|
|
"top_physicians": top_physicians,
|
|
"actions_overview": actions_overview,
|
|
"satisfaction": satisfaction,
|
|
"complaint_severity": complaint_severity,
|
|
"top_categories": top_categories,
|
|
"kpi": {
|
|
"avg_physician_rating": avg_physician_rating,
|
|
"resolution_rate": resolution_rate,
|
|
"open_actions": open_actions,
|
|
"satisfaction_rate": satisfaction_rate,
|
|
"total_complaints": total_complaints,
|
|
"reopened": reopened_count,
|
|
"reassigned": reassigned_count,
|
|
},
|
|
}
|
|
)
|
|
except Exception as e:
|
|
return JsonResponse({"error": str(e), "traceback": traceback.format_exc()}, status=500)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def set_department_respondent(request, pk):
|
|
"""Set the respondent for a department - Admin or Department Manager only"""
|
|
from .models import Department, Staff
|
|
from django.contrib.auth.models import Group
|
|
|
|
department = get_object_or_404(Department, pk=pk)
|
|
user = request.user
|
|
|
|
if not (
|
|
user.is_px_admin()
|
|
or (user.is_hospital_admin() and user.hospital == department.hospital)
|
|
or (user.is_department_manager() and user.department == department)
|
|
):
|
|
messages.error(request, _("You don't have permission to set the respondent."))
|
|
return redirect("organizations:department_detail", pk=pk)
|
|
|
|
respondent_id = request.POST.get("respondent_id")
|
|
if respondent_id:
|
|
respondent = Staff.objects.filter(pk=respondent_id, department=department, status="active").first()
|
|
if not respondent:
|
|
messages.error(request, _("Invalid staff member selected."))
|
|
return redirect("organizations:department_detail", pk=pk)
|
|
|
|
if not respondent.email:
|
|
messages.error(
|
|
request,
|
|
_(f"{respondent.get_full_name()} does not have an email address. Add an email first."),
|
|
)
|
|
return redirect("organizations:department_detail", pk=pk)
|
|
|
|
if not respondent.user:
|
|
messages.error(
|
|
request,
|
|
_(f"{respondent.get_full_name()} does not have a user account. Create a user account first."),
|
|
)
|
|
return redirect("organizations:department_detail", pk=pk)
|
|
|
|
department.respondent = respondent
|
|
department.save(update_fields=["respondent", "updated_at"])
|
|
|
|
group, _created = Group.objects.get_or_create(name="Champion")
|
|
respondent.user.groups.add(group)
|
|
messages.success(
|
|
request,
|
|
_(f"Champion set to {respondent.get_full_name()}. Champion role assigned."),
|
|
)
|
|
else:
|
|
department.respondent = None
|
|
department.save(update_fields=["respondent", "updated_at"])
|
|
messages.success(request, _("Champion removed."))
|
|
|
|
return redirect("organizations:department_detail", pk=pk)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def set_department_role(request, pk):
|
|
from .models import Department, Staff
|
|
|
|
department = get_object_or_404(Department, pk=pk)
|
|
user = request.user
|
|
|
|
if not (
|
|
user.is_px_admin()
|
|
or user.is_px_employee()
|
|
or (user.is_hospital_admin() and user.hospital == department.hospital)
|
|
):
|
|
messages.error(request, _("You don't have permission to set department roles."))
|
|
return redirect("organizations:department_detail", pk=pk)
|
|
|
|
role_field = request.POST.get("role_field", "")
|
|
valid_fields = {f[0] for f in Department.ROLE_FIELDS} | {"manager"}
|
|
if role_field not in valid_fields:
|
|
messages.error(request, _("Invalid role."))
|
|
return redirect("organizations:department_detail", pk=pk)
|
|
|
|
staff_id = request.POST.get("staff_id")
|
|
role_labels = dict(Department.ROLE_FIELDS)
|
|
role_labels["manager"] = "Manager"
|
|
role_label = role_labels.get(role_field, role_field)
|
|
|
|
if staff_id:
|
|
staff = Staff.objects.filter(
|
|
pk=staff_id, hospital=department.hospital, status="active"
|
|
).first()
|
|
if not staff:
|
|
messages.error(request, _("Invalid staff member selected."))
|
|
return redirect("organizations:department_detail", pk=pk)
|
|
|
|
if role_field == "manager":
|
|
if not staff.user:
|
|
from apps.organizations.services import StaffService
|
|
|
|
if not staff.email:
|
|
messages.warning(
|
|
request,
|
|
_(f"Selected staff {staff.get_full_name()} has no email. Add an email and create a user account first."),
|
|
)
|
|
return redirect(f"{reverse('organizations:department_detail', kwargs={'pk': pk})}?tab=roles")
|
|
try:
|
|
user_account, _created, _result = StaffService.create_user_for_staff(
|
|
staff, role="department_manager", request=request
|
|
)
|
|
department.manager = user_account
|
|
department.save(update_fields=["manager", "updated_at"])
|
|
messages.success(
|
|
request,
|
|
_(f"Manager set to {staff.get_full_name()}. User account created with Department Manager role."),
|
|
)
|
|
except ValueError as e:
|
|
messages.error(request, _(f"Could not create user account: {e}"))
|
|
return redirect(f"{reverse('organizations:department_detail', kwargs={'pk': pk})}?tab=roles")
|
|
else:
|
|
department.manager = staff.user
|
|
department.save(update_fields=["manager", "updated_at"])
|
|
messages.success(request, _(f"Manager set to {staff.get_full_name()}."))
|
|
else:
|
|
setattr(department, role_field, staff)
|
|
department.save(update_fields=[role_field, "updated_at"])
|
|
messages.success(request, _(f"{role_label} set to {staff.get_full_name()}."))
|
|
else:
|
|
if role_field == "manager":
|
|
department.manager = None
|
|
department.save(update_fields=["manager", "updated_at"])
|
|
else:
|
|
setattr(department, role_field, None)
|
|
department.save(update_fields=[role_field, "updated_at"])
|
|
messages.success(request, _(f"{role_label} removed."))
|
|
|
|
return redirect(f"{reverse('organizations:department_detail', kwargs={'pk': pk})}?tab=roles")
|
|
|
|
|
|
@login_required
|
|
def staff_import(request):
|
|
from django.db import transaction
|
|
import csv
|
|
import io
|
|
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, _("You don't have permission to import staff."))
|
|
return redirect("organizations:staff_list")
|
|
|
|
hospital = getattr(request, "tenant_hospital", None) or user.hospital
|
|
if not hospital:
|
|
messages.error(request, _("No hospital associated with your account."))
|
|
return redirect("organizations:staff_list")
|
|
|
|
context = {
|
|
"hospital": hospital,
|
|
"results": None,
|
|
}
|
|
|
|
if request.method == "POST":
|
|
csv_file = request.FILES.get("csv_file")
|
|
if not csv_file:
|
|
messages.error(request, _("Please select a CSV file to upload."))
|
|
return render(request, "organizations/staff_import.html", context)
|
|
|
|
if not csv_file.name.endswith(".csv"):
|
|
messages.error(request, _("Please upload a CSV file (.csv extension)."))
|
|
return render(request, "organizations/staff_import.html", context)
|
|
|
|
update_existing = request.POST.get("update_existing") == "on"
|
|
create_departments = request.POST.get("create_departments") == "on"
|
|
deactivate_missing = request.POST.get("deactivate_missing") == "on"
|
|
dry_run = request.POST.get("dry_run") == "on"
|
|
staff_type = request.POST.get("staff_type", "other")
|
|
|
|
try:
|
|
raw = csv_file.read().decode("utf-8-sig")
|
|
except UnicodeDecodeError:
|
|
try:
|
|
csv_file.seek(0)
|
|
raw = csv_file.read().decode("latin-1")
|
|
except Exception:
|
|
messages.error(request, _("Could not decode the file. Please save as UTF-8 CSV."))
|
|
return render(request, "organizations/staff_import.html", context)
|
|
|
|
reader = csv.DictReader(io.StringIO(raw))
|
|
headers = [h.strip() for h in (reader.fieldnames or [])]
|
|
|
|
required_cols = ["Staff ID", "Name"]
|
|
missing = [c for c in required_cols if c not in headers]
|
|
if missing:
|
|
messages.error(request, _(f"Missing required columns: {', '.join(missing)}"))
|
|
return render(request, "organizations/staff_import.html", context)
|
|
|
|
dept_cache = {}
|
|
section_cache = {}
|
|
subsection_cache = {}
|
|
|
|
created_rows = []
|
|
updated_rows = []
|
|
skipped_rows = []
|
|
error_rows = []
|
|
|
|
rows = list(reader)
|
|
|
|
if not dry_run:
|
|
try:
|
|
with transaction.atomic():
|
|
for row_num, row in enumerate(rows, start=2):
|
|
result = _process_staff_row(
|
|
row, row_num, hospital, staff_type,
|
|
update_existing, create_departments,
|
|
dept_cache, section_cache, subsection_cache,
|
|
)
|
|
if result[0] == "created":
|
|
created_rows.append(result[1])
|
|
elif result[0] == "updated":
|
|
updated_rows.append(result[1])
|
|
elif result[0] == "skipped":
|
|
skipped_rows.append(result[1])
|
|
elif result[0] == "error":
|
|
error_rows.append(result[1])
|
|
except Exception as e:
|
|
messages.error(request, _(f"Import failed: {str(e)}"))
|
|
return render(request, "organizations/staff_import.html", context)
|
|
else:
|
|
for row_num, row in enumerate(rows, start=2):
|
|
result = _process_staff_row_dry(
|
|
row, row_num, hospital, staff_type,
|
|
update_existing, create_departments,
|
|
dept_cache, section_cache, subsection_cache,
|
|
)
|
|
if result[0] == "created":
|
|
created_rows.append(result[1])
|
|
elif result[0] == "updated":
|
|
updated_rows.append(result[1])
|
|
elif result[0] == "skipped":
|
|
skipped_rows.append(result[1])
|
|
elif result[0] == "error":
|
|
error_rows.append(result[1])
|
|
|
|
deactivated_rows = []
|
|
if deactivate_missing:
|
|
csv_ids = set()
|
|
for row in rows:
|
|
sid = row.get("Staff ID", "").strip()
|
|
if sid:
|
|
csv_ids.add(sid)
|
|
|
|
missing_staff = Staff.objects.filter(
|
|
hospital=hospital, status="active"
|
|
).exclude(employee_id__in=csv_ids)
|
|
|
|
if dry_run:
|
|
for s in missing_staff:
|
|
deactivated_rows.append({
|
|
"row": "-", "id": s.employee_id, "name": s.get_full_name(),
|
|
"message": "Would deactivate",
|
|
})
|
|
else:
|
|
missing_staff.update(status="inactive")
|
|
for s in missing_staff:
|
|
deactivated_rows.append({
|
|
"row": "-", "id": s.employee_id, "name": s.get_full_name(),
|
|
"message": "Deactivated",
|
|
})
|
|
|
|
context["results"] = {
|
|
"created": created_rows,
|
|
"updated": updated_rows,
|
|
"skipped": skipped_rows,
|
|
"errors": error_rows,
|
|
"deactivated": deactivated_rows,
|
|
"created_count": len(created_rows),
|
|
"updated_count": len(updated_rows),
|
|
"skipped_count": len(skipped_rows),
|
|
"error_count": len(error_rows),
|
|
"deactivated_count": len(deactivated_rows),
|
|
"total_rows": len(rows),
|
|
"dry_run": dry_run,
|
|
}
|
|
|
|
if not dry_run:
|
|
total_ok = len(created_rows) + len(updated_rows)
|
|
msg = f"Import complete: {total_ok} processed ({len(created_rows)} created, {len(updated_rows)} updated), {len(skipped_rows)} skipped, {len(error_rows)} errors."
|
|
if deactivate_missing:
|
|
msg += f" {len(deactivated_rows)} staff deactivated."
|
|
messages.success(request, _(msg))
|
|
|
|
return render(request, "organizations/staff_import.html", context)
|
|
|
|
|
|
def _parse_staff_csv_row(row):
|
|
name = row.get("Name", "").strip()
|
|
parts = name.split(None, 1)
|
|
name_ar = row.get("Name_ar", "").strip()
|
|
parts_ar = name_ar.split(None, 1) if name_ar else ["", ""]
|
|
|
|
manager_id = None
|
|
if row.get("Manager", "").strip():
|
|
manager_parts = row["Manager"].split("-", 1)
|
|
manager_id = manager_parts[0].strip()
|
|
|
|
return {
|
|
"staff_id": row.get("Staff ID", "").strip(),
|
|
"name": name,
|
|
"name_ar": name_ar,
|
|
"first_name": parts[0] if parts else name,
|
|
"last_name": parts[1] if len(parts) > 1 else "",
|
|
"first_name_ar": parts_ar[0] if parts_ar else "",
|
|
"last_name_ar": parts_ar[1] if len(parts_ar) > 1 else "",
|
|
"civil_id": row.get("Civil Identity Number", "").strip(),
|
|
"location": row.get("Location", "").strip(),
|
|
"location_ar": row.get("Location_ar", "").strip(),
|
|
"department": row.get("Department", "").strip(),
|
|
"department_ar": row.get("Department_ar", "").strip(),
|
|
"section": row.get("Section", "").strip(),
|
|
"section_ar": row.get("Section_ar", "").strip(),
|
|
"subsection": row.get("Subsection", "").strip(),
|
|
"subsection_ar": row.get("Subsection_ar", "").strip(),
|
|
"job_title": row.get("AlHammadi Job Title", "").strip(),
|
|
"job_title_ar": row.get("AlHammadi Job Title_ar", "").strip(),
|
|
"country": row.get("Country", "").strip(),
|
|
"country_ar": row.get("Country_ar", "").strip(),
|
|
"gender": (row.get("Gender", "").strip().lower() if row.get("Gender") else ""),
|
|
"manager_id": manager_id,
|
|
}
|
|
|
|
|
|
def _import_get_or_create_dept(hospital, dept_name, dept_name_ar, cache):
|
|
if not dept_name:
|
|
return None
|
|
cache_key = (str(hospital.id), dept_name.lower())
|
|
if cache_key in cache:
|
|
return cache[cache_key]
|
|
dept, _ = Department.objects.get_or_create(
|
|
hospital=hospital,
|
|
name__iexact=dept_name,
|
|
parent__isnull=True,
|
|
defaults={
|
|
"name": dept_name,
|
|
"name_ar": dept_name_ar or "",
|
|
"code": str(uuid.uuid4())[:8],
|
|
"status": "active",
|
|
},
|
|
)
|
|
if dept.name_ar != dept_name_ar and dept_name_ar:
|
|
dept.name_ar = dept_name_ar
|
|
dept.save(update_fields=["name_ar"])
|
|
cache[cache_key] = dept
|
|
return dept
|
|
|
|
|
|
def _import_get_or_create_section(department, section_name, section_name_ar, cache):
|
|
if not section_name or not department:
|
|
return None
|
|
cache_key = (str(department.id), section_name.lower())
|
|
if cache_key in cache:
|
|
return cache[cache_key]
|
|
if section_name.lower() == department.name.lower():
|
|
cache[cache_key] = None
|
|
return None
|
|
section, _ = StaffSection.objects.get_or_create(
|
|
department=department,
|
|
name__iexact=section_name,
|
|
defaults={
|
|
"name": section_name,
|
|
"name_ar": section_name_ar or "",
|
|
"code": str(uuid.uuid4())[:8],
|
|
"status": "active",
|
|
},
|
|
)
|
|
if section.name_ar != section_name_ar and section_name_ar:
|
|
section.name_ar = section_name_ar
|
|
section.save(update_fields=["name_ar"])
|
|
cache[cache_key] = section
|
|
return section
|
|
|
|
|
|
def _import_get_or_create_subsection(section_obj, subsection_name, subsection_name_ar, cache):
|
|
if not subsection_name or not section_obj:
|
|
return None
|
|
cache_key = (str(section_obj.id), subsection_name.lower())
|
|
if cache_key in cache:
|
|
return cache[cache_key]
|
|
if subsection_name.lower() == section_obj.name.lower():
|
|
cache[cache_key] = None
|
|
return None
|
|
subsection, _ = StaffSubsection.objects.get_or_create(
|
|
section=section_obj,
|
|
name__iexact=subsection_name,
|
|
defaults={
|
|
"name": subsection_name,
|
|
"name_ar": subsection_name_ar or "",
|
|
"code": str(uuid.uuid4())[:8],
|
|
"status": "active",
|
|
},
|
|
)
|
|
if subsection.name_ar != subsection_name_ar and subsection_name_ar:
|
|
subsection.name_ar = subsection_name_ar
|
|
subsection.save(update_fields=["name_ar"])
|
|
cache[cache_key] = subsection
|
|
return subsection
|
|
|
|
|
|
def _resolve_staff_org(r, hospital, create_depts, dept_cache, section_cache, subsection_cache):
|
|
department = None
|
|
section_obj = None
|
|
subsection_obj = None
|
|
|
|
if r["department"]:
|
|
if create_depts:
|
|
department = _import_get_or_create_dept(hospital, r["department"], r["department_ar"], dept_cache)
|
|
else:
|
|
department = Department.objects.filter(
|
|
hospital=hospital, name__iexact=r["department"], parent__isnull=True
|
|
).first()
|
|
|
|
if r["section"] and department:
|
|
if create_depts:
|
|
section_obj = _import_get_or_create_section(department, r["section"], r["section_ar"], section_cache)
|
|
else:
|
|
section_obj = StaffSection.objects.filter(
|
|
department=department, name__iexact=r["section"]
|
|
).first()
|
|
|
|
if r["subsection"] and section_obj:
|
|
if create_depts:
|
|
subsection_obj = _import_get_or_create_subsection(section_obj, r["subsection"], r["subsection_ar"], subsection_cache)
|
|
else:
|
|
subsection_obj = StaffSubsection.objects.filter(
|
|
section=section_obj, name__iexact=r["subsection"]
|
|
).first()
|
|
|
|
return department, section_obj, subsection_obj
|
|
|
|
|
|
def _process_staff_row(row, row_num, hospital, staff_type, update_existing, create_depts,
|
|
dept_cache, section_cache, subsection_cache):
|
|
r = _parse_staff_csv_row(row)
|
|
if not r["staff_id"]:
|
|
return ("error", {"row": row_num, "id": "", "name": r["name"], "message": "Missing Staff ID"})
|
|
if not r["first_name"]:
|
|
return ("error", {"row": row_num, "id": r["staff_id"], "name": "", "message": "Missing Name"})
|
|
|
|
department, section_obj, subsection_obj = _resolve_staff_org(
|
|
r, hospital, create_depts, dept_cache, section_cache, subsection_cache
|
|
)
|
|
|
|
existing = Staff.objects.filter(employee_id=r["staff_id"], hospital=hospital).first()
|
|
|
|
if existing:
|
|
if not update_existing:
|
|
return ("skipped", {"row": row_num, "id": r["staff_id"], "name": r["name"], "message": "Already exists"})
|
|
existing.name = r["name"]
|
|
existing.name_ar = r["name_ar"]
|
|
existing.first_name = r["first_name"]
|
|
existing.last_name = r["last_name"]
|
|
existing.first_name_ar = r["first_name_ar"]
|
|
existing.last_name_ar = r["last_name_ar"]
|
|
existing.civil_id = r["civil_id"]
|
|
existing.job_title = r["job_title"]
|
|
existing.job_title_ar = r["job_title_ar"]
|
|
existing.specialization = r["job_title"]
|
|
existing.hospital = hospital
|
|
existing.department = department
|
|
existing.section_fk = section_obj
|
|
existing.subsection_fk = subsection_obj
|
|
existing.department_name = r["department"]
|
|
existing.department_name_ar = r["department_ar"]
|
|
existing.section = r["section"]
|
|
existing.section_ar = r["section_ar"]
|
|
existing.subsection = r["subsection"]
|
|
existing.subsection_ar = r["subsection_ar"]
|
|
existing.location = r["location"]
|
|
existing.location_ar = r["location_ar"]
|
|
existing.country = r["country"]
|
|
existing.country_ar = r["country_ar"]
|
|
if r["gender"]:
|
|
existing.gender = r["gender"]
|
|
existing.save()
|
|
return ("updated", {"row": row_num, "id": r["staff_id"], "name": r["name"], "message": "Updated"})
|
|
|
|
Staff.objects.create(
|
|
employee_id=r["staff_id"],
|
|
name=r["name"],
|
|
name_ar=r["name_ar"],
|
|
first_name=r["first_name"],
|
|
last_name=r["last_name"],
|
|
first_name_ar=r["first_name_ar"],
|
|
last_name_ar=r["last_name_ar"],
|
|
civil_id=r["civil_id"],
|
|
staff_type=staff_type,
|
|
job_title=r["job_title"],
|
|
job_title_ar=r["job_title_ar"],
|
|
specialization=r["job_title"],
|
|
hospital=hospital,
|
|
department=department,
|
|
section_fk=section_obj,
|
|
subsection_fk=subsection_obj,
|
|
department_name=r["department"],
|
|
department_name_ar=r["department_ar"],
|
|
section=r["section"],
|
|
section_ar=r["section_ar"],
|
|
subsection=r["subsection"],
|
|
subsection_ar=r["subsection_ar"],
|
|
location=r["location"],
|
|
location_ar=r["location_ar"],
|
|
country=r["country"],
|
|
country_ar=r["country_ar"],
|
|
gender=r["gender"],
|
|
status="active",
|
|
)
|
|
return ("created", {"row": row_num, "id": r["staff_id"], "name": r["name"], "message": "Created"})
|
|
|
|
|
|
def _process_staff_row_dry(row, row_num, hospital, staff_type, update_existing, create_depts,
|
|
dept_cache, section_cache, subsection_cache):
|
|
r = _parse_staff_csv_row(row)
|
|
if not r["staff_id"]:
|
|
return ("error", {"row": row_num, "id": "", "name": r["name"], "message": "Missing Staff ID"})
|
|
if not r["first_name"]:
|
|
return ("error", {"row": row_num, "id": r["staff_id"], "name": "", "message": "Missing Name"})
|
|
|
|
existing = Staff.objects.filter(employee_id=r["staff_id"], hospital=hospital).first()
|
|
|
|
if existing:
|
|
if not update_existing:
|
|
return ("skipped", {"row": row_num, "id": r["staff_id"], "name": r["name"], "message": "Already exists (would skip)"})
|
|
return ("updated", {"row": row_num, "id": r["staff_id"], "name": r["name"], "message": "Would update"})
|
|
|
|
return ("created", {"row": row_num, "id": r["staff_id"], "name": r["name"], "message": "Would create"})
|
|
|
|
|
|
@login_required
|
|
def staff_import_sample_csv(request):
|
|
from django.http import HttpResponse
|
|
import csv
|
|
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, _("You don't have permission."))
|
|
return redirect("organizations:staff_list")
|
|
|
|
headers = [
|
|
"Staff ID", "Name", "Name_ar", "Manager", "Manager_ar",
|
|
"Civil Identity Number", "Location", "Location_ar",
|
|
"Department", "Department_ar", "Section", "Section_ar",
|
|
"Subsection", "Subsection_ar", "AlHammadi Job Title",
|
|
"AlHammadi Job Title_ar", "Country", "Country_ar",
|
|
]
|
|
example = [
|
|
"EMP001", "Ahmed Al-Rashid", "أحمد الراشد", "EMP100 - Mohammad Al-Salem", "محمد السالم",
|
|
"12345678", "Building A, Floor 2", "المبنى أ، الطابق ٢",
|
|
"Cardiology", "أمراض القلب", "Cardiac Care Unit", "وحدة العناية القلبية",
|
|
"Catheterization Lab", "مختبر القسطرة", "Senior Cardiologist",
|
|
"استشاري أمراض قلب كبار", "Saudi Arabia", "المملكة العربية السعودية",
|
|
]
|
|
|
|
response = HttpResponse(content_type="text/csv")
|
|
response["Content-Disposition"] = 'attachment; filename="staff_import_template.csv"'
|
|
writer = csv.writer(response)
|
|
writer.writerow(headers)
|
|
writer.writerow(example)
|
|
return response
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def department_manager_review(request, pk, idept_pk):
|
|
"""
|
|
Department Manager reviews a champion's response.
|
|
Shows the champion response + configurable questions form.
|
|
On approval, notifies PX Admin. On rejection, notifies champion.
|
|
"""
|
|
from apps.complaints.models import (
|
|
ComplaintInvolvedDepartment,
|
|
ManagerReviewQuestion,
|
|
DepartmentManagerReview,
|
|
ManagerReviewAnswer,
|
|
ComplaintUpdate,
|
|
)
|
|
from apps.core.services import AuditService
|
|
|
|
department = get_object_or_404(Department.objects.select_related("hospital"), pk=pk)
|
|
involved_dept = get_object_or_404(
|
|
ComplaintInvolvedDepartment.objects.select_related(
|
|
"complaint", "complaint__assigned_to", "department",
|
|
),
|
|
pk=idept_pk,
|
|
department=department,
|
|
)
|
|
user = request.user
|
|
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
if not (
|
|
user.is_department_manager()
|
|
and (user.department == department or department.manager == user)
|
|
):
|
|
messages.error(request, _("Only the department manager can review champion responses."))
|
|
return redirect("organizations:department_detail", pk=department.pk)
|
|
|
|
if not involved_dept.response_submitted:
|
|
messages.error(request, _("No champion response submitted yet."))
|
|
return redirect("organizations:department_detail", pk=department.pk)
|
|
|
|
if involved_dept.manager_review_status == "approved":
|
|
messages.info(request, _("This response has already been approved."))
|
|
return redirect("organizations:department_detail", pk=department.pk)
|
|
|
|
complaint = involved_dept.complaint
|
|
|
|
# Ensure default manager review questions exist for this hospital
|
|
defaults = [
|
|
("Is there negligence resulting from carelessness?", "هل يوجد تقصير ناتج عن إهمال؟", "yes_no", 1),
|
|
("Is it resulting from policies or regulations?", "هل هو ناتج عن سياسات أو تنظيمات؟", "yes_no", 2),
|
|
("Radical action taken for this issue", "الإجراء المتخذ لهذا الموضوع جذرياً", "textarea", 3),
|
|
]
|
|
for text_en, text_ar, qtype, order in defaults:
|
|
ManagerReviewQuestion.objects.get_or_create(
|
|
hospital=department.hospital, text_en=text_en,
|
|
defaults={"text_ar": text_ar, "question_type": qtype, "order": order, "is_active": True},
|
|
)
|
|
|
|
questions = ManagerReviewQuestion.objects.filter(
|
|
hospital=department.hospital, is_active=True,
|
|
).order_by("order", "created_at")
|
|
|
|
if request.method == "POST":
|
|
action = request.POST.get("review_action")
|
|
if action not in ("approve", "reject"):
|
|
messages.error(request, _("Invalid action."))
|
|
return redirect("organizations:department_manager_review", pk=department.pk, idept_pk=involved_dept.pk)
|
|
|
|
rejection_reason = request.POST.get("rejection_reason", "").strip()
|
|
|
|
if action == "reject" and not rejection_reason:
|
|
messages.error(request, _("Please provide a rejection reason."))
|
|
return redirect("organizations:department_manager_review", pk=department.pk, idept_pk=involved_dept.pk)
|
|
|
|
review_obj = DepartmentManagerReview.objects.create(
|
|
involved_department=involved_dept,
|
|
reviewed_by=user,
|
|
status="approved" if action == "approve" else "rejected",
|
|
rejection_reason=rejection_reason,
|
|
)
|
|
|
|
for q in questions:
|
|
answer_key = f"question_{q.pk}"
|
|
answer = ManagerReviewAnswer(
|
|
review=review_obj,
|
|
question=q,
|
|
)
|
|
if q.question_type in ("text", "textarea"):
|
|
answer.text_value = request.POST.get(answer_key, "").strip()
|
|
elif q.question_type == "yes_no":
|
|
val = request.POST.get(answer_key, "")
|
|
answer.text_value = val
|
|
elif q.question_type == "rating":
|
|
try:
|
|
answer.numeric_value = int(request.POST.get(answer_key, 0))
|
|
except (ValueError, TypeError):
|
|
answer.numeric_value = None
|
|
elif q.question_type == "multiple_choice":
|
|
answer.text_value = request.POST.get(answer_key, "").strip()
|
|
answer.save()
|
|
|
|
if action == "approve":
|
|
involved_dept.manager_review_status = "approved"
|
|
involved_dept.manager_reviewed_by = user
|
|
involved_dept.manager_reviewed_at = timezone.now()
|
|
involved_dept.save()
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="note",
|
|
message=f"Department manager {user.get_full_name()} approved the response from {department.name}.",
|
|
created_by=user,
|
|
)
|
|
|
|
AuditService.log_event(
|
|
event_type="dept_manager_approved_response",
|
|
description=f"Manager {user.get_full_name()} approved response for complaint {complaint.reference_number}",
|
|
user=user,
|
|
content_object=complaint,
|
|
)
|
|
|
|
if complaint.assigned_to and complaint.assigned_to.email:
|
|
try:
|
|
from apps.notifications.services import NotificationService, get_email_header_html
|
|
complaint_url = request.build_absolute_uri(
|
|
reverse("complaints:complaint_detail", kwargs={"pk": complaint.pk})
|
|
)
|
|
NotificationService.send_email(
|
|
complaint.assigned_to.email,
|
|
subject=f"Manager-Approved Response Ready - {complaint.reference_number}",
|
|
message=(
|
|
f"The department manager has approved the response from {department.name} "
|
|
f"for complaint {complaint.reference_number}. Please review.\n\n{complaint_url}"
|
|
),
|
|
html_message=f"""
|
|
<div style="font-family: 'Segoe UI', Tahoma, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;">
|
|
{get_email_header_html()}
|
|
<div style="padding: 20px;">
|
|
<h2 style="color: #005696; font-size: 18px; margin: 0 0 12px 0;">Manager-Approved Response Ready</h2>
|
|
<p>The department manager of <strong>{department.name}</strong> has approved the champion response
|
|
for complaint <strong>{complaint.reference_number}</strong>.</p>
|
|
<p>It is now ready for your final review.</p>
|
|
<p style="margin-top: 16px;">
|
|
<a href="{complaint_url}" style="background: #005696; color: white; padding: 10px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">View Complaint</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
""",
|
|
related_object=complaint,
|
|
)
|
|
except Exception as e:
|
|
import logging
|
|
logging.getLogger(__name__).error(f"Failed to send px-admin notification: {e}")
|
|
|
|
messages.success(request, _("Response approved and forwarded to the PX team."))
|
|
|
|
elif action == "reject":
|
|
involved_dept.manager_review_status = "rejected"
|
|
involved_dept.manager_reviewed_by = user
|
|
involved_dept.manager_reviewed_at = timezone.now()
|
|
involved_dept.response_submitted = False
|
|
involved_dept.response_submitted_at = None
|
|
involved_dept.response_notes = ""
|
|
involved_dept.response_notes_en = ""
|
|
involved_dept.response_notes_ar = ""
|
|
involved_dept.save()
|
|
|
|
ComplaintUpdate.objects.create(
|
|
complaint=complaint,
|
|
update_type="note",
|
|
message=f"Department manager {user.get_full_name()} rejected the response from {department.name}. Reason: {rejection_reason}",
|
|
created_by=user,
|
|
)
|
|
|
|
AuditService.log_event(
|
|
event_type="dept_manager_rejected_response",
|
|
description=f"Manager {user.get_full_name()} rejected response for complaint {complaint.reference_number}",
|
|
user=user,
|
|
content_object=complaint,
|
|
)
|
|
|
|
recipients = set()
|
|
if department.champion and department.champion.user and department.champion.user.email:
|
|
recipients.add(department.champion.user.email)
|
|
if involved_dept.assigned_to and involved_dept.assigned_to.email:
|
|
recipients.add(involved_dept.assigned_to.email)
|
|
for email in recipients:
|
|
try:
|
|
from apps.notifications.services import NotificationService, get_email_header_html
|
|
dept_url = request.build_absolute_uri(
|
|
reverse("organizations:department_detail", kwargs={"pk": department.pk})
|
|
)
|
|
NotificationService.send_email(
|
|
email,
|
|
subject=f"Response Rejected by Manager - {complaint.reference_number}",
|
|
message=(
|
|
f"The department manager rejected your response for complaint "
|
|
f"{complaint.reference_number}.\n\nReason: {rejection_reason}\n\n"
|
|
f"Please submit a new response.\n\n{dept_url}"
|
|
),
|
|
html_message=f"""
|
|
<div style="font-family: 'Segoe UI', Tahoma, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden;">
|
|
{get_email_header_html()}
|
|
<div style="padding: 20px;">
|
|
<h2 style="color: #005696; font-size: 18px; margin: 0 0 12px 0;">Response Rejected by Manager</h2>
|
|
<p>The department manager has rejected your response for complaint
|
|
<strong>{complaint.reference_number}</strong>.</p>
|
|
<p><strong>Reason:</strong> {rejection_reason}</p>
|
|
<p style="margin-top: 16px;">Please review and submit a new response.</p>
|
|
<p style="margin-top: 16px;">
|
|
<a href="{dept_url}" style="background: #005696; color: white; padding: 10px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">Go to Department</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
""",
|
|
related_object=complaint,
|
|
)
|
|
except Exception as e:
|
|
import logging
|
|
logging.getLogger(__name__).error(f"Failed to send rejection notification: {e}")
|
|
|
|
messages.success(request, _("Response rejected. The champion has been notified to submit a new response."))
|
|
|
|
return redirect("organizations:department_detail", pk=department.pk)
|
|
|
|
# Fetch investigation Q&A if one exists for this involved department
|
|
investigation = involved_dept.investigations.prefetch_related(
|
|
"questions", "responses__staff", "responses__answers__question"
|
|
).first()
|
|
investigation_staff_data = []
|
|
if investigation:
|
|
inv_questions = list(investigation.questions.all().order_by("order"))
|
|
for resp in investigation.responses.all():
|
|
qa_pairs = []
|
|
for q in inv_questions:
|
|
answer_text = ""
|
|
for ans in resp.answers.all():
|
|
if ans.question_id == q.id:
|
|
answer_text = ans.answer_text
|
|
break
|
|
qa_pairs.append({"question": q, "answer": answer_text})
|
|
investigation_staff_data.append({
|
|
"staff": resp.staff,
|
|
"is_completed": resp.is_completed,
|
|
"completed_at": resp.completed_at,
|
|
"qa_pairs": qa_pairs,
|
|
})
|
|
|
|
context = {
|
|
"department": department,
|
|
"involved_dept": involved_dept,
|
|
"complaint": complaint,
|
|
"questions": questions,
|
|
"investigation": investigation,
|
|
"investigation_staff_data": investigation_staff_data,
|
|
}
|
|
return render(request, "organizations/department_manager_review.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def manager_review_questions(request):
|
|
"""PX Admin manages the questions shown to department managers during review."""
|
|
from apps.complaints.models import ManagerReviewQuestion
|
|
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, _("You don't have permission to manage review questions."))
|
|
return redirect("dashboard:dashboard")
|
|
|
|
hospital = request.tenant_hospital
|
|
if not hospital:
|
|
messages.error(request, _("No hospital context found."))
|
|
return redirect("dashboard:dashboard")
|
|
|
|
questions = ManagerReviewQuestion.objects.filter(hospital=hospital).order_by("order", "created_at")
|
|
|
|
context = {
|
|
"questions": questions,
|
|
"hospital": hospital,
|
|
}
|
|
return render(request, "organizations/manager_review_questions.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def manager_review_question_create(request):
|
|
from apps.complaints.models import ManagerReviewQuestion, ManagerReviewQuestionType
|
|
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, _("You don't have permission to manage review questions."))
|
|
return redirect("dashboard:dashboard")
|
|
|
|
hospital = request.tenant_hospital
|
|
if not hospital:
|
|
messages.error(request, _("No hospital context found."))
|
|
return redirect("dashboard:dashboard")
|
|
|
|
if request.method == "POST":
|
|
text_en = request.POST.get("text_en", "").strip()
|
|
text_ar = request.POST.get("text_ar", "").strip()
|
|
question_type = request.POST.get("question_type", "textarea")
|
|
order = int(request.POST.get("order", 0))
|
|
is_active = request.POST.get("is_active") == "on"
|
|
|
|
if not text_en:
|
|
messages.error(request, _("English text is required."))
|
|
return redirect("organizations:manager_review_question_create")
|
|
|
|
choices_raw = request.POST.get("choices_json", "").strip()
|
|
choices_json = []
|
|
if question_type == "multiple_choice" and choices_raw:
|
|
import json
|
|
try:
|
|
choices_json = json.loads(choices_raw)
|
|
except json.JSONDecodeError:
|
|
choices_json = [c.strip() for c in choices_raw.split(",") if c.strip()]
|
|
|
|
ManagerReviewQuestion.objects.create(
|
|
hospital=hospital,
|
|
text_en=text_en,
|
|
text_ar=text_ar,
|
|
question_type=question_type,
|
|
choices_json=choices_json,
|
|
order=order,
|
|
is_active=is_active,
|
|
created_by=user,
|
|
)
|
|
messages.success(request, _("Question created successfully."))
|
|
return redirect("organizations:manager_review_questions")
|
|
|
|
question_types = ManagerReviewQuestionType.choices
|
|
context = {"hospital": hospital, "question_types": question_types}
|
|
return render(request, "organizations/manager_review_question_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
@require_http_methods(["GET", "POST"])
|
|
def manager_review_question_update(request, pk):
|
|
from apps.complaints.models import ManagerReviewQuestion, ManagerReviewQuestionType
|
|
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, _("You don't have permission to manage review questions."))
|
|
return redirect("dashboard:dashboard")
|
|
|
|
hospital = request.tenant_hospital
|
|
question = get_object_or_404(ManagerReviewQuestion, pk=pk, hospital=hospital)
|
|
|
|
if request.method == "POST":
|
|
question.text_en = request.POST.get("text_en", "").strip()
|
|
question.text_ar = request.POST.get("text_ar", "").strip()
|
|
question.question_type = request.POST.get("question_type", "textarea")
|
|
question.order = int(request.POST.get("order", 0))
|
|
question.is_active = request.POST.get("is_active") == "on"
|
|
|
|
choices_raw = request.POST.get("choices_json", "").strip()
|
|
if question.question_type == "multiple_choice" and choices_raw:
|
|
import json
|
|
try:
|
|
question.choices_json = json.loads(choices_raw)
|
|
except json.JSONDecodeError:
|
|
question.choices_json = [c.strip() for c in choices_raw.split(",") if c.strip()]
|
|
else:
|
|
question.choices_json = []
|
|
|
|
question.save()
|
|
messages.success(request, _("Question updated successfully."))
|
|
return redirect("organizations:manager_review_questions")
|
|
|
|
question_types = ManagerReviewQuestionType.choices
|
|
context = {"question": question, "hospital": hospital, "question_types": question_types}
|
|
return render(request, "organizations/manager_review_question_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
@require_POST
|
|
def manager_review_question_delete(request, pk):
|
|
from apps.complaints.models import ManagerReviewQuestion
|
|
|
|
user = request.user
|
|
if not (user.is_px_admin() or user.is_hospital_admin()):
|
|
messages.error(request, _("You don't have permission to manage review questions."))
|
|
return redirect("dashboard:dashboard")
|
|
|
|
hospital = request.tenant_hospital
|
|
question = get_object_or_404(ManagerReviewQuestion, pk=pk, hospital=hospital)
|
|
question.delete()
|
|
messages.success(request, _("Question deleted."))
|
|
return redirect("organizations:manager_review_questions")
|
|
|
|
|
|
# ==================== OrgSubSection CRUD ====================
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def orgsection_list(request):
|
|
queryset = Section.objects.select_related("department", "department__hospital", "champion")
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and user.hospital:
|
|
queryset = queryset.filter(department__hospital=user.hospital)
|
|
|
|
department_filter = request.GET.get("department")
|
|
if department_filter:
|
|
queryset = queryset.filter(department_id=department_filter)
|
|
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(name_en__icontains=search_query) | Q(name_ar__icontains=search_query) | Q(code__icontains=search_query)
|
|
)
|
|
|
|
queryset = queryset.order_by("department__name", "name_en")
|
|
|
|
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)
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"sections": page_obj.object_list,
|
|
"departments": departments,
|
|
"filters": request.GET,
|
|
}
|
|
return render(request, "organizations/orgsection_list.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def orgsection_create(request):
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You don't have permission to create sections")
|
|
|
|
if request.method == "POST":
|
|
name_en = request.POST.get("name_en")
|
|
name_ar = request.POST.get("name_ar", "")
|
|
code = request.POST.get("code", "")
|
|
department_id = request.POST.get("department")
|
|
status = request.POST.get("status", "active")
|
|
location_type = request.POST.get("location_type", "")
|
|
sub_location = request.POST.get("sub_location", "")
|
|
floor = request.POST.get("floor", "")
|
|
champion_id = request.POST.get("champion")
|
|
|
|
if name_en and department_id:
|
|
department = get_object_or_404(Department, pk=department_id)
|
|
if not user.is_px_admin() and department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You can only create sections in your hospital")
|
|
|
|
Section.objects.create(
|
|
name_en=name_en,
|
|
name_ar=name_ar,
|
|
code=code,
|
|
department=department,
|
|
status=status,
|
|
location_type=location_type,
|
|
sub_location=sub_location,
|
|
floor=floor,
|
|
champion_id=champion_id if champion_id else None,
|
|
)
|
|
messages.success(request, "Section created successfully.")
|
|
return redirect("organizations:orgsection_list")
|
|
|
|
departments = Department.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
|
|
staff = Staff.objects.filter(status="active").order_by("first_name", "last_name")
|
|
|
|
context = {"departments": departments, "staff": staff}
|
|
return render(request, "organizations/orgsection_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def orgsection_detail(request, pk):
|
|
section = get_object_or_404(
|
|
Section.objects.select_related("department", "department__hospital", "champion"),
|
|
pk=pk,
|
|
)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and user.hospital:
|
|
if section.department.hospital != user.hospital:
|
|
messages.error(request, _("You don't have permission to view this section."))
|
|
return redirect("organizations:orgsection_list")
|
|
|
|
sub_sections = SubSection.objects.filter(section=section).order_by("name_en")
|
|
|
|
context = {
|
|
"section": section,
|
|
"sub_sections": sub_sections,
|
|
"sub_sections_count": sub_sections.count(),
|
|
"can_edit": user.is_px_admin() or (user.is_hospital_admin() and user.hospital == section.department.hospital),
|
|
}
|
|
return render(request, "organizations/orgsection_detail.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def orgsection_update(request, pk):
|
|
section = get_object_or_404(Section, pk=pk)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You don't have permission to update sections")
|
|
|
|
if not user.is_px_admin() and section.department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You can only update sections in your hospital")
|
|
|
|
if request.method == "POST":
|
|
section.name_en = request.POST.get("name_en", section.name_en)
|
|
section.name_ar = request.POST.get("name_ar", "")
|
|
section.code = request.POST.get("code", "")
|
|
section.status = request.POST.get("status", section.status)
|
|
section.location_type = request.POST.get("location_type", "")
|
|
section.sub_location = request.POST.get("sub_location", "")
|
|
section.floor = request.POST.get("floor", "")
|
|
champion_id = request.POST.get("champion")
|
|
section.champion_id = champion_id if champion_id else None
|
|
section.save()
|
|
|
|
messages.success(request, "Section updated successfully.")
|
|
return redirect("organizations:orgsection_detail", pk=section.pk)
|
|
|
|
departments = Department.objects.filter(status="active")
|
|
if not user.is_px_admin() and user.hospital:
|
|
departments = departments.filter(hospital=user.hospital)
|
|
|
|
staff = Staff.objects.filter(status="active").order_by("first_name", "last_name")
|
|
|
|
context = {"section": section, "departments": departments, "staff": staff}
|
|
return render(request, "organizations/orgsection_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def orgsection_delete(request, pk):
|
|
section = get_object_or_404(Section, pk=pk)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You don't have permission to delete sections")
|
|
|
|
if not user.is_px_admin() and section.department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You can only delete sections in your hospital")
|
|
|
|
if request.method == "POST":
|
|
sub_count = section.sub_subsections.count()
|
|
if sub_count > 0:
|
|
messages.error(request, f"Cannot delete section. {sub_count} sub-sections are linked to it.")
|
|
return redirect("organizations:orgsection_list")
|
|
|
|
section.delete()
|
|
messages.success(request, "Section deleted successfully.")
|
|
return redirect("organizations:orgsection_list")
|
|
|
|
context = {"section": section}
|
|
return render(request, "organizations/orgsection_confirm_delete.html", context)
|
|
|
|
|
|
# ==================== SubSection CRUD ====================
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def orgsubsection_list(request):
|
|
queryset = SubSection.objects.select_related("section", "section__department", "section__department__hospital")
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and user.hospital:
|
|
queryset = queryset.filter(section__department__hospital=user.hospital)
|
|
|
|
section_filter = request.GET.get("section")
|
|
if section_filter:
|
|
queryset = queryset.filter(section_id=section_filter)
|
|
|
|
status_filter = request.GET.get("status")
|
|
if status_filter:
|
|
queryset = queryset.filter(status=status_filter)
|
|
|
|
search_query = request.GET.get("search")
|
|
if search_query:
|
|
queryset = queryset.filter(
|
|
Q(name_en__icontains=search_query) | Q(name_ar__icontains=search_query) | Q(code__icontains=search_query)
|
|
)
|
|
|
|
queryset = queryset.order_by("section__name_en", "name_en")
|
|
|
|
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)
|
|
|
|
org_sections = Section.objects.filter(status="active").select_related("department")
|
|
if not user.is_px_admin() and user.hospital:
|
|
org_sections = org_sections.filter(department__hospital=user.hospital)
|
|
|
|
context = {
|
|
"page_obj": page_obj,
|
|
"sub_sections": page_obj.object_list,
|
|
"org_sections": org_sections,
|
|
"filters": request.GET,
|
|
}
|
|
return render(request, "organizations/orgsubsection_list.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def orgsubsection_create(request):
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You don't have permission to create sub-sections")
|
|
|
|
preselected_section_id = request.GET.get("section")
|
|
preselected_section = None
|
|
if preselected_section_id:
|
|
try:
|
|
preselected_section = Section.objects.get(pk=preselected_section_id)
|
|
except (Section.DoesNotExist, ValueError):
|
|
preselected_section = None
|
|
|
|
if request.method == "POST":
|
|
name_en = request.POST.get("name_en")
|
|
name_ar = request.POST.get("name_ar", "")
|
|
code = request.POST.get("code", "")
|
|
section_id = request.POST.get("section")
|
|
status = request.POST.get("status", "active")
|
|
champion_id = request.POST.get("champion")
|
|
|
|
if name_en and section_id:
|
|
parent_section = get_object_or_404(Section, pk=section_id)
|
|
if not user.is_px_admin() and parent_section.department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You can only create sub-sections in your hospital")
|
|
|
|
SubSection.objects.create(
|
|
name_en=name_en,
|
|
name_ar=name_ar,
|
|
code=code,
|
|
section=parent_section,
|
|
status=status,
|
|
champion_id=champion_id if champion_id else None,
|
|
)
|
|
messages.success(request, "Sub-section created successfully.")
|
|
return redirect("organizations:orgsection_detail", pk=section_id)
|
|
|
|
org_sections = Section.objects.filter(status="active").select_related("department")
|
|
if not user.is_px_admin() and user.hospital:
|
|
org_sections = org_sections.filter(department__hospital=user.hospital)
|
|
|
|
staff = Staff.objects.filter(status="active").order_by("first_name", "last_name")
|
|
|
|
context = {
|
|
"org_sections": org_sections,
|
|
"preselected_section": preselected_section,
|
|
"staff": staff,
|
|
}
|
|
return render(request, "organizations/orgsubsection_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def orgsubsection_update(request, pk):
|
|
sub_section = get_object_or_404(SubSection, pk=pk)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You don't have permission to update sub-sections")
|
|
|
|
if not user.is_px_admin() and sub_section.section.department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You can only update sub-sections in your hospital")
|
|
|
|
if request.method == "POST":
|
|
sub_section.name_en = request.POST.get("name_en", sub_section.name_en)
|
|
sub_section.name_ar = request.POST.get("name_ar", "")
|
|
sub_section.code = request.POST.get("code", "")
|
|
sub_section.status = request.POST.get("status", sub_section.status)
|
|
champion_id = request.POST.get("champion")
|
|
sub_section.champion_id = champion_id if champion_id else None
|
|
sub_section.save()
|
|
|
|
messages.success(request, "Sub-section updated successfully.")
|
|
return redirect("organizations:orgsection_detail", pk=sub_section.section_id)
|
|
|
|
org_sections = Section.objects.filter(status="active").select_related("department")
|
|
if not user.is_px_admin() and user.hospital:
|
|
org_sections = org_sections.filter(department__hospital=user.hospital)
|
|
|
|
staff = Staff.objects.filter(status="active").order_by("first_name", "last_name")
|
|
|
|
context = {
|
|
"sub_section": sub_section,
|
|
"org_sections": org_sections,
|
|
"staff": staff,
|
|
}
|
|
return render(request, "organizations/orgsubsection_form.html", context)
|
|
|
|
|
|
@block_source_user
|
|
@login_required
|
|
def orgsubsection_delete(request, pk):
|
|
sub_section = get_object_or_404(SubSection, pk=pk)
|
|
|
|
user = request.user
|
|
if not user.is_px_admin() and not user.is_hospital_admin():
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You don't have permission to delete sub-sections")
|
|
|
|
if not user.is_px_admin() and sub_section.section.department.hospital != user.hospital:
|
|
from django.http import HttpResponseForbidden
|
|
return HttpResponseForbidden("You can only delete sub-sections in your hospital")
|
|
|
|
if request.method == "POST":
|
|
parent_pk = sub_section.section_id
|
|
sub_section.delete()
|
|
messages.success(request, "Sub-section deleted successfully.")
|
|
return redirect("organizations:orgsection_detail", pk=parent_pk)
|
|
|
|
context = {"sub_section": sub_section}
|
|
return render(request, "organizations/orgsubsection_confirm_delete.html", context)
|