1089 lines
38 KiB
Python
1089 lines
38 KiB
Python
"""
|
|
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", "category"]
|
|
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() or user.is_hospital_admin() or user.is_department_manager()):
|
|
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
|
|
)
|
|
|
|
# Department Managers can only create accounts for staff in their department
|
|
if user.is_department_manager() and (
|
|
not user.department or staff.department != user.department
|
|
):
|
|
return Response(
|
|
{"error": "You can only create accounts for staff in your department"}, status=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
# Get role - always 'staff' by default for staff accounts
|
|
from .services import StaffService
|
|
|
|
role = 'staff'
|
|
|
|
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.active_locations()
|
|
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=<query>&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=<query>&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)
|