Per the workflow realignment, the point of appreciation is recognition — managers should see the appreciations their department received alongside other feedback. Adds an 'Appreciations Received' card (amber theme) after the Pending Actions section, showing the count and the 10 most recent SENT appreciations. The card only renders when the department has at least one appreciation.
4952 lines
192 KiB
Python
4952 lines
192 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")
|
|
),
|
|
# Appreciations received by this department (SENT = terminal/recognized).
|
|
# Surfaced so managers see recognition alongside other feedback.
|
|
"dept_appreciations": _dept_appreciations_qs(department),
|
|
"dept_appreciations_count": _dept_appreciations_count(department),
|
|
}
|
|
return render(request, "organizations/department_detail.html", context)
|
|
|
|
|
|
def _dept_appreciations_qs(department):
|
|
"""Recent SENT appreciations for the department detail dashboard."""
|
|
from apps.appreciation.models import Appreciation, AppreciationStatus
|
|
return (
|
|
Appreciation.objects.filter(
|
|
department=department,
|
|
status=AppreciationStatus.SENT,
|
|
).select_related("sender", "category").order_by("-sent_at")[:10]
|
|
)
|
|
|
|
|
|
def _dept_appreciations_count(department):
|
|
"""Total SENT appreciations for the department detail dashboard."""
|
|
from apps.appreciation.models import Appreciation, AppreciationStatus
|
|
return Appreciation.objects.filter(
|
|
department=department,
|
|
status=AppreciationStatus.SENT,
|
|
).count()
|
|
|
|
|
|
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
|
|
|
|
# A department member may view a complaint when their department is the
|
|
# complaint's primary department (and it has been dispatched) OR when their
|
|
# department is one of the routed involved departments (and that routing
|
|
# was sent). This mirrors the OR-filter used to surface complaints on the
|
|
# department detail page, so the modal authorizes exactly the records the
|
|
# user can already see in the list.
|
|
allowed_dept_ids = set()
|
|
if complaint.department_id and complaint.sent_to_department:
|
|
allowed_dept_ids.add(complaint.department_id)
|
|
allowed_dept_ids.update(
|
|
complaint.involved_departments.filter(sent=True).values_list("department_id", flat=True)
|
|
)
|
|
|
|
dept_member_ok = user.department_id in allowed_dept_ids
|
|
director_ok = user.is_director() and user.get_directed_departments().filter(
|
|
id__in=allowed_dept_ids
|
|
).exists()
|
|
|
|
if not (
|
|
user.is_px_admin()
|
|
or (user.is_hospital_admin() and user.hospital == complaint.hospital)
|
|
or (user.is_champion() and dept_member_ok)
|
|
or (user.is_department_manager() and dept_member_ok)
|
|
or (user.is_basic_staff() and dept_member_ok)
|
|
or director_ok
|
|
):
|
|
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)
|