1198 lines
60 KiB
C#

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<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly UserManager<ApplicationUser> _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<MarcoHub> _signalR;
private readonly PermissionServices _permission;
private readonly IMapper _mapper;
private readonly IProjectServices _projectServices;
private readonly Guid tenantId;
private readonly Guid organizationId;
public EmployeeController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
IServiceScopeFactory serviceScopeFactory,
UserManager<ApplicationUser> userManager,
IEmailSender emailSender,
ApplicationDbContext context,
EmployeeHelper employeeHelper,
UserHelper userHelper,
IConfiguration configuration,
ILoggingService logger,
IHubContext<MarcoHub> 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<IActionResult> GetRoles(Guid employeeId)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
return BadRequest(ApiResponse<object>.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<EmployeeRolesVM> roles = new List<EmployeeRolesVM>();
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<object>.SuccessResponse(roles, "Success.", 200));
}
else
{
return BadRequest(ApiResponse<object>.ErrorResponse("This employee has no assigned permissions.", "This employee has no assigned permissions.", 400));
}
}
[HttpGet("list/organizations/{projectId}")]
public async Task<IActionResult> 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<object>.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<object>.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<EmployeeVM>(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<object>.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<object>.ErrorResponse("Internal server error", "An unexpected error occurred", 500));
}
}
[HttpGet("list/{projectId?}")]
public async Task<IActionResult> 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<object>.ErrorResponse("Invalid data", errors, 400));
}
List<EmployeeVM> result = new List<EmployeeVM>();
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<EmployeeVM>(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<object>.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<object>.ErrorResponse("Internal server error. Please try again later.", null, 500));
}
}
[HttpGet("basic")]
public async Task<IActionResult> GetEmployeesByProjectBasic(Guid? projectId, [FromQuery] string? searchString, [FromQuery] bool sendAll = false)
{
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<object>.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));
}
if (!sendAll)
{
employeeQuery = employeeQuery.Take(10);
}
var response = await employeeQuery.Select(e => _mapper.Map<BasicEmployeeVM>(e)).ToListAsync();
return Ok(ApiResponse<object>.SuccessResponse(response, $"{response.Count} records of employees fetched successfully", 200));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="projectId">Optional project identifier to filter employees by project.</param>
/// <param name="searchString">Optional search string to filter employees by name.</param>
/// <param name="pageNumber">Page number for pagination (default = 1).</param>
/// <returns>Paginated list of employees in BasicEmployeeVM format wrapped in ApiResponse.</returns>
[HttpGet("search")]
public async Task<IActionResult> 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<object>.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<BasicEmployeeVM>(e))
.ToListAsync();
_logger.LogInfo("Employees fetched successfully. Records returned: {Count}", employees.Count);
return Ok(ApiResponse<object>.SuccessResponse(
employees,
$"{employees.Count} employee records fetched successfully",
200));
}
[HttpGet]
[Route("profile/get/{employeeId}")]
public async Task<IActionResult> GetEmployeeProfileById(Guid employeeId)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
Employee emp = await _employeeHelper.GetEmployeeByID(employeeId);
EmployeeVM employeeVM = EmployeeMapper.ToEmployeeVMFromEmployee(emp);
return Ok(ApiResponse<object>.SuccessResponse(employeeVM, "Employee Profile.", 200));
}
private Guid GetTenantId()
{
return _userHelper.GetTenantId();
}
[HttpPost("old/manage")]
public async Task<IActionResult> CreateUser([FromBody] CreateUserDto model)
{
Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
Guid employeeId = Guid.Empty;
if (model == null)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invaild Data", 400));
if (model.FirstName == null && model.PhoneNumber == null)
return BadRequest(ApiResponse<object>.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<object>.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<object>.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<object>.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<object>.SuccessResponse("Success.", responsemessage, 200));
}
[HttpPost("manage")]
public async Task<IActionResult> 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<object>.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<object>.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<object>.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<object>.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);
if (emailExists)
{
_logger.LogInfo("Employee email already exists. Email={Email}", model.Email);
return StatusCode(403, ApiResponse<object>.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<object>.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<EmployeeVM>(existingEmployee);
responseMessage = "Employee Updated Successfully";
_logger.LogInfo("Employee updated. EmployeeId={EmployeeId}, Org={OrgId}", employeeId, existingEmployee.OrganizationId);
}
else
{
// Create flow
var newEmployee = _mapper.Map<Employee>(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<EmployeeVM>(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<object>.SuccessResponse(employeeVM, responseMessage, 200));
}
catch (DbUpdateException dbEx)
{
await transaction.RollbackAsync();
_logger.LogError(dbEx, "Database exception occurred while managing employee");
return StatusCode(500, ApiResponse<object>.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<object>.ErrorResponse(
"Internal exception occurred",
"Internal exception has occurred", 500));
}
}
}
[HttpPost("manage-mobile")]
public async Task<IActionResult> 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<object>.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<object>.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<object>.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<object>.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<object>.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<object>.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<object>.SuccessResponse(employeeVM, "Employee updated successfully", 200));
}
}
[HttpPost("app/manage")]
public async Task<IActionResult> 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<object>.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<object>.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<object>.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<object>.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);
if (emailExists)
{
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email ?? string.Empty, organizationId);
return StatusCode(409, ApiResponse<object>.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<object>.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<EmployeeVM>(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<object>.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<object>.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<object>.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<object>.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<EmployeeVM>(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<object>.SuccessResponse(employeeVM, "Employee updated successfully", 200));
}
}
[HttpDelete("{id}")]
public async Task<IActionResult> 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.OrganizationId == organizationId);
if (employee == null)
{
_logger.LogWarning("Employee with ID {EmploueeId} not found in database", id);
return NotFound(ApiResponse<object>.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<object>.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<Guid> 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<object>.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<object>.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<ProjectAllocation> allocations = new List<ProjectAllocation>();
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<IFirebaseService>();
_ = 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<object>.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<object>.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<FileDetails> 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);
}
}
}
}