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 apps.core.decorators import block_source_user, hospital_admin_required from .models import Department, Hospital, Organization, Patient, Staff, StaffSection, StaffSubsection 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(hospital=request.tenant_hospital) else: queryset = queryset.none() elif user.hospital: queryset = queryset.filter(hospital=user.hospital) # Apply filters hospital_filter = request.GET.get("hospital") if hospital_filter: queryset = queryset.filter(hospital_id=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("hospital", "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 try: user_account, was_created, password = StaffService.create_user_for_staff( staff, role='staff', request=request ) if was_created and password: try: StaffService.send_credentials_email(staff, password, request) messages.success(request, "Staff member created and credentials 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: password = StaffService.generate_password() staff.user.set_password(password) staff.user.save() StaffService.send_credentials_email(staff, password, request) messages.success(request, "Credentials 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 try: user_account, was_created, password = StaffService.create_user_for_staff( staff, role='staff', request=request ) if was_created and password: try: StaffService.send_credentials_email(staff, password, request) messages.success(request, "User account created and credentials 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: 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": Department.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 django.db.models import Q from collections import defaultdict department = get_object_or_404( Department.objects.select_related("hospital", "manager", "parent", "respondent"), 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) | Q(involved_departments__department=department) ).select_related( "patient", "assigned_to", "category", "domain", "subcategory_obj", "classification_obj", "staff" ).prefetch_related( "involved_staff__staff" ).distinct().order_by("-created_at") inquiries = Inquiry.objects.filter( Q(department=department) | Q(outgoing_department=department) ).select_related("assigned_to", "patient").order_by("-created_at") observations = Observation.objects.filter( assigned_department=department ).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) 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(), } 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( hospital=department.hospital, status="active" ).order_by("first_name", "last_name") pending_actions = [] from apps.complaints.models import ComplaintExplanation, ComplaintInvolvedDepartment 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, response_submitted=False, ).select_related("complaint").order_by("-forwarded_at") for pc in pending_complaint_dept_responses: pending_actions.append({ "type": "complaint_department_response", "type_label": _("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": "#", "badge_color": "orange", "item_id": str(pc.pk), "item_type": "complaint_involved_department", "complaint_id": str(pc.complaint_id), "department_name": pc.department.name, }) # 2. Complaint Explanations (staff-level) pending_explanations = ComplaintExplanation.objects.filter( complaint__department=department, is_used=False, ).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, 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", }) pending_inquiries = Inquiry.objects.filter( Q(outgoing_department=department) | Q(department=department), transferred_at__isnull=False, department_responded_at__isnull=True, 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 Response"), "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": "/inquiries/{}/department-response/".format(inq.pk), "badge_color": "cyan", }) # 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[:50], "inquiries": inquiries[:50], "observations": observations[:50], "stats": stats, "active_tab": active_tab, "assignable_staff": assignable_staff, "managers": managers, "standards_data": standards_data, "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_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), } return render(request, "organizations/department_detail.html", context) @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) | Q(involved_departments__department=department) ).distinct() # 1. Complaint Trend (last 12 months) complaint_trend_raw = ( complaints_qs.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_qs.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_qs.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_qs.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_qs.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_qs.count() resolved_count = complaints_qs.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_qs.filter(reopened_from__isnull=False).count() reassigned_count = ComplaintUpdate.objects.filter( complaint__in=complaints_qs, 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"]) def set_department_manager(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_hospital_admin() and user.hospital == department.hospital) ): messages.error(request, _("You don't have permission to set the manager.")) return redirect("organizations:department_detail", pk=pk) manager_id = request.POST.get("manager_id") if manager_id: manager = Staff.objects.filter( pk=manager_id, hospital=department.hospital, status="active" ).first() if not manager: messages.error(request, _("Invalid staff member selected.")) return redirect("organizations:department_detail", pk=pk) if not manager.user: from apps.organizations.services import StaffService from django.contrib.auth.models import Group if not manager.email: messages.warning( request, _(f"Selected staff {manager.get_full_name()} has no email. Add an email and create a user account first."), ) return redirect("organizations:department_detail", pk=pk) try: user_account, _created, _result = StaffService.create_user_for_staff( manager, role="department_manager", request=request ) department.manager = user_account department.save(update_fields=["manager", "updated_at"]) messages.success( request, _(f"Manager set to {manager.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("organizations:department_detail", pk=pk) else: department.manager = manager.user department.save(update_fields=["manager", "updated_at"]) messages.success( request, _(f"Manager set to {manager.get_full_name()}."), ) else: department.manager = None department.save(update_fields=["manager", "updated_at"]) messages.success(request, _("Manager removed.")) return redirect("organizations:department_detail", pk=pk) @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