""" Services for Staff management """ import secrets import string from django.contrib.auth import get_user_model from django.template.loader import render_to_string from django.conf import settings from django.utils import timezone from apps.core.services import AuditService from apps.notifications.services import NotificationService from apps.accounts.services import PasswordResetTokenService User = get_user_model() class StaffService: """Service for managing staff user accounts""" @staticmethod def generate_username(staff): """ Generate a unique username from staff name. Format: first.last (lowercase) If duplicate exists, append number. """ base_username = f"{staff.first_name.lower()}.{staff.last_name.lower()}" username = base_username counter = 1 # Ensure uniqueness while User.objects.filter(username=username).exists(): username = f"{base_username}{counter}" counter += 1 return username @staticmethod def generate_password(length=12): """ Generate a secure random password. """ alphabet = string.ascii_letters + string.digits + string.punctuation password = ''.join(secrets.choice(alphabet) for _ in range(length)) return password @staticmethod def create_user_for_staff(staff, role='staff', request=None): """ Create a User account for a Staff member. If a user with the same email already exists, link it to the staff member instead. Args: staff: Staff instance role: Role name to assign (default: 'staff') request: HTTP request for audit logging Returns: tuple: (User instance, was_created: bool, password: None) - was_created is True if a new user was created - password is always None because users set passwords through a secure email link Raises: ValueError: If staff already has a user account or has no email """ if staff.user: raise ValueError("Staff member already has a user account") # Generate email (required for authentication) if not staff.email: raise ValueError("Staff member must have an email address") # Check if user with this email already exists existing_user = User.objects.filter(email=staff.email).first() if existing_user: # Link existing user to staff staff.user = existing_user staff.save(update_fields=['user']) # Update user's organization data if not set if not existing_user.hospital: existing_user.hospital = staff.hospital if not existing_user.department: existing_user.department = staff.department if not existing_user.employee_id: existing_user.employee_id = staff.employee_id existing_user.save(update_fields=['hospital', 'department', 'employee_id']) # Assign role if not already assigned from apps.accounts.models import Role as RoleModel try: role_obj = RoleModel.objects.get(name=role) if not existing_user.groups.filter(id=role_obj.group.id).exists(): existing_user.groups.add(role_obj.group) except RoleModel.DoesNotExist: pass # Log the action if request: AuditService.log_from_request( event_type='other', description=f"Existing user account linked to staff member {staff.get_full_name()}", request=request, content_object=existing_user, metadata={ 'staff_id': str(staff.id), 'staff_name': staff.get_full_name(), 'user_id': str(existing_user.id), 'action': 'linked_existing_user' } ) return existing_user, False, None # Existing user was linked, no password # Create new user account # Generate username (optional, for backward compatibility) username = StaffService.generate_username(staff) # Create user - email is now the username field user = User.objects.create_user( email=staff.email, password=None, first_name=staff.first_name, last_name=staff.last_name, username=username, # Optional field employee_id=staff.employee_id, hospital=staff.hospital, department=staff.department, is_active=True, is_provisional=False ) # Assign role from apps.accounts.models import Role as RoleModel try: role_obj = RoleModel.objects.get(name=role) user.groups.add(role_obj.group) except RoleModel.DoesNotExist: pass # Link to staff staff.user = user staff.save(update_fields=['user']) # Log the action if request: AuditService.log_from_request( event_type='user_creation', description=f"User account created for staff member {staff.get_full_name()}", request=request, content_object=user, metadata={ 'staff_id': str(staff.id), 'staff_name': staff.get_full_name(), 'role': role, 'action': 'created_new_user' } ) return user, True, None # New user was created with no emailed password @staticmethod def create_staff_for_user(user, staff_type='other', job_title='', request=None): """ Create a Staff record from an existing User. Args: user: User instance (must have first_name, last_name, hospital, employee_id) staff_type: Staff type (physician|nurse|admin|other) job_title: Job title string request: HTTP request for audit logging Returns: tuple: (Staff instance, was_created: bool) Raises: ValueError: If user already has a staff profile, lacks hospital, or employee_id is already taken by another staff member. """ from apps.organizations.models import Staff if hasattr(user, 'staff_profile') and user.staff_profile: raise ValueError("User already has a staff record") if not user.hospital: raise ValueError("User must have a hospital assigned before creating a staff record") if not user.employee_id: raise ValueError("Employee ID is required to create a staff record") if Staff.objects.filter(employee_id=user.employee_id).exists(): raise ValueError( f"Employee ID '{user.employee_id}' is already in use by another staff member" ) staff = Staff.objects.create( user=user, first_name=user.first_name, last_name=user.last_name, email=user.email, phone=user.phone or '', employee_id=user.employee_id, hospital=user.hospital, department=user.department, staff_type=staff_type or 'other', job_title=job_title or '', status='active', ) if request: AuditService.log_from_request( event_type='other', description=f"Staff record created for user {user.email}", request=request, content_object=staff, metadata={ 'user_id': str(user.id), 'staff_id': str(staff.id), 'staff_name': staff.get_full_name(), 'action': 'created_staff_from_user', } ) return staff, True @staticmethod def link_user_to_staff(staff, user_id, request=None): """ Link an existing User account to a Staff member. Args: staff: Staff instance user_id: UUID of the user to link request: HTTP request for audit logging Returns: Staff: Updated staff instance Raises: ValueError: If staff already has a user account or user not found """ if staff.user: raise ValueError("Staff member already has a user account") try: user = User.objects.get(id=user_id) except User.DoesNotExist: raise ValueError("User not found") # Link to staff staff.user = user staff.save(update_fields=['user']) # Update user's organization data if not user.hospital: user.hospital = staff.hospital if not user.department: user.department = staff.department if not user.employee_id: user.employee_id = staff.employee_id user.save(update_fields=['hospital', 'department', 'employee_id']) # Log the action if request: AuditService.log_from_request( event_type='other', description=f"User {user.email} linked to staff member {staff.get_full_name()}", request=request, content_object=staff, metadata={'user_id': str(user.id)} ) return staff @staticmethod def unlink_user_from_staff(staff, request=None): """ Remove User account association from a Staff member. Args: staff: Staff instance request: HTTP request for audit logging Returns: Staff: Updated staff instance Raises: ValueError: If staff has no user account """ if not staff.user: raise ValueError("Staff member has no user account") user = staff.user staff.user = None staff.save(update_fields=['user']) # Log the action if request: AuditService.log_from_request( event_type='other', description=f"User {user.email} unlinked from staff member {staff.get_full_name()}", request=request, content_object=staff, metadata={'user_id': str(user.id)} ) return staff @staticmethod def send_password_reset_email(staff, request=None): """ Send a one-time password reset link to a staff member. """ if not staff.email: raise ValueError("Staff member has no email address") user = staff.user if not user: raise ValueError("Staff member has no user account") reset_token = PasswordResetTokenService.create_reset_token(user) if request: base_url = request.build_absolute_uri("/") else: base_url = settings.SITE_URL if hasattr(settings, "SITE_URL") else "http://localhost:8000" reset_url = PasswordResetTokenService.build_reset_url(base_url, reset_token) context = { 'staff': staff, 'user': user, 'reset_url': reset_url, } subject = "Set Your PX360 Password" html_message = render_to_string('organizations/emails/staff_credentials.html', context) plain_message = f"""Welcome to PX360! Dear {staff.get_full_name()}, Your PX360 account has been created successfully. For your security, no password is sent by email. Username: {user.username} Email: {staff.email} Set your password here: {reset_url} This link expires in 24 hours. If you did not request this, please contact your system administrator. Best regards, The PX360 Team """ notification_log = NotificationService.send_email( email=staff.email, subject=subject, message=plain_message, html_message=html_message, related_object=staff, metadata={ 'notification_type': 'staff_password_reset', 'staff_id': str(staff.id), 'user_id': str(user.id), 'username': user.username } ) if request: AuditService.log_from_request( event_type='password_reset', description=f"Password reset link sent to {staff.email} for staff member {staff.get_full_name()}", request=request, content_object=staff, metadata={ 'notification_log_id': str(notification_log.id) if notification_log else None } ) return notification_log @staticmethod def send_credentials_email(staff, password=None, request=None): return StaffService.send_password_reset_email(staff, request) @staticmethod def reset_password_and_resend_credentials(staff, request=None): """ Reset user password and resend password reset link. Args: staff: Staff instance request: HTTP request for building absolute URLs and audit logging Returns: tuple: (None, notification_log: NotificationLog) Raises: ValueError: If staff has no user account or no email """ if not staff.user: raise ValueError("Staff member has no user account") if not staff.email: raise ValueError("Staff member has no email address") notification_log = StaffService.send_password_reset_email(staff, request) # Log the action if request: AuditService.log_from_request( event_type='password_reset', description=f"Password reset link resent to {staff.email} for staff member {staff.get_full_name()}", request=request, content_object=staff, metadata={ 'user_id': str(staff.user.id), 'notification_log_id': str(notification_log.id) if notification_log else None } ) return None, notification_log @staticmethod def get_staff_type_role(staff_type): """ Map staff_type to role name. Currently all staff get the 'staff' role. """ role_mapping = { 'physician': 'staff', 'nurse': 'staff', 'admin': 'staff', 'other': 'staff' } return role_mapping.get(staff_type, 'staff')