diff --git a/Marco.Pms.Services/Controllers/OrganizationController.cs b/Marco.Pms.Services/Controllers/OrganizationController.cs index 18e9fbc..ec6f168 100644 --- a/Marco.Pms.Services/Controllers/OrganizationController.cs +++ b/Marco.Pms.Services/Controllers/OrganizationController.cs @@ -104,7 +104,18 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } - + [HttpPost("hierarchy/manage/{employeeId}")] + public async Task ManageOrganizationHierarchy(Guid employeeId, [FromBody] List model) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _organizationService.ManageOrganizationHierarchyAsync(employeeId, model, loggedInEmployee, tenantId, loggedOrganizationId); + if (response.Success) + { + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Organization_Hierarchy", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + } #endregion diff --git a/Marco.Pms.Services/Service/OrganizationService.cs b/Marco.Pms.Services/Service/OrganizationService.cs index 1d08336..1c92cea 100644 --- a/Marco.Pms.Services/Service/OrganizationService.cs +++ b/Marco.Pms.Services/Service/OrganizationService.cs @@ -781,6 +781,166 @@ namespace Marco.Pms.Services.Service } } + /// + /// Atomically manage the organization hierarchy for an employee: add, deactivate, and audit changes. + /// + /// Employee GUID to manage hierarchies for. + /// List of hierarchy changes (DTOs). + /// Current user performing the operation. + /// Tenant context for multi-tenancy support. + /// Current logged-in organization context. + /// Standardized ApiResponse with updated hierarchy or error details. + public async Task> ManageOrganizationHierarchyAsync(Guid employeeId, List model, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId) + { + // Validate required parameters early to avoid wasted DB calls + if (tenantId == Guid.Empty || loggedOrganizationId == Guid.Empty) + { + _logger.LogWarning("Unauthorized attempt: Invalid tenant or organization IDs. TenantId: {TenantId}, OrgId: {OrgId}", tenantId, loggedOrganizationId); + + return ApiResponse.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.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.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(); + var deactivateEntries = new List(); + var auditLogs = new List(); + + // 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 matchingEntry = existingHierarchies + .FirstOrDefault(oh => oh.ReportToId == dto.ReportToId && oh.IsPrimary == dto.IsPrimary); + + if (dto.IsActive) + { + // Add new entry if none exists + if (matchingEntry == null) + { + // Enforce primary uniqueness by checking if a primary exists and whether client intends to deactivate the old one + if (dto.IsPrimary && existingPrimary != null) + { + var intendedPrimaryDeactivation = model.Any(m => + m.IsPrimary && !m.IsActive && m.ReportToId == existingPrimary.ReportToId); + + if (!intendedPrimaryDeactivation) + { + _logger.LogWarning("Attempt to assign a second primary hierarchy for employee {EmployeeId} without deactivating current one.", + employeeId); + continue; // Skip this to maintain data integrity + } + } + + 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 + { + // Deactivate existing entry if found and allowed + if (matchingEntry != null) + { + if (dto.IsPrimary) + { + // 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); + } + } + } + + // 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>(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.SuccessResponse(response, $"{response.Count} active superior(s) retrieved and updated successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception while managing organization hierarchy for employee {EmployeeId} in tenant {TenantId}.", employeeId, tenantId); + return ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred while processing the request.", 500); + } + } + #endregion diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs index b1087e4..bc9c612 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs @@ -16,7 +16,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> CreateOrganizationAsync(CreateOrganizationDto model, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); Task> AssignOrganizationToProjectAsync(AssignOrganizationDto model, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); Task> AssignOrganizationToTenantAsync(Guid organizationId, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); - + Task> ManageOrganizationHierarchyAsync(Guid employeeId, List model, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); #endregion #region =================================================================== Put Functions ===================================================================