using AutoMapper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Dtos.Employees; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Hubs; using Marco.Pms.Services.Service; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; using System.Data; using System.Net; namespace MarcoBMS.Services.Controllers { [Route("api/[controller]")] [ApiController] [Authorize] public class EmployeeController : ControllerBase { private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly UserManager _userManager; private readonly IEmailSender _emailSender; private readonly EmployeeHelper _employeeHelper; private readonly UserHelper _userHelper; private readonly GeneralHelper _generalHelper; private readonly IConfiguration _configuration; private readonly ILoggingService _logger; private readonly IHubContext _signalR; private readonly PermissionServices _permission; private readonly IMapper _mapper; private readonly IProjectServices _projectServices; private readonly Guid tenantId; private readonly Guid organizationId; public EmployeeController(IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScopeFactory, UserManager userManager, IEmailSender emailSender, ApplicationDbContext context, EmployeeHelper employeeHelper, UserHelper userHelper, IConfiguration configuration, ILoggingService logger, IHubContext signalR, PermissionServices permission, IProjectServices projectServices, IMapper mapper, GeneralHelper generalHelper) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _serviceScopeFactory = serviceScopeFactory; _context = context; _userManager = userManager; _emailSender = emailSender; _employeeHelper = employeeHelper; _userHelper = userHelper; _generalHelper = generalHelper; _configuration = configuration; _logger = logger; _signalR = signalR; _permission = permission; _projectServices = projectServices; _mapper = mapper; tenantId = _userHelper.GetTenantId(); organizationId = _userHelper.GetCurrentOrganizationId(); } [HttpGet] [Route("roles/{employeeId?}")] public async Task GetRoles(Guid employeeId) { if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } Guid tenantId = GetTenantId(); var empRoles = await _context.EmployeeRoleMappings.Where(c => c.EmployeeId == employeeId).Include(c => c.Role).Include(c => c.Employee).ToListAsync(); if (empRoles.Any()) { List roles = new List(); foreach (EmployeeRoleMapping mapping in empRoles) { roles.Add(new EmployeeRolesVM() { Id = mapping.Id, EmployeeId = mapping.EmployeeId, Name = mapping.Role != null ? mapping.Role.Role : null, Description = mapping.Role != null ? mapping.Role.Description : null, IsEnabled = mapping.IsEnabled, RoleId = mapping.RoleId, }); } return Ok(ApiResponse.SuccessResponse(roles, "Success.", 200)); } else { return BadRequest(ApiResponse.ErrorResponse("This employee has no assigned permissions.", "This employee has no assigned permissions.", 400)); } } [HttpGet("list/organizations/{projectId}")] public async Task GetEmployeesByProjectAsync(Guid projectId, [FromQuery] string searchString, [FromQuery] Guid? organizationId) { try { // Get the currently logged-in employee information var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var projectTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Projects.FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId); }); var tenantTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Tenants.FirstOrDefaultAsync(t => t.Id == tenantId); }); await Task.WhenAll(projectTask, tenantTask); var project = projectTask.Result; var tenant = tenantTask.Result; if (project == null || tenant == null) { _logger.LogWarning("Project {ProjectId} not found in database for tenant {TenantId}", projectId, tenantId); return NotFound(ApiResponse.ErrorResponse("Project not found", "Project not found", 404)); } // Check if the logged-in employee has permission for the requested project var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId); if (!hasProjectPermission) { _logger.LogWarning("User {EmployeeId} attempts to get employees for project {ProjectId} without permission", loggedInEmployee.Id, projectId); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have access to view the employees for this project", 403)); } var organizationQuery = _context.ProjectOrgMappings .Include(po => po.ProjectService) .Where(po => po.ProjectService != null && po.ProjectService.ProjectId == projectId); if (loggedInEmployee.OrganizationId != project.PMCId && loggedInEmployee.OrganizationId != project.PromoterId && loggedInEmployee.OrganizationId != tenant.OrganizationId) { organizationQuery = organizationQuery.Where(po => po.ParentOrganizationId == loggedInEmployee.OrganizationId || po.OrganizationId == loggedInEmployee.OrganizationId); } var organizationIds = await organizationQuery.Select(po => po.OrganizationId).ToListAsync(); if (loggedInEmployee.OrganizationId == project.PMCId || loggedInEmployee.OrganizationId == project.PromoterId || loggedInEmployee.OrganizationId == tenant.OrganizationId) { organizationIds.Add(project.PMCId); organizationIds.Add(project.PromoterId); organizationIds.Add(tenant.OrganizationId); } // Fetch employees allocated to the project matching the search criteria var employeesQuery = _context.Employees .AsNoTracking() // Improves performance by disabling change tracking for read-only query .Include(e => e.JobRole) .Where(e => (e.FirstName + " " + e.LastName).Contains(searchString) && organizationIds.Contains(e.OrganizationId)); if (organizationId.HasValue) { employeesQuery = employeesQuery.Where(e => e.OrganizationId == organizationId); } var employees = await employeesQuery .ToListAsync(); var result = employees.Select(e => _mapper.Map(e)).Distinct().ToList(); _logger.LogInfo("Employees fetched for project {ProjectId} by user {EmployeeId}. Count: {Count}", projectId, loggedInEmployee.Id, employees.Count); // Return the employee list wrapped in a successful API response return Ok(ApiResponse.SuccessResponse(result, "Employee list fetched successfully", 200)); } catch (Exception ex) { // Log the exception and return a 500 status code with error message _logger.LogError(ex, "Error occurred while fetching employees for project {ProjectId}", projectId); return StatusCode(500, ApiResponse.ErrorResponse("Internal server error", "An unexpected error occurred", 500)); } } [HttpGet("list/{projectId?}")] public async Task GetEmployeesByProjectAsync(Guid? projectId, [FromQuery] bool showInactive = false) { // Step 1: Validate incoming request model state if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); _logger.LogWarning("Invalid model state in GetEmployeesByProject. Errors: {@Errors}", errors); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } List result = new List(); try { // Dependency injection scope for services using var scope = _serviceScopeFactory.CreateScope(); // Step 2: Get logged-in employee details var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); _logger.LogInfo("GetEmployeesByProject called. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}, showInactive: {ShowInactive}", loggedInEmployee.Id, projectId ?? Guid.Empty, showInactive); var employees = await _context.Employees .Include(e => e.JobRole) .Include(e => e.Organization) .Where(e => e.OrganizationId == loggedInEmployee.OrganizationId && e.IsActive != showInactive) .ToListAsync(); // Step 5: Map to view model result = employees.Select(e => _mapper.Map(e)).Distinct().ToList(); _logger.LogInfo("Employees successfully fetched. EmployeeId: {EmployeeId} for ProjectId: {ProjectId}. Final Count: {Count}", loggedInEmployee.Id, projectId ?? Guid.Empty, result.Count); return Ok(ApiResponse.SuccessResponse(result, "Filter applied.", 200)); } catch (Exception ex) { // Step 6: Error logging and response[web:6] _logger.LogError(ex, "Exception occurred while getting the list of employees"); return StatusCode(500, ApiResponse.ErrorResponse("Internal server error. Please try again later.", null, 500)); } } [HttpGet("basic")] public async Task GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var employeeQuery = _context.Employees.Where(e => e.IsActive); if (projectId != null && projectId != Guid.Empty) { var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value); if (!hasProjectPermission) { _logger.LogWarning("User {EmployeeId} attempts to get employee for project {ProjectId}, but not have access to the project", loggedInEmployee.Id, projectId); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User do not have access to view the list for this project", 403)); } var employeeIds = await _context.ProjectAllocations.Where(pa => pa.ProjectId == projectId && pa.IsActive && pa.TenantId == tenantId).Select(p => p.EmployeeId).ToListAsync(); employeeQuery = employeeQuery.Where(e => employeeIds.Contains(e.Id)); } else { employeeQuery = employeeQuery.Where(e => e.OrganizationId == organizationId); } if (!string.IsNullOrWhiteSpace(searchString)) { var searchStringLower = searchString.ToLower(); employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower)); } var response = await employeeQuery.Take(10).Select(e => _mapper.Map(e)).ToListAsync(); return Ok(ApiResponse.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200)); } /// /// Retrieves a paginated list of employees assigned to a specified project (if provided), /// with optional search functionality. /// Ensures that the logged-in user has necessary permissions before accessing project employees. /// /// Optional project identifier to filter employees by project. /// Optional search string to filter employees by name. /// Page number for pagination (default = 1). /// Paginated list of employees in BasicEmployeeVM format wrapped in ApiResponse. [HttpGet("search")] public async Task GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 10) { // Log API entry with context _logger.LogInfo("Fetching employees. ProjectId: {ProjectId}, SearchString: {SearchString}, PageNumber: {PageNumber}", projectId ?? Guid.Empty, searchString ?? "", pageNumber); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); _logger.LogDebug("Logged-in EmployeeId: {EmployeeId}", loggedInEmployee.Id); // Initialize query scoped by tenant var employeeQuery = _context.Employees.Where(e => e.TenantId == tenantId); // Filter by project if projectId is supplied if (projectId.HasValue && projectId.Value != Guid.Empty) { _logger.LogDebug("Project filter applied. Checking permission for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); // Validate project access permission var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value); if (!hasProjectPermission) { _logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have permission for ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); return StatusCode(403, ApiResponse.ErrorResponse( "Access denied", "User does not have access to view employees for this project", 403)); } // Employees allocated to the project var employeeIds = await _context.ProjectAllocations .Where(pa => pa.ProjectId == projectId && pa.IsActive && pa.TenantId == tenantId) .Select(pa => pa.EmployeeId) .ToListAsync(); _logger.LogDebug("Project employees retrieved. Total linked employees found: {Count}", employeeIds.Count); // Apply project allocation filter employeeQuery = employeeQuery.Where(e => employeeIds.Contains(e.Id)); } // Apply search filter if provided if (!string.IsNullOrWhiteSpace(searchString)) { var searchStringLower = searchString.ToLower(); _logger.LogDebug("Search filter applied. Search term: {SearchTerm}", searchStringLower); employeeQuery = employeeQuery.Where(e => (e.FirstName + " " + e.LastName).ToLower().Contains(searchStringLower)); } // Pagination and Projection (executed in DB) var employees = await employeeQuery .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .Select(e => _mapper.Map(e)) .ToListAsync(); _logger.LogInfo("Employees fetched successfully. Records returned: {Count}", employees.Count); return Ok(ApiResponse.SuccessResponse( employees, $"{employees.Count} employee records fetched successfully", 200)); } [HttpGet] [Route("profile/get/{employeeId}")] public async Task GetEmployeeProfileById(Guid employeeId) { if (!ModelState.IsValid) { var errors = ModelState.Values .SelectMany(v => v.Errors) .Select(e => e.ErrorMessage) .ToList(); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } Employee emp = await _employeeHelper.GetEmployeeByID(employeeId); EmployeeVM employeeVM = EmployeeMapper.ToEmployeeVMFromEmployee(emp); return Ok(ApiResponse.SuccessResponse(employeeVM, "Employee Profile.", 200)); } private Guid GetTenantId() { return _userHelper.GetTenantId(); } [HttpPost("old/manage")] public async Task CreateUser([FromBody] CreateUserDto model) { Guid tenantId = _userHelper.GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); Guid employeeId = Guid.Empty; if (model == null) return BadRequest(ApiResponse.ErrorResponse("Invalid data", "Invaild Data", 400)); if (model.FirstName == null && model.PhoneNumber == null) return BadRequest(ApiResponse.ErrorResponse("Invalid data", "Invaild Data", 400)); string responsemessage = ""; if (model.Email != null) { // Check if user already exists by email IdentityUser? existingUser = await _userHelper.GetRegisteredUser(model.Email); var existingEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == model.Id && e.IsActive == true); if (existingUser != null) { /* Identity user Exists - Create/update employee Employee */ // Update Employee record existingEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Email == model.Email && e.Id == model.Id && e.IsActive == true); if (existingEmployee != null) { existingEmployee = GetUpdateEmployeeModel(model, existingEmployee, existingUser); _context.Employees.Update(existingEmployee); await _context.SaveChangesAsync(); employeeId = existingEmployee.Id; responsemessage = "User updated successfully."; } else { // Create Employee record if missing //Employee newEmployee = GetNewEmployeeModel(model, TenantId, existingUser.Id); //_context.Employees.Add(newEmployee); return Conflict(ApiResponse.ErrorResponse("Email already exist", "Email already exist", 409)); } } else { var user = new ApplicationUser { UserName = model.Email, Email = model.Email, EmailConfirmed = true }; var isSeatsAvaiable = await _generalHelper.CheckSeatsRemainingAsync(tenantId); if (!isSeatsAvaiable) { _logger.LogWarning("Maximum number of users reached for Tenant {TenantId}", tenantId); return BadRequest(ApiResponse.ErrorResponse("Maximum number of users reached. Cannot add new user", "Maximum number of users reached. Cannot add new user", 400)); } // Create Identity User var result = await _userManager.CreateAsync(user, "User@123"); if (!result.Succeeded) return BadRequest(ApiResponse.ErrorResponse("Failed to create user", result.Errors, 400)); if (existingEmployee == null) { Employee newEmployee = GetNewEmployeeModel(model, tenantId, user.Id); _context.Employees.Add(newEmployee); await _context.SaveChangesAsync(); employeeId = newEmployee.Id; /* SEND USER REGISTRATION MAIL*/ var token = await _userManager.GeneratePasswordResetTokenAsync(user); var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; if (newEmployee.FirstName != null) { await _emailSender.SendResetPasswordEmailOnRegister(user.Email, newEmployee.FirstName, resetLink); } } else { existingEmployee.Email = model.Email; existingEmployee = GetUpdateEmployeeModel(model, existingEmployee, user); _context.Employees.Update(existingEmployee); await _context.SaveChangesAsync(); employeeId = existingEmployee.Id; /* SEND USER REGISTRATION MAIL*/ var token = await _userManager.GeneratePasswordResetTokenAsync(user); var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; if (existingEmployee.FirstName != null) { await _emailSender.SendResetPasswordEmailOnRegister(user.Email, existingEmployee.FirstName, resetLink); } } responsemessage = "User created successfully. Password reset link is sent to registered email"; } } else { var existingEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == model.Id && e.IsActive == true); if (existingEmployee != null) { existingEmployee = GetUpdateEmployeeModel(model, existingEmployee); _context.Employees.Update(existingEmployee); responsemessage = "User updated successfully."; employeeId = existingEmployee.Id; } else { // Create Employee record if missing Employee newEmployee = GetNewEmployeeModel(model, tenantId, string.Empty); _context.Employees.Add(newEmployee); employeeId = newEmployee.Id; } await _context.SaveChangesAsync(); responsemessage = "User created successfully."; } var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Employee", EmployeeId = employeeId }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); return Ok(ApiResponse.SuccessResponse("Success.", responsemessage, 200)); } [HttpPost("manage")] public async Task CreateEmployeeAsync([FromBody] CreateUserDto model) { // Correlation and context capture for logs var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); Guid organizationId = model.OrganizationId ?? loggedInEmployee.OrganizationId; { if (model == null) { _logger.LogWarning("Model is null in CreateEmployeeAsync"); return BadRequest(ApiResponse.ErrorResponse("Invalid payload", "Request body is required", 400)); } // Basic validation if (model.HasApplicationAccess && string.IsNullOrWhiteSpace(model.Email)) { _logger.LogWarning("Application access requested but email is missing"); return BadRequest(ApiResponse.ErrorResponse("Invalid email", "Application users must have a valid email", 400)); } await using var transaction = await _context.Database.BeginTransactionAsync(); try { // Load existing employee if updating, constrained by organization scope Employee? existingEmployee = null; if (model.Id.HasValue && model.Id.Value != Guid.Empty) { existingEmployee = await _context.Employees .FirstOrDefaultAsync(e => e.Id == model.Id && e.OrganizationId == organizationId); if (existingEmployee == null) { _logger.LogInfo("Employee not found for update. Id={EmployeeId}, Org={OrgId}", model.Id, organizationId); return NotFound(ApiResponse.ErrorResponse("Employee not found", "Employee not found in database", 404)); } } // Identity user creation path (only if needed) ApplicationUser? identityUserToCreate = null; ApplicationUser? createdIdentityUser = null; if (model.HasApplicationAccess) { // Only attempt identity resolution/creation if email supplied and either: // - Creating new employee, or // - Updating but existing employee does not have ApplicationUserId var needsIdentity = string.IsNullOrWhiteSpace(existingEmployee?.ApplicationUserId); if (needsIdentity && !string.IsNullOrWhiteSpace(model.Email)) { var existingUser = await _userManager.FindByEmailAsync(model.Email); if (existingUser == null) { // Seat check only when provisioning a new identity user var isSeatsAvailable = await _generalHelper.CheckSeatsRemainingAsync(tenantId); if (!isSeatsAvailable) { _logger.LogWarning("Maximum users reached for Tenant {TenantId}. Cannot create identity user for {Email}", tenantId, model.Email); return BadRequest(ApiResponse.ErrorResponse( "Maximum number of users reached. Cannot add new user", "Maximum number of users reached. Cannot add new user", 400)); } identityUserToCreate = new ApplicationUser { UserName = model.Email, Email = model.Email, EmailConfirmed = true }; } else { // If identity exists, re-use it; do not re-create createdIdentityUser = existingUser; } } } // For create path: enforce uniqueness of employee email if applicable to business rules // Consider adding a unique filtered index: (OrganizationId, Email) WHERE Email IS NOT NULL if (!model.Id.HasValue || model.Id == Guid.Empty) { if (!string.IsNullOrWhiteSpace(model.Email)) { var emailExists = await _context.Employees .AnyAsync(e => e.Email == model.Email && e.OrganizationId == organizationId); if (emailExists) { _logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, organizationId); return StatusCode(403, ApiResponse.ErrorResponse( "Employee with email already exists", "Employee with this email already exists", 403)); } } } // Create identity user if needed if (identityUserToCreate != null && !string.IsNullOrWhiteSpace(identityUserToCreate.Email)) { var createResult = await _userManager.CreateAsync(identityUserToCreate, "User@123"); if (!createResult.Succeeded) { _logger.LogWarning("Failed to create identity user for {Email}. Errors={Errors}", identityUserToCreate.Email, string.Join(", ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}"))); return BadRequest(ApiResponse.ErrorResponse("Failed to create user", createResult.Errors, 400)); } createdIdentityUser = identityUserToCreate; _logger.LogInfo("Identity user created. IdentityUserId={UserId}, Email={Email}", createdIdentityUser.Id, createdIdentityUser.Email); } Guid employeeId; EmployeeVM employeeVM; string responseMessage; if (existingEmployee != null) { // Update flow _mapper.Map(model, existingEmployee); if (createdIdentityUser != null && !string.IsNullOrWhiteSpace(createdIdentityUser.Email)) { existingEmployee.ApplicationUserId = createdIdentityUser.Id; await SendResetIfApplicableAsync(createdIdentityUser, existingEmployee.FirstName ?? "User"); } existingEmployee.OrganizationId = organizationId; await _context.SaveChangesAsync(); employeeId = existingEmployee.Id; employeeVM = _mapper.Map(existingEmployee); responseMessage = "Employee Updated Successfully"; _logger.LogInfo("Employee updated. EmployeeId={EmployeeId}, Org={OrgId}", employeeId, existingEmployee.OrganizationId); } else { // Create flow var newEmployee = _mapper.Map(model); newEmployee.IsSystem = false; newEmployee.IsActive = true; newEmployee.IsPrimary = false; if (createdIdentityUser != null && !string.IsNullOrWhiteSpace(createdIdentityUser.Email)) { newEmployee.ApplicationUserId = createdIdentityUser.Id; await SendResetIfApplicableAsync(createdIdentityUser, newEmployee.FirstName ?? "User"); } newEmployee.OrganizationId = organizationId; await _context.Employees.AddAsync(newEmployee); await _context.SaveChangesAsync(); employeeId = newEmployee.Id; employeeVM = _mapper.Map(newEmployee); responseMessage = "Employee Created Successfully"; _logger.LogInfo("Employee created. EmployeeId={EmployeeId}, Org={OrgId}", employeeId, newEmployee.OrganizationId); } await transaction.CommitAsync(); // SignalR notification var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Employee", EmployeeId = employeeId }; // Consider broadcasting to tenant/organization group instead of Clients.All to avoid cross-tenant leaks: // await _signalR.Clients.Group($"org:{model.OrganizationId}").SendAsync("NotificationEventHandler", notification); await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); _logger.LogInfo("Notification broadcasted for EmployeeId={EmployeeId}", employeeId); return Ok(ApiResponse.SuccessResponse(employeeVM, responseMessage, 200)); } catch (DbUpdateException dbEx) { await transaction.RollbackAsync(); _logger.LogError(dbEx, "Database exception occurred while managing employee"); return StatusCode(500, ApiResponse.ErrorResponse( "Internal exception occurred", "Internal database exception has occurred", 500)); } catch (Exception ex) { await transaction.RollbackAsync(); _logger.LogError(ex, "Unhandled exception occurred while managing employee"); return StatusCode(500, ApiResponse.ErrorResponse( "Internal exception occurred", "Internal exception has occurred", 500)); } } } [HttpPost("manage-mobile")] public async Task CreateUserMoblie([FromBody] MobileUserManageDto model) { Guid tenantId = _userHelper.GetTenantId(); if (model == null) { _logger.LogWarning("User submitted empty or null employee information during employee creation or update in tenant {TenantId}", tenantId); return BadRequest(ApiResponse.ErrorResponse("Invalid data", "Invaild Data", 400)); } if (string.IsNullOrWhiteSpace(model.FirstName) || string.IsNullOrWhiteSpace(model.PhoneNumber)) { _logger.LogWarning("User submitted empty or null first name or phone number during employee creation or update in tenant {TenantId}", tenantId); return BadRequest(ApiResponse.ErrorResponse("First name and phone number are required fields.", "First name and phone number are required fields.", 400)); } if (model.Id == null) { byte[]? imageBytes = null; if (!string.IsNullOrWhiteSpace(model.ProfileImage)) { try { imageBytes = Convert.FromBase64String(model.ProfileImage); } catch (FormatException) { return BadRequest(ApiResponse.ErrorResponse("Invalid image format.", "Invalid image format", 400)); } } Employee employee = model.ToEmployeeFromMobileUserManageDto(tenantId, imageBytes); _context.Employees.Add(employee); await _context.SaveChangesAsync(); EmployeeVM employeeVM = employee.ToEmployeeVMFromEmployee(); _logger.LogInfo($"Employee {employee.FirstName} {employee.LastName} created in tenant {tenantId}"); return Ok(ApiResponse.SuccessResponse(employeeVM, "Employee created successfully", 200)); } else { Employee? existingEmployee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == model.Id.Value); if (existingEmployee == null) { _logger.LogWarning("User tries to update employee {EmployeeId} but not found in database", model.Id); return NotFound(ApiResponse.ErrorResponse("Employee not found", "Employee not found", 404)); } byte[]? imageBytes = null; if (!string.IsNullOrWhiteSpace(model.ProfileImage)) { try { imageBytes = Convert.FromBase64String(model.ProfileImage); } catch (FormatException) { return BadRequest(ApiResponse.ErrorResponse("Invalid image format.", "Invalid image format", 400)); } } imageBytes ??= existingEmployee.Photo; existingEmployee.FirstName = model.FirstName; existingEmployee.LastName = model.LastName; existingEmployee.Gender = model.Gender; existingEmployee.PhoneNumber = model.PhoneNumber; existingEmployee.JoiningDate = model.JoiningDate; existingEmployee.JobRoleId = model.JobRoleId; existingEmployee.Photo = imageBytes; await _context.SaveChangesAsync(); EmployeeVM employeeVM = existingEmployee.ToEmployeeVMFromEmployee(); _logger.LogInfo($"Employee {existingEmployee.FirstName} {existingEmployee.LastName} updated in tenant {tenantId}"); return Ok(ApiResponse.SuccessResponse(employeeVM, "Employee updated successfully", 200)); } } [HttpPost("app/manage")] public async Task CreateUserMobileAsync([FromBody] MobileUserManageDto model) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); Guid organizationId = model.OrganizationId ?? loggedInEmployee.OrganizationId; if (tenantId == Guid.Empty) { _logger.LogWarning("Tenant resolution failed in CreateUserMobile"); // structured warning return StatusCode(403, ApiResponse.ErrorResponse("Unauthorized tenant context", "Unauthorized", 403)); } if (model is null) { _logger.LogWarning("Null payload in CreateUserMobile for Tenant {TenantId}", tenantId); // validation log return BadRequest(ApiResponse.ErrorResponse("Invalid data", "Invalid data", 400)); } if (string.IsNullOrWhiteSpace(model.FirstName) || string.IsNullOrWhiteSpace(model.PhoneNumber)) { _logger.LogWarning("Missing required fields FirstName/Phone for Tenant {TenantId}", tenantId); // validation log return BadRequest(ApiResponse.ErrorResponse("First name and phone number are required.", "Required fields missing", 400)); } // Strict Base64 parse byte[]? imageBytes = null; if (!string.IsNullOrWhiteSpace(model.ProfileImage)) { try { imageBytes = Convert.FromBase64String(model.ProfileImage); } catch (FormatException ex) { _logger.LogError(ex, "Invalid base64 image in CreateUserMobile for Tenant {TenantId}", tenantId); // input issue return BadRequest(ApiResponse.ErrorResponse("Invalid image format.", "Invalid image", 400)); } } if (model.Id == null || model.Id == Guid.Empty) { var emailExists = await _context.Employees .AnyAsync(e => e.Email == model.Email && e.OrganizationId == organizationId); if (emailExists) { _logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email ?? string.Empty, organizationId); return StatusCode(409, ApiResponse.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409)); } // Create path: map only allowed fields var employee = new Employee { Id = Guid.NewGuid(), TenantId = tenantId, FirstName = model.FirstName.Trim(), LastName = model.LastName?.Trim(), Email = model.Email, Gender = model.Gender, PhoneNumber = model.PhoneNumber, JoiningDate = model.JoiningDate, JobRoleId = model.JobRoleId, Photo = imageBytes, OrganizationId = organizationId, HasApplicationAccess = model.HasApplicationAccess, }; if (!string.IsNullOrWhiteSpace(model.Email) && model.HasApplicationAccess) { var existingUser = await _userManager.FindByEmailAsync(model.Email); if (existingUser == null) { existingUser = new ApplicationUser { UserName = model.Email, Email = model.Email, EmailConfirmed = true }; var createResult = await _userManager.CreateAsync(existingUser, "User@123"); if (!createResult.Succeeded) { _logger.LogWarning("Failed to create identity user for {Email}. Errors={Errors}", existingUser.Email, string.Join(", ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}"))); return BadRequest(ApiResponse.ErrorResponse("Failed to create user", createResult.Errors, 400)); } await SendResetIfApplicableAsync(existingUser, employee.FirstName ?? "User"); employee.ApplicationUserId = existingUser.Id; } } _context.Employees.Add(employee); await _context.SaveChangesAsync(); var employeeVM = _mapper.Map(employee); var notification = new { LoggedInUserId = loggedInEmployee?.Id, Keyword = "Employee", EmployeeId = employee.Id }; // Consider broadcasting to tenant/organization group instead of Clients.All to avoid cross-tenant leaks: // await _signalR.Clients.Group($"org:{model.OrganizationId}").SendAsync("NotificationEventHandler", notification); await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); _logger.LogInfo("Employee {EmployeeId} created in Tenant {TenantId}", employee.Id, tenantId); // success return Ok(ApiResponse.SuccessResponse(employeeVM, "Employee created successfully", 200)); } else { // Update path: fetch scoped to tenant var employeeId = model.Id.Value; var existingEmployee = await _context.Employees .FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == tenantId); // tenant-safe lookup if (existingEmployee is null) { _logger.LogWarning("Update attempted for missing Employee {EmployeeId} in Tenant {TenantId}", employeeId, tenantId); // not found return NotFound(ApiResponse.ErrorResponse("Employee not found", "Not found", 404)); } // Update allowed fields only existingEmployee.FirstName = model.FirstName.Trim(); existingEmployee.LastName = model.LastName?.Trim(); existingEmployee.Gender = model.Gender; existingEmployee.PhoneNumber = model.PhoneNumber; existingEmployee.JoiningDate = model.JoiningDate; existingEmployee.JobRoleId = model.JobRoleId; existingEmployee.OrganizationId = organizationId; existingEmployee.HasApplicationAccess = model.HasApplicationAccess; if (string.IsNullOrWhiteSpace(existingEmployee.Email) && !string.IsNullOrWhiteSpace(model.Email)) { var emailExists = await _context.Employees .AnyAsync(e => e.Email == model.Email && e.OrganizationId == model.OrganizationId); if (emailExists) { _logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, organizationId); return StatusCode(409, ApiResponse.ErrorResponse("Employee with email already exists", "Employee with this email already exists", 409)); } existingEmployee.Email = model.Email; } if (model.HasApplicationAccess && !string.IsNullOrWhiteSpace(model.Email) && string.IsNullOrWhiteSpace(existingEmployee.ApplicationUserId)) { var existingUser = await _userManager.FindByEmailAsync(model.Email); if (existingUser == null) { existingUser = new ApplicationUser { UserName = model.Email, Email = model.Email, EmailConfirmed = true }; var createResult = await _userManager.CreateAsync(existingUser, "User@123"); if (!createResult.Succeeded) { _logger.LogWarning("Failed to create identity user for {Email}. Errors={Errors}", existingUser.Email, string.Join(", ", createResult.Errors.Select(e => $"{e.Code}:{e.Description}"))); return BadRequest(ApiResponse.ErrorResponse("Failed to create user", createResult.Errors, 400)); } await SendResetIfApplicableAsync(existingUser, existingEmployee.FirstName ?? "User"); existingEmployee.ApplicationUserId = existingUser.Id; } } if (imageBytes != null) { existingEmployee.Photo = imageBytes; } await _context.SaveChangesAsync(); var employeeVM = _mapper.Map(existingEmployee); var notification = new { LoggedInUserId = loggedInEmployee?.Id, Keyword = "Employee", EmployeeId = employeeId }; // Consider broadcasting to tenant/organization group instead of Clients.All to avoid cross-tenant leaks: // await _signalR.Clients.Group($"org:{model.OrganizationId}").SendAsync("NotificationEventHandler", notification); await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); _logger.LogInfo("Employee {EmployeeId} updated in Tenant {TenantId}", existingEmployee.Id, tenantId); // success return Ok(ApiResponse.SuccessResponse(employeeVM, "Employee updated successfully", 200)); } } [HttpDelete("{id}")] public async Task SuspendEmployee(Guid id, [FromQuery] bool active = false) { using var scope = _serviceScopeFactory.CreateScope(); Guid tenantId = _userHelper.GetTenantId(); var LoggedEmployee = await _userHelper.GetCurrentEmployeeAsync(); Employee? employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == id && e.TenantId == tenantId); if (employee == null) { _logger.LogWarning("Employee with ID {EmploueeId} not found in database", id); return NotFound(ApiResponse.ErrorResponse("Employee Not found successfully", "Employee Not found successfully", 404)); } if (employee.IsSystem) { _logger.LogWarning("Employee with ID {LoggedEmployeeId} tries to suspend system-defined employee with ID {EmployeeId}", LoggedEmployee.Id, employee.Id); return BadRequest(ApiResponse.ErrorResponse("System-defined employees cannot be suspended.", "System-defined employees cannot be suspended.", 400)); } var assignedToTasks = await _context.TaskMembers.Where(t => t.EmployeeId == employee.Id).ToListAsync(); if (assignedToTasks.Count != 0) { List taskIds = assignedToTasks.Select(t => t.TaskAllocationId).ToList(); var tasks = await _context.TaskAllocations.Where(t => taskIds.Contains(t.Id)).ToListAsync(); foreach (var assignedToTask in assignedToTasks) { var task = tasks.Find(t => t.Id == assignedToTask.TaskAllocationId); if (task != null && task.CompletedTask == 0) { _logger.LogWarning("Employee with ID {EmployeeId} is currently assigned to any incomplete task", employee.Id); return BadRequest(ApiResponse.ErrorResponse("Employee is currently assigned to any incomplete task", "Employee is currently assigned to any incomplete task", 400)); } } } var attendance = await _context.Attendes.Where(a => a.EmployeeId == employee.Id && (a.OutTime == null || a.Activity == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)).ToListAsync(); if (attendance.Count != 0) { _logger.LogWarning("Employee with ID {EmployeeId} have any pending check-out or regularization requests", employee.Id); return BadRequest(ApiResponse.ErrorResponse("Employee have any pending check-out or regularization requests", "Employee have any pending check-out or regularization requests", 400)); } if (active) { employee.IsActive = true; var user = await _context.ApplicationUsers.FirstOrDefaultAsync(u => u.Id == employee.ApplicationUserId); if (user != null) { user.IsActive = true; _logger.LogInfo("The application user associated with employee ID {EmployeeId} has been actived.", employee.Id); } _logger.LogInfo("Employee with ID {EmployeId} Actived successfully", employee.Id); } else { employee.IsActive = false; var projectAllocations = await _context.ProjectAllocations.Where(a => a.EmployeeId == employee.Id).ToListAsync(); if (projectAllocations.Count != 0) { List allocations = new List(); foreach (var projectAllocation in projectAllocations) { projectAllocation.ReAllocationDate = DateTime.UtcNow; projectAllocation.IsActive = false; allocations.Add(projectAllocation); } _logger.LogInfo("Employee with ID {EmployeeId} has been removed from all assigned projects.", employee.Id); } var user = await _context.ApplicationUsers.FirstOrDefaultAsync(u => u.Id == employee.ApplicationUserId); if (user != null) { user.IsActive = false; _logger.LogInfo("The application user associated with employee ID {EmployeeId} has been suspended.", employee.Id); var refreshTokens = await _context.RefreshTokens.AsNoTracking().Where(t => t.UserId == user.Id).ToListAsync(); if (refreshTokens.Count != 0) { _context.RefreshTokens.RemoveRange(refreshTokens); _logger.LogInfo("Refresh tokens associated with employee ID {EmployeeId} has been removed.", employee.Id); } } var roleMapping = await _context.EmployeeRoleMappings.AsNoTracking().Where(r => r.EmployeeId == employee.Id).ToListAsync(); if (roleMapping.Count != 0) { _context.EmployeeRoleMappings.RemoveRange(roleMapping); _logger.LogInfo("Application role mapping associated with employee ID {EmployeeId} has been removed.", employee.Id); } _logger.LogInfo("Employee with ID {EmployeId} Deleted successfully", employee.Id); var _firebase = scope.ServiceProvider.GetRequiredService(); _ = Task.Run(async () => { // --- Push Notification Section --- // This section attempts to send a test push notification to the user's device. // It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens. await _firebase.SendEmployeeSuspendMessageAsync(employee.Id, tenantId); }); } try { await _context.SaveChangesAsync(); } catch (DbUpdateException ex) { _logger.LogError(ex, "Exception Occured While activting/deactivting employee {EmployeeId}", employee.Id); return StatusCode(500, ApiResponse.ErrorResponse("Internal Error Occured", "Error occured while saving the entity", 500)); } var notification = new { LoggedInUserId = LoggedEmployee.Id, Keyword = "Employee", EmployeeId = employee.Id }; await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); return Ok(ApiResponse.SuccessResponse(new { }, "Employee Suspended successfully", 200)); } private static Employee GetNewEmployeeModel(CreateUserDto model, Guid TenantId, string ApplicationUserId) { var newEmployee = new Employee { ApplicationUserId = String.IsNullOrEmpty(ApplicationUserId) ? null : ApplicationUserId, FirstName = model.FirstName, LastName = model.LastName, Email = model.Email, TenantId = TenantId, CurrentAddress = model.CurrentAddress, BirthDate = Convert.ToDateTime(model.BirthDate), EmergencyPhoneNumber = model.EmergencyPhoneNumber, EmergencyContactPerson = model.EmergencyContactPerson, Gender = model.Gender, MiddleName = model.MiddleName, PermanentAddress = model.PermanentAddress, PhoneNumber = model.PhoneNumber, Photo = null, // GetFileDetails(model.Photo).Result.FileData, JobRoleId = model.JobRoleId, JoiningDate = Convert.ToDateTime(model.JoiningDate), }; return newEmployee; } private static Employee GetUpdateEmployeeModel(CreateUserDto model, Employee existingEmployee, IdentityUser? existingIdentityUser = null) { if (existingEmployee.ApplicationUserId == null && existingIdentityUser != null) { existingEmployee.ApplicationUserId = existingIdentityUser.Id; } existingEmployee.FirstName = model.FirstName; existingEmployee.LastName = model.LastName; existingEmployee.CurrentAddress = model.CurrentAddress; existingEmployee.BirthDate = Convert.ToDateTime(model.BirthDate); existingEmployee.JoiningDate = Convert.ToDateTime(model.JoiningDate); existingEmployee.EmergencyPhoneNumber = model.EmergencyPhoneNumber; existingEmployee.EmergencyContactPerson = model.EmergencyContactPerson; existingEmployee.Gender = model.Gender; existingEmployee.MiddleName = model.MiddleName; existingEmployee.PermanentAddress = model.PermanentAddress; existingEmployee.PhoneNumber = model.PhoneNumber; existingEmployee.Photo = existingEmployee.Photo; // GetFileDetails(model.Photo).Result.FileData, existingEmployee.JobRoleId = model.JobRoleId; return existingEmployee; } private static async Task GetFileDetails(IFormFile file) { FileDetails info = new FileDetails(); info.ContentType = file.ContentType; info.FileName = file.FileName; using (var memoryStream = new MemoryStream()) { await file.CopyToAsync(memoryStream); info.FileData = memoryStream.ToArray(); } return info; } // Prepare reset link sender helper private async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName) { if (!string.IsNullOrWhiteSpace(u.Email)) { var token = await _userManager.GeneratePasswordResetTokenAsync(u); var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; await _emailSender.SendResetPasswordEmailOnRegister(u.Email, firstName, resetLink); _logger.LogInfo("Reset password email queued. Email={Email}", u.Email); } } } }