Enhanced the manage employe API
This commit is contained in:
parent
a405cfec13
commit
c5da400e6b
@ -9,8 +9,8 @@
|
||||
public string? Email { get; set; }
|
||||
|
||||
public required string Gender { get; set; }
|
||||
public required string BirthDate { get; set; }
|
||||
public required string JoiningDate { get; set; }
|
||||
public required DateTime BirthDate { get; set; }
|
||||
public required DateTime JoiningDate { get; set; }
|
||||
|
||||
public required string PermanentAddress { get; set; }
|
||||
public required string CurrentAddress { get; set; }
|
||||
@ -19,6 +19,8 @@
|
||||
public string? EmergencyPhoneNumber { get; set; }
|
||||
public string? EmergencyContactPerson { get; set; }
|
||||
public Guid JobRoleId { get; set; }
|
||||
public required Guid OrganizationId { get; set; }
|
||||
public required bool HasApplicationAccess { get; set; }
|
||||
}
|
||||
public class MobileUserManageDto
|
||||
{
|
||||
@ -30,6 +32,7 @@
|
||||
public required string Gender { get; set; }
|
||||
public Guid JobRoleId { get; set; }
|
||||
public string? ProfileImage { get; set; }
|
||||
public required Guid OrganizationId { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IProjectServices _projectServices;
|
||||
private readonly Guid tenantId;
|
||||
private readonly Guid organizationId;
|
||||
|
||||
|
||||
public EmployeeController(IServiceScopeFactory serviceScope,
|
||||
@ -76,6 +77,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
_projectServices = projectServices;
|
||||
_mapper = mapper;
|
||||
tenantId = _userHelper.GetTenantId();
|
||||
organizationId = _userHelper.GetCurrentOrganizationId();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -315,7 +317,7 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("manage")]
|
||||
[HttpPost("old/manage")]
|
||||
public async Task<IActionResult> CreateUser([FromBody] CreateUserDto model)
|
||||
{
|
||||
Guid tenantId = _userHelper.GetTenantId();
|
||||
@ -448,6 +450,211 @@ namespace MarcoBMS.Services.Controllers
|
||||
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();
|
||||
|
||||
{
|
||||
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 == model.OrganizationId);
|
||||
if (existingEmployee == null)
|
||||
{
|
||||
_logger.LogInfo("Employee not found for update. Id={EmployeeId}, Org={OrgId}", model.Id, model.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 && e.OrganizationId == model.OrganizationId);
|
||||
if (emailExists)
|
||||
{
|
||||
_logger.LogInfo("Employee email already exists in org. Email={Email}, Org={OrgId}", model.Email, model.OrganizationId);
|
||||
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);
|
||||
}
|
||||
|
||||
// Prepare reset link sender helper
|
||||
async Task SendResetIfApplicableAsync(ApplicationUser u, string firstName)
|
||||
{
|
||||
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 ?? "");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@ -527,6 +734,126 @@ namespace MarcoBMS.Services.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("app/manage")]
|
||||
public async Task<IActionResult> CreateUserMobileAsync([FromBody] MobileUserManageDto model)
|
||||
{
|
||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
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)
|
||||
{
|
||||
// Create path: map only allowed fields
|
||||
var employee = new Employee
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
FirstName = model.FirstName.Trim(),
|
||||
LastName = model.LastName?.Trim(),
|
||||
Gender = model.Gender,
|
||||
PhoneNumber = model.PhoneNumber,
|
||||
JoiningDate = model.JoiningDate,
|
||||
JobRoleId = model.JobRoleId,
|
||||
Photo = imageBytes,
|
||||
OrganizationId = model.OrganizationId
|
||||
};
|
||||
|
||||
await _context.Employees.AddAsync(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 = model.OrganizationId;
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -26,6 +26,11 @@ namespace MarcoBMS.Services.Helpers
|
||||
var tenant = _httpContextAccessor.HttpContext?.User.FindFirst("TenantId")?.Value;
|
||||
return (tenant != null ? Guid.Parse(tenant) : Guid.Empty);
|
||||
}
|
||||
public Guid GetCurrentOrganizationId()
|
||||
{
|
||||
var tenant = _httpContextAccessor.HttpContext?.User.FindFirst("OrganizationId")?.Value;
|
||||
return (tenant != null ? Guid.Parse(tenant) : Guid.Empty);
|
||||
}
|
||||
public async Task<Tenant?> GetCurrentTenant()
|
||||
{
|
||||
var tenantId = _httpContextAccessor.HttpContext?.User.FindFirst("TenantId")?.Value;
|
||||
|
@ -6,6 +6,7 @@ using Marco.Pms.Model.Dtos.Activities;
|
||||
using Marco.Pms.Model.Dtos.AppMenu;
|
||||
using Marco.Pms.Model.Dtos.Directory;
|
||||
using Marco.Pms.Model.Dtos.DocumentManager;
|
||||
using Marco.Pms.Model.Dtos.Employees;
|
||||
using Marco.Pms.Model.Dtos.Expenses;
|
||||
using Marco.Pms.Model.Dtos.Master;
|
||||
using Marco.Pms.Model.Dtos.Organization;
|
||||
@ -177,6 +178,9 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
#region ======================================================= Employee =======================================================
|
||||
|
||||
CreateMap<Employee, EmployeeVM>();
|
||||
CreateMap<CreateUserDto, Employee>();
|
||||
CreateMap<MobileUserManageDto, Employee>();
|
||||
|
||||
CreateMap<Employee, BasicEmployeeVM>()
|
||||
.ForMember(
|
||||
dest => dest.JobRoleName,
|
||||
|
Loading…
x
Reference in New Issue
Block a user