Optimized the manage organization hierarchy API
This commit is contained in:
parent
b023883233
commit
128417858e
@ -780,79 +780,168 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception has occurred", 500);
|
return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception has occurred", 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atomically manage the organization hierarchy for an employee: add, deactivate, and audit changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="employeeId">Employee GUID to manage hierarchies for.</param>
|
||||||
|
/// <param name="model">List of hierarchy changes (DTOs).</param>
|
||||||
|
/// <param name="loggedInEmployee">Current user performing the operation.</param>
|
||||||
|
/// <param name="tenantId">Tenant context for multi-tenancy support.</param>
|
||||||
|
/// <param name="loggedOrganizationId">Current logged-in organization context.</param>
|
||||||
|
/// <returns>Standardized ApiResponse with updated hierarchy or error details.</returns>
|
||||||
public async Task<ApiResponse<object>> ManageOrganizationHierarchyAsync(Guid employeeId, List<OrganizationHierarchyDto> model, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
|
public async Task<ApiResponse<object>> ManageOrganizationHierarchyAsync(Guid employeeId, List<OrganizationHierarchyDto> model, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
|
||||||
{
|
{
|
||||||
var reportToIDs = model.Select(oh => oh.ReportToId).ToList();
|
// Validate required parameters early to avoid wasted DB calls
|
||||||
var existingOrganizationHieraechies = await _context.OrganizationHierarchies
|
if (tenantId == Guid.Empty || loggedOrganizationId == Guid.Empty)
|
||||||
.AsNoTracking()
|
|
||||||
.Where(oh => oh.EmployeeId == employeeId && reportToIDs.Contains(oh.ReportToId) && oh.IsActive && oh.TenantId == tenantId)
|
|
||||||
.ToListAsync();
|
|
||||||
var newOrganizationHieraechies = new List<OrganizationHierarchy>();
|
|
||||||
var removeOrganizationHieraechies = new List<OrganizationHierarchy>();
|
|
||||||
var orgHierarchyLogs = new List<OrgHierarchyLog>();
|
|
||||||
|
|
||||||
foreach (var hierarchy in model)
|
|
||||||
{
|
{
|
||||||
var existingOrganizationHieraechy = existingOrganizationHieraechies.FirstOrDefault(oh => oh.EmployeeId == employeeId && oh.ReportToId == hierarchy.ReportToId);
|
_logger.LogWarning("Unauthorized attempt: Invalid tenant or organization IDs. TenantId: {TenantId}, OrgId: {OrgId}", tenantId, loggedOrganizationId);
|
||||||
if (hierarchy.IsActive && existingOrganizationHieraechy == null)
|
|
||||||
|
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant or organization context.", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model == null || model.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogInfo("No data provided for employee {EmployeeId} hierarchy update.", employeeId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("No hierarchy data provided.", "No hierarchy data provided.", 400);
|
||||||
|
}
|
||||||
|
var primaryHierarchies = model.Where(oh => oh.IsPrimary && oh.IsActive).ToList();
|
||||||
|
// Check if multiple primary hierarchies are provided for the employee
|
||||||
|
if (primaryHierarchies.Count > 1)
|
||||||
|
{
|
||||||
|
// Log a warning indicating multiple primary hierarchies are not allowed
|
||||||
|
_logger.LogWarning("Multiple primary hierarchy entries detected for employee {EmployeeId}. Only one primary hierarchy is allowed.", employeeId);
|
||||||
|
|
||||||
|
// Return a bad request response with a clear, user-friendly message and an error code
|
||||||
|
return ApiResponse<object>.ErrorResponse(
|
||||||
|
"Multiple primary hierarchies detected. Only one primary hierarchy is permitted per employee.",
|
||||||
|
"Multiple primary hierarchies detected. Only one primary hierarchy is permitted per employee.",
|
||||||
|
400);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Fetch current active hierarchies for employee and tenant status, no tracking needed since we will update selectively
|
||||||
|
var existingHierarchies = await _context.OrganizationHierarchies
|
||||||
|
.Where(oh => oh.EmployeeId == employeeId && oh.IsActive && oh.TenantId == tenantId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var newEntries = new List<OrganizationHierarchy>();
|
||||||
|
var deactivateEntries = new List<OrganizationHierarchy>();
|
||||||
|
var auditLogs = new List<OrgHierarchyLog>();
|
||||||
|
|
||||||
|
// Cache primary hierarchy for quick reference to enforce business rules about one primary per employee
|
||||||
|
var existingPrimary = existingHierarchies.FirstOrDefault(oh => oh.IsPrimary);
|
||||||
|
|
||||||
|
// Process each input model item intelligently
|
||||||
|
foreach (var dto in model)
|
||||||
{
|
{
|
||||||
var newOrganizationHieraechy = new OrganizationHierarchy
|
var matchingEntry = existingHierarchies
|
||||||
|
.FirstOrDefault(oh => oh.ReportToId == dto.ReportToId && oh.IsPrimary == dto.IsPrimary);
|
||||||
|
|
||||||
|
if (dto.IsActive)
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
// Add new entry if none exists
|
||||||
EmployeeId = employeeId,
|
if (matchingEntry == null)
|
||||||
ReportToId = hierarchy.ReportToId,
|
{
|
||||||
IsPrimary = hierarchy.IsPrimary,
|
// Enforce primary uniqueness by checking if a primary exists and whether client intends to deactivate the old one
|
||||||
IsActive = true,
|
if (dto.IsPrimary && existingPrimary != null)
|
||||||
AssignedAt = DateTime.UtcNow,
|
{
|
||||||
AssignedById = loggedInEmployee.Id,
|
var intendedPrimaryDeactivation = model.Any(m =>
|
||||||
TenantId = tenantId
|
m.IsPrimary && !m.IsActive && m.ReportToId == existingPrimary.ReportToId);
|
||||||
};
|
|
||||||
newOrganizationHieraechies.Add(newOrganizationHieraechy);
|
|
||||||
|
|
||||||
}
|
if (!intendedPrimaryDeactivation)
|
||||||
else if (!hierarchy.IsActive && existingOrganizationHieraechy != null)
|
{
|
||||||
{
|
_logger.LogWarning("Attempt to assign a second primary hierarchy for employee {EmployeeId} without deactivating current one.",
|
||||||
existingOrganizationHieraechy.IsActive = false;
|
employeeId);
|
||||||
removeOrganizationHieraechies.Add(existingOrganizationHieraechy);
|
continue; // Skip this to maintain data integrity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
orgHierarchyLogs.Add(new OrgHierarchyLog
|
newEntries.Add(new OrganizationHierarchy
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
EmployeeId = employeeId,
|
||||||
|
ReportToId = dto.ReportToId,
|
||||||
|
IsPrimary = dto.IsPrimary,
|
||||||
|
IsActive = true,
|
||||||
|
AssignedAt = DateTime.UtcNow,
|
||||||
|
AssignedById = loggedInEmployee.Id,
|
||||||
|
TenantId = tenantId
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInfo("Prepared new active hierarchy link: EmployeeId {EmployeeId}, ReportsTo {ReportToId}, Primary {Primary}",
|
||||||
|
employeeId, dto.ReportToId, dto.IsPrimary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
// Deactivate existing entry if found and allowed
|
||||||
OrganizationHierarchyId = existingOrganizationHieraechy.Id,
|
if (matchingEntry != null)
|
||||||
ReAssignedAt = DateTime.UtcNow,
|
{
|
||||||
ReAssignedById = loggedInEmployee.Id,
|
if (dto.IsPrimary)
|
||||||
TenantId = tenantId
|
{
|
||||||
});
|
// Confirm alternative primary exists on active state to avoid orphan primary state
|
||||||
|
var alternativePrimaryExists = model.Any(m =>
|
||||||
|
m.IsPrimary && m.IsActive && m.ReportToId != dto.ReportToId);
|
||||||
|
|
||||||
|
if (!alternativePrimaryExists)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Attempt to deactivate sole primary hierarchy for employee {EmployeeId} prevented.", employeeId);
|
||||||
|
continue; // Skip deactivation to avoid orphan primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingEntry.IsActive = false;
|
||||||
|
deactivateEntries.Add(matchingEntry);
|
||||||
|
|
||||||
|
auditLogs.Add(new OrgHierarchyLog
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
OrganizationHierarchyId = matchingEntry.Id,
|
||||||
|
ReAssignedAt = DateTime.UtcNow,
|
||||||
|
ReAssignedById = loggedInEmployee.Id,
|
||||||
|
TenantId = tenantId
|
||||||
|
});
|
||||||
|
|
||||||
|
_logger.LogInfo("Marked hierarchy for deactivation: EmployeeId {EmployeeId}, ReportsTo {ReportToId}", employeeId, dto.ReportToId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (newOrganizationHieraechies.Any())
|
// Batch database operations for insertions and updates
|
||||||
|
if (newEntries.Any()) _context.OrganizationHierarchies.AddRange(newEntries);
|
||||||
|
if (deactivateEntries.Any()) _context.OrganizationHierarchies.UpdateRange(deactivateEntries);
|
||||||
|
if (auditLogs.Any()) _context.OrgHierarchyLogs.AddRange(auditLogs);
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Reload updated active hierarchy with related entities to respond with fresh data
|
||||||
|
var updatedHierarchy = await _context.OrganizationHierarchies
|
||||||
|
.Include(o => o.Employee).ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(o => o.ReportTo).ThenInclude(e => e!.JobRole)
|
||||||
|
.Include(o => o.AssignedBy).ThenInclude(e => e!.JobRole)
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(oh => oh.EmployeeId == employeeId && oh.IsActive && oh.TenantId == tenantId)
|
||||||
|
.OrderByDescending(oh => oh.AssignedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var response = _mapper.Map<List<OrganizationHierarchyVM>>(updatedHierarchy);
|
||||||
|
|
||||||
|
_logger.LogInfo("Organization hierarchy update completed for employee {EmployeeId}. NewEntries: {NewCount}, DeactivatedEntries: {DeactivatedCount}, LogsCreated: {LogCount}.",
|
||||||
|
employeeId, newEntries.Count, deactivateEntries.Count, auditLogs.Count);
|
||||||
|
|
||||||
|
return ApiResponse<object>.SuccessResponse(response, $"{response.Count} active superior(s) retrieved and updated successfully.", 200);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_context.OrganizationHierarchies.AddRange(newOrganizationHieraechies);
|
_logger.LogError(ex, "Exception while managing organization hierarchy for employee {EmployeeId} in tenant {TenantId}.", employeeId, tenantId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred while processing the request.", 500);
|
||||||
}
|
}
|
||||||
if (removeOrganizationHieraechies.Any())
|
|
||||||
{
|
|
||||||
_context.OrganizationHierarchies.UpdateRange(removeOrganizationHieraechies);
|
|
||||||
}
|
|
||||||
if (orgHierarchyLogs.Any())
|
|
||||||
{
|
|
||||||
_context.OrgHierarchyLogs.AddRange(orgHierarchyLogs);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
|
|
||||||
var organizationHieraechies = await _context.OrganizationHierarchies
|
|
||||||
.Include(oh => oh.Employee).ThenInclude(e => e!.JobRole)
|
|
||||||
.Include(oh => oh.ReportTo).ThenInclude(e => e!.JobRole)
|
|
||||||
.Include(oh => oh.AssignedBy).ThenInclude(e => e!.JobRole)
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(oh => oh.EmployeeId == employeeId && oh.IsActive && oh.TenantId == tenantId)
|
|
||||||
.OrderByDescending(oh => oh.AssignedAt)
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var response = _mapper.Map<List<OrganizationHierarchyVM>>(organizationHieraechies);
|
|
||||||
return ApiResponse<object>.SuccessResponse(response, $"{response.Count} superior fetched successfully", 200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Put Functions ===================================================================
|
#region =================================================================== Put Functions ===================================================================
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user