""" Organizations views and viewsets """ from django.db import models from rest_framework import status, viewsets from rest_framework.decorators import action, api_view, permission_classes from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.response import Response from apps.accounts.permissions import ( CanAccessDepartmentData, CanAccessHospitalData, IsPXAdminOrHospitalAdmin, IsPXAdmin, ) from .models import Department, Hospital, Organization, Patient, Staff, Location, MainSection, SubSection from .models import Staff as StaffModel from .serializers import ( DepartmentSerializer, HospitalSerializer, LocationSerializer, MainSectionSerializer, OrganizationSerializer, PatientListSerializer, PatientSerializer, StaffSerializer, SubSectionSerializer, ) class OrganizationViewSet(viewsets.ModelViewSet): """ ViewSet for Organization model. Permissions: - PX Admins can manage organizations - Others can view organizations """ queryset = Organization.objects.all() serializer_class = OrganizationSerializer permission_classes = [IsAuthenticated] filterset_fields = ["status", "city"] search_fields = ["name", "name_ar", "code", "license_number"] ordering_fields = ["name", "created_at"] ordering = ["name"] def get_queryset(self): """Filter organizations based on user role""" queryset = super().get_queryset().prefetch_related("hospitals") user = self.request.user # PX Admins see all organizations if user.is_px_admin(): return queryset # Hospital Admins and others see their organization if user.is_hospital_admin() and user.hospital and user.hospital.organization: return queryset.filter(id=user.hospital.organization.id) # Others with hospital see their organization if user.hospital and user.hospital.organization: return queryset.filter(id=user.hospital.organization.id) return queryset.none() class HospitalViewSet(viewsets.ModelViewSet): """ ViewSet for Hospital model. Permissions: - PX Admins and Hospital Admins can manage hospitals - Others can view hospitals they belong to """ queryset = Hospital.objects.all() serializer_class = HospitalSerializer permission_classes = [IsAuthenticated, CanAccessHospitalData] filterset_fields = ["status", "city", "organization"] search_fields = ["name", "name_ar", "code", "city"] ordering_fields = ["name", "created_at"] ordering = ["name"] def get_queryset(self): """Filter hospitals based on user role""" queryset = super().get_queryset() user = self.request.user # PX Admins see all hospitals if user.is_px_admin(): return queryset # Hospital Admins see their hospital if user.is_hospital_admin() and user.hospital: return queryset.filter(id=user.hospital.id) # Department Managers see their hospital if user.is_department_manager() and user.hospital: return queryset.filter(id=user.hospital.id) # Others see hospitals they're associated with if user.hospital: return queryset.filter(id=user.hospital.id) return queryset.none() class DepartmentViewSet(viewsets.ModelViewSet): """ ViewSet for Department model. Permissions: - PX Admins and Hospital Admins can manage departments - Department Managers can view their department """ queryset = Department.objects.all() serializer_class = DepartmentSerializer permission_classes = [IsAuthenticated, CanAccessDepartmentData] filterset_fields = ["status", "hospital", "parent", "hospital__organization"] search_fields = ["name", "name_ar", "code"] ordering_fields = ["name", "created_at"] ordering = ["hospital", "name"] def get_queryset(self): """Filter departments based on user role""" queryset = super().get_queryset().select_related("hospital", "parent", "manager") user = self.request.user # PX Admins see all departments if user.is_px_admin(): return queryset # Hospital Admins see departments in their hospital if user.is_hospital_admin() and user.hospital: return queryset.filter(hospital=user.hospital) # Department Managers see their department and sub-departments if user.is_department_manager() and user.department: return queryset.filter(hospital=user.hospital).filter( models.Q(id=user.department.id) | models.Q(parent=user.department) ) # Others see departments in their hospital if user.hospital: return queryset.filter(hospital=user.hospital) return queryset.none() class StaffViewSet(viewsets.ModelViewSet): """ ViewSet for Staff model. Permissions: - PX Admins and Hospital Admins can manage staff - Others can view staff """ queryset = StaffModel.objects.all() serializer_class = StaffSerializer permission_classes = [IsAuthenticated] filterset_fields = [ "status", "hospital", "department", "staff_type", "specialization", "job_title", "hospital__organization", ] search_fields = [ "first_name", "last_name", "first_name_ar", "last_name_ar", "employee_id", "license_number", "job_title", ] ordering_fields = ["last_name", "created_at"] ordering = ["last_name", "first_name"] def get_permissions(self): """Set permissions based on action""" if self.action in [ "create_user_account", "link_user", "unlink_user", "send_invitation", "reset_password_and_resend", ]: return [IsAuthenticated()] return super().get_permissions() def get_queryset(self): """Filter staff based on user role""" queryset = super().get_queryset().select_related("hospital", "department", "user") user = self.request.user # Source Users don't have access to staff management if user.is_source_user(): return queryset.none() # PX Admins see staff in their selected hospital if user.is_px_admin(): tenant_hospital = getattr(self.request, "tenant_hospital", None) if tenant_hospital: return queryset.filter(hospital=tenant_hospital) return queryset # Hospital Admins see staff in their hospital if user.is_hospital_admin() and user.hospital: return queryset.filter(hospital=user.hospital) # Department Managers see staff in their department if user.is_department_manager() and user.department: return queryset.filter(department=user.department) # Others see staff in their hospital if user.hospital: return queryset.filter(hospital=user.hospital) return queryset.none() @action(detail=False, methods=["get"], url_path="search") def search(self, request): q = request.query_params.get("q", "").strip() queryset = self.get_queryset().filter(status="active") if q: queryset = queryset.filter( models.Q(first_name__icontains=q) | models.Q(last_name__icontains=q) | models.Q(name__icontains=q) | models.Q(name_ar__icontains=q) | models.Q(employee_id__icontains=q) ) queryset = queryset[:20] data = [ {"id": s.id, "first_name": s.first_name, "last_name": s.last_name, "employee_id": s.employee_id} for s in queryset ] return Response(data) @action(detail=True, methods=["post"]) def create_user_account(self, request, pk=None): """ Create a user account for a staff member. Auto-generates username, password, and sends email. """ staff = self.get_object() if staff.user: return Response({"error": "Staff member already has a user account"}, status=status.HTTP_400_BAD_REQUEST) # Check permissions user = request.user if not user.is_px_admin() and not user.is_hospital_admin(): return Response( {"error": "You do not have permission to create user accounts"}, status=status.HTTP_403_FORBIDDEN ) # Hospital Admins can only create accounts for staff in their hospital if user.is_hospital_admin() and staff.hospital != user.hospital: return Response( {"error": "You can only create accounts for staff in your hospital"}, status=status.HTTP_403_FORBIDDEN ) # Get role from request or use default based on staff_type from .services import StaffService role = request.data.get("role", StaffService.get_staff_type_role(staff.staff_type)) try: user_account, was_created, password = StaffService.create_user_for_staff(staff, role=role, request=request) if was_created: # Send email with credentials (password is already set in create_user_for_staff) try: StaffService.send_credentials_email(staff, password, request) message = "User account created and credentials emailed successfully" except Exception as e: message = f"User account created. Email sending failed: {str(e)}" else: # Existing user was linked - no password to generate or email to send message = "Existing user account linked successfully. The staff member can now log in with their existing credentials." serializer = self.get_serializer(staff) return Response( {"message": message, "staff": serializer.data, "email": user_account.email, "was_created": was_created}, status=status.HTTP_200_OK if not was_created else status.HTTP_201_CREATED, ) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @action(detail=True, methods=["post"]) def link_user(self, request, pk=None): """ Link an existing user account to a staff member. """ staff = self.get_object() if staff.user: return Response({"error": "Staff member already has a user account"}, status=status.HTTP_400_BAD_REQUEST) # Check permissions user = request.user if not user.is_px_admin() and not user.is_hospital_admin(): return Response( {"error": "You do not have permission to link user accounts"}, status=status.HTTP_403_FORBIDDEN ) # Hospital Admins can only link accounts for staff in their hospital if user.is_hospital_admin() and staff.hospital != user.hospital: return Response( {"error": "You can only link accounts for staff in your hospital"}, status=status.HTTP_403_FORBIDDEN ) user_id = request.data.get("user_id") if not user_id: return Response({"error": "user_id is required"}, status=status.HTTP_400_BAD_REQUEST) from .services import StaffService try: StaffService.link_user_to_staff(staff, user_id, request=request) serializer = self.get_serializer(staff) return Response({"message": "User account linked successfully", "staff": serializer.data}) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @action(detail=True, methods=["post"]) def unlink_user(self, request, pk=None): """ Remove user account association from a staff member. """ staff = self.get_object() if not staff.user: return Response({"error": "Staff member has no user account"}, status=status.HTTP_400_BAD_REQUEST) # Check permissions user = request.user if not user.is_px_admin() and not user.is_hospital_admin(): return Response( {"error": "You do not have permission to unlink user accounts"}, status=status.HTTP_403_FORBIDDEN ) # Hospital Admins can only unlink accounts for staff in their hospital if user.is_hospital_admin() and staff.hospital != user.hospital: return Response( {"error": "You can only unlink accounts for staff in your hospital"}, status=status.HTTP_403_FORBIDDEN ) from .services import StaffService try: StaffService.unlink_user_from_staff(staff, request=request) serializer = self.get_serializer(staff) return Response({"message": "User account unlinked successfully", "staff": serializer.data}) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @action(detail=True, methods=["post"]) def send_invitation(self, request, pk=None): """ Send credentials email to staff member. Generates new password and emails it. """ staff = self.get_object() if not staff.user: return Response({"error": "Staff member has no user account"}, status=status.HTTP_400_BAD_REQUEST) # Check permissions user = request.user if not user.is_px_admin() and not user.is_hospital_admin(): return Response( {"error": "You do not have permission to send invitations"}, status=status.HTTP_403_FORBIDDEN ) # Hospital Admins can only send invitations to staff in their hospital if user.is_hospital_admin() and staff.hospital != user.hospital: return Response( {"error": "You can only send invitations to staff in your hospital"}, status=status.HTTP_403_FORBIDDEN ) from .services import StaffService try: # Generate new password password = StaffService.generate_password() # Update user password staff.user.set_password(password) staff.user.save() # Send email StaffService.send_credentials_email(staff, password, request) serializer = self.get_serializer(staff) return Response({"message": "Invitation email sent successfully", "staff": serializer.data}) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @action(detail=True, methods=["post"]) def reset_password_and_resend(self, request, pk=None): """ Reset user password and resend credentials email. This action: 1. Generates a new random password 2. Updates the user's password 3. Sends credentials email via NotificationService Returns the new password in the response (only shown to admin). """ staff = self.get_object() if not staff.user: return Response({"error": "Staff member has no user account"}, status=status.HTTP_400_BAD_REQUEST) # Check permissions user = request.user if not user.is_px_admin() and not user.is_hospital_admin(): return Response( {"error": "You do not have permission to reset passwords"}, status=status.HTTP_403_FORBIDDEN ) # Hospital Admins can only reset passwords for staff in their hospital if user.is_hospital_admin() and staff.hospital != user.hospital: return Response( {"error": "You can only reset passwords for staff in your hospital"}, status=status.HTTP_403_FORBIDDEN ) from .services import StaffService try: # Reset password and resend credentials new_password, notification_log = StaffService.reset_password_and_resend_credentials(staff, request=request) serializer = self.get_serializer(staff) return Response( { "success": True, "message": "Password reset successfully and credentials email sent", "new_password": new_password, # Show password to admin "email_sent_to": staff.email, "notification_log_id": str(notification_log.id) if notification_log else None, "staff": serializer.data, } ) except ValueError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) @action(detail=False, methods=["get"]) def hierarchy(self, request): """ Get staff hierarchy as D3-compatible JSON. Used for interactive tree visualization. Note: This action uses a more permissive queryset to allow all authenticated users to view the organization hierarchy for visualization purposes. """ from django.db.models import Q # Get filter parameters hospital_id = request.query_params.get("hospital") department_id = request.query_params.get("department") search = request.query_params.get("search", "").strip() # Build base queryset - use all staff for hierarchy visualization # This allows any authenticated user to see the full organizational structure queryset = StaffModel.objects.all().select_related("report_to", "hospital", "department") # Apply filters if hospital_id: queryset = queryset.filter(hospital_id=hospital_id) if department_id: queryset = queryset.filter(department_id=department_id) if search: queryset = queryset.filter( Q(first_name__icontains=search) | Q(last_name__icontains=search) | Q(employee_id__icontains=search) ) # Get all staff with their managers staff_list = queryset.select_related("report_to", "hospital", "department") # Build staff lookup dictionary staff_dict = {staff.id: staff for staff in staff_list} # Build hierarchy tree def build_node(staff): """Recursively build hierarchy node for D3""" node = { "id": staff.id, "name": staff.get_full_name(), "employee_id": staff.employee_id, "job_title": staff.job_title or "", "hospital": staff.hospital.name if staff.hospital else "", "department": staff.department.name if staff.department else "", "status": staff.status, "staff_type": staff.staff_type, "team_size": 0, # Will be calculated "children": [], } # Find direct reports direct_reports = [s for s in staff_list if s.report_to_id == staff.id] # Recursively build children for report in direct_reports: child_node = build_node(report) node["children"].append(child_node) node["team_size"] += 1 + child_node["team_size"] return node # Group root nodes by organization from collections import defaultdict org_groups = defaultdict(list) # Find root nodes (staff with no manager in the filtered set) root_staff = [ staff for staff in staff_list if staff.report_to_id is None or staff.report_to_id not in staff_dict ] # Group root staff by organization for staff in root_staff: if staff.hospital and staff.hospital.organization: org_name = staff.hospital.organization.name else: org_name = "Organization" org_groups[org_name].append(staff) # Build hierarchy for each organization hierarchy = [] top_managers = 0 for org_name, org_root_staff in org_groups.items(): # Build hierarchy nodes for this organization's root staff org_root_nodes = [build_node(staff) for staff in org_root_staff] # Calculate total team size for this organization org_team_size = sum(node["team_size"] + 1 for node in org_root_nodes) # Create organization node as parent org_node = { "id": None, "name": org_name, "employee_id": "", "job_title": "Organization", "hospital": "", "department": "", "status": "active", "staff_type": "organization", "team_size": org_team_size, "children": org_root_nodes, "is_organization_root": True, } hierarchy.append(org_node) top_managers += len(org_root_nodes) # If there are multiple organizations, wrap them in a single root if len(hierarchy) > 1: total_team_size = sum(node["team_size"] for node in hierarchy) hierarchy = [ { "id": None, "name": "All Organizations", "employee_id": "", "job_title": "", "hospital": "", "department": "", "status": "active", "staff_type": "root", "team_size": total_team_size, "children": hierarchy, "is_virtual_root": True, } ] # Calculate statistics total_staff = len(staff_list) return Response( {"hierarchy": hierarchy, "statistics": {"total_staff": total_staff, "top_managers": top_managers}} ) class PatientViewSet(viewsets.ModelViewSet): """ ViewSet for Patient model. Permissions: - All authenticated users can view patients - PX Admins and Hospital Admins can manage patients """ queryset = Patient.objects.all() permission_classes = [IsAuthenticated] filterset_fields = ["status", "gender", "primary_hospital", "city", "primary_hospital__organization"] search_fields = ["mrn", "national_id_hash", "first_name", "last_name", "phone", "email"] ordering_fields = ["last_name", "created_at"] ordering = ["last_name", "first_name"] def get_serializer_class(self): """Use simplified serializer for list view""" if self.action == "list": return PatientListSerializer return PatientSerializer def get_queryset(self): """Filter patients based on user role""" queryset = super().get_queryset().select_related("primary_hospital") user = self.request.user # PX Admins see patients in their selected hospital if user.is_px_admin(): tenant_hospital = getattr(self.request, "tenant_hospital", None) if tenant_hospital: return queryset.filter(primary_hospital=tenant_hospital) return queryset # Hospital Admins see patients in their hospital if user.is_hospital_admin() and user.hospital: return queryset.filter(primary_hospital=user.hospital) # Others see patients in their hospital if user.hospital: return queryset.filter(primary_hospital=user.hospital) return queryset @action(detail=False, methods=["get"], url_path="search") def search(self, request): q = request.query_params.get("q", "").strip() queryset = self.get_queryset().filter(status="active") if q: from apps.core.encryption import compute_national_id_hash nid_hash = compute_national_id_hash(q) queryset = queryset.filter( models.Q(mrn__icontains=q) | models.Q(first_name__icontains=q) | models.Q(last_name__icontains=q) | models.Q(national_id_hash=nid_hash) | models.Q(phone__icontains=q) ) queryset = queryset[:20] data = [ { "id": p.id, "first_name": p.first_name, "last_name": p.last_name, "mrn": p.mrn, "national_id_masked": p.get_masked_national_id(), } for p in queryset ] return Response(data) class LocationViewSet(viewsets.ReadOnlyModelViewSet): """ ViewSet for Location model. Publicly accessible for complaint form dropdowns. """ queryset = Location.objects.all() serializer_class = LocationSerializer permission_classes = [] # Public access ordering = ["id"] class MainSectionViewSet(viewsets.ReadOnlyModelViewSet): """ ViewSet for MainSection model. Publicly accessible for complaint form dropdowns. """ queryset = MainSection.objects.all() serializer_class = MainSectionSerializer permission_classes = [] # Public access ordering = ["id"] class SubSectionViewSet(viewsets.ReadOnlyModelViewSet): """ ViewSet for SubSection model. Publicly accessible for complaint form dropdowns. Supports filtering by location and main_section. """ queryset = SubSection.objects.all() serializer_class = SubSectionSerializer permission_classes = [] # Public access filterset_fields = ["location", "main_section"] ordering = ["internal_id"] # Dedicated API endpoints for complaint form dropdowns # These avoid conflicts with UI routes @api_view(["GET"]) @permission_classes([]) def api_location_list(request): """API endpoint for location dropdown (public access)""" locations = Location.objects.filter(id__in=[48, 49, 82, 110]).order_by("id") serializer = LocationSerializer(locations, many=True) return Response(serializer.data) @api_view(["GET"]) @permission_classes([]) def api_main_section_list(request): """API endpoint for main section dropdown (public access)""" sections = MainSection.objects.all().order_by("id") serializer = MainSectionSerializer(sections, many=True) return Response(serializer.data) @api_view(["GET"]) @permission_classes([]) def api_subsection_list(request): """API endpoint for subsection dropdown (public access) Optional query params: - location: Filter by location ID - main_section: Filter by main section ID """ subsections = SubSection.objects.all().order_by("name") location_id = request.GET.get("location") main_section_id = request.GET.get("main_section") if location_id: subsections = subsections.filter(location_id=location_id) if main_section_id: subsections = subsections.filter(main_section_id=main_section_id) serializer = SubSectionSerializer(subsections, many=True) return Response(serializer.data) # AJAX endpoints for complaint form cascading dropdowns @api_view(["GET"]) @permission_classes([]) def ajax_main_sections(request): """ AJAX endpoint for main sections filtered by location. Used in complaint form for cascading dropdown. """ location_id = request.GET.get("location_id") if location_id: # Get main sections that have subsections for this location available_section_ids = ( SubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct() ) main_sections = MainSection.objects.filter(id__in=available_section_ids).order_by("name_en") else: main_sections = MainSection.objects.none() serializer = MainSectionSerializer(main_sections, many=True) return Response({"sections": serializer.data}) @api_view(["GET"]) @permission_classes([]) def ajax_subsections(request): """ AJAX endpoint for subsections filtered by location and main section. Used in complaint form for cascading dropdown. """ location_id = request.GET.get("location_id") main_section_id = request.GET.get("main_section_id") if location_id and main_section_id: subsections = SubSection.objects.filter(location_id=location_id, main_section_id=main_section_id).order_by( "name_en" ) else: subsections = SubSection.objects.none() serializer = SubSectionSerializer(subsections, many=True) return Response({"subsections": serializer.data}) @api_view(["GET"]) @permission_classes([]) def ajax_departments(request): """ AJAX endpoint for departments filtered by hospital. Used in complaint form for cascading dropdown. """ hospital_id = request.GET.get("hospital_id") if hospital_id: departments = Department.objects.filter(hospital_id=hospital_id, status="active").order_by("name") else: departments = Department.objects.none() serializer = DepartmentSerializer(departments, many=True) return Response({"departments": serializer.data}) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def api_staff_hierarchy(request): """ API endpoint for staff hierarchy data (used by D3 visualization). GET /organizations/api/staff/hierarchy/ Query params: - hospital: Filter by hospital ID - department: Filter by department ID - max_depth: Maximum hierarchy depth to return (default: 3, use 0 for all) - flat: Return flat list instead of tree (for large datasets) Returns: { "hierarchy": [...], "statistics": { "total_staff": 100, "top_managers": 5 } } """ import time start_time = time.time() user = request.user cache_key = f"staff_hierarchy:{user.id}:{user.hospital_id if user.hospital else 'all'}" # Check cache first (30 second cache for real-time feel) from django.core.cache import cache cached_result = cache.get(cache_key) if cached_result: return Response(cached_result) # Get base queryset with only needed fields queryset = Staff.objects.select_related("hospital", "department", "report_to").only( "id", "first_name", "last_name", "employee_id", "job_title", "hospital__name", "department__name", "report_to_id", "status", ) # Apply RBAC if user.is_px_admin() and hasattr(request, "tenant_hospital") and request.tenant_hospital: queryset = queryset.filter(hospital=request.tenant_hospital) elif not user.is_px_admin() and 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) cache_key += f":h:{hospital_filter}" department_filter = request.GET.get("department") if department_filter: queryset = queryset.filter(department_id=department_filter) cache_key += f":d:{department_filter}" # Get options max_depth = int(request.GET.get("max_depth", 3)) flat_mode = request.GET.get("flat", "false").lower() == "true" # Fetch all staff as values for faster processing all_staff = list(queryset) total_count = len(all_staff) # OPTIMIZATION 1: Pre-calculate team sizes using a dictionary report_count = {} for staff in all_staff: if staff.report_to_id: report_count[staff.report_to_id] = report_count.get(staff.report_to_id, 0) + 1 # OPTIMIZATION 2: Build lookup dictionaries staff_by_id = {str(s.id): s for s in all_staff} children_by_parent = {} for staff in all_staff: parent_id = str(staff.report_to_id) if staff.report_to_id else None if parent_id not in children_by_parent: children_by_parent[parent_id] = [] children_by_parent[parent_id].append(staff) # OPTIMIZATION 3: Recursive function with depth limit and memoization def build_hierarchy_optimized(parent_id=None, current_depth=0): """Build hierarchy tree using pre-calculated lookups""" if max_depth > 0 and current_depth >= max_depth: return [] children = children_by_parent.get(parent_id, []) result = [] for staff in children: node = { "name": staff.get_localized_name() or staff.employee_id, "id": str(staff.id), "employee_id": staff.employee_id, "job_title": staff.job_title or "", "department": staff.department.name if staff.department else "", "hospital": staff.hospital.name if staff.hospital else "", "team_size": report_count.get(str(staff.id), 0), "has_children": str(staff.id) in children_by_parent, "children": [], # Lazy load - children fetched on expand } # Only build children if not at max depth if max_depth == 0 or current_depth < max_depth - 1: node["children"] = build_hierarchy_optimized(str(staff.id), current_depth + 1) result.append(node) return result # Build hierarchy starting from top-level hierarchy = build_hierarchy_optimized(None, 0) # Calculate statistics top_managers = len(children_by_parent.get(None, [])) result = { "hierarchy": hierarchy, "statistics": { "total_staff": total_count, "top_managers": top_managers, "load_time_ms": int((time.time() - start_time) * 1000), }, } # Cache for 30 seconds cache.set(cache_key, result, 30) return Response(result) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def api_staff_hierarchy_children(request, staff_id): """ API endpoint to fetch children of a specific staff member. Used for lazy loading in D3 visualization. GET /organizations/api/staff/hierarchy/{staff_id}/children/ Returns: { "staff_id": "uuid", "children": [...] } """ user = request.user # Get the parent staff member try: parent = Staff.objects.select_related("hospital", "department").get(id=staff_id) except Staff.DoesNotExist: return Response({"error": "Staff not found"}, status=404) # Check permission if not user.is_px_admin() and user.hospital != parent.hospital: return Response({"error": "Permission denied"}, status=403) # Get children with optimized query children = ( Staff.objects.select_related("hospital", "department") .filter(report_to=parent) .only("id", "first_name", "last_name", "employee_id", "job_title", "hospital__name", "department__name") ) # Pre-calculate which children have their own children child_ids = list(children.values_list("id", flat=True)) children_with_reports = set( Staff.objects.filter(report_to_id__in=child_ids).values_list("report_to_id", flat=True).distinct() ) result = [] for staff in children: result.append( { "name": staff.get_localized_name() or staff.employee_id, "id": str(staff.id), "employee_id": staff.employee_id, "job_title": staff.job_title or "", "department": staff.department.name if staff.department else "", "hospital": staff.hospital.name if staff.hospital else "", "team_size": 0, # Will be calculated on next expand "has_children": staff.id in children_with_reports, "children": [], # Empty - load on next expand } ) return Response({"staff_id": str(staff_id), "children": result}) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def api_patient_search(request): """ Standalone patient search API endpoint. Used by survey manual send form to search for patients. URL: /api/patients/search/?q=&active=true """ q = request.query_params.get("q", "").strip() queryset = Patient.objects.filter(status="active") # Filter by user's hospital if not admin user = request.user if hasattr(user, "hospital") and user.hospital and not user.is_superuser: queryset = queryset.filter(primary_hospital=user.hospital) if q: queryset = queryset.filter( models.Q(mrn__icontains=q) | models.Q(first_name__icontains=q) | models.Q(last_name__icontains=q) | models.Q(national_id__icontains=q) | models.Q(phone__icontains=q) ) queryset = queryset[:20] data = [{"id": p.id, "first_name": p.first_name, "last_name": p.last_name, "mrn": p.mrn} for p in queryset] return Response(data) @api_view(["GET"]) @permission_classes([IsAuthenticated]) def api_staff_search(request): """ Standalone staff search API endpoint. Used by survey manual send form to search for staff. URL: /api/staffs/search/?q=&active=true """ q = request.query_params.get("q", "").strip() queryset = Staff.objects.filter(status="active") # Filter by user's hospital if not admin user = request.user if hasattr(user, "hospital") and user.hospital and not user.is_superuser: queryset = queryset.filter(hospital=user.hospital) if q: queryset = queryset.filter( models.Q(first_name__icontains=q) | models.Q(last_name__icontains=q) | models.Q(name__icontains=q) | models.Q(name_ar__icontains=q) | models.Q(employee_id__icontains=q) ) queryset = queryset[:20] data = [ {"id": s.id, "first_name": s.first_name, "last_name": s.last_name, "employee_id": s.employee_id} for s in queryset ] return Response(data)