1289 lines
68 KiB
C#
1289 lines
68 KiB
C#
using AutoMapper;
|
|
using AutoMapper.QueryableExtensions;
|
|
using Marco.Pms.DataAccess.Data;
|
|
using Marco.Pms.Model.Dtos.Organization;
|
|
using Marco.Pms.Model.Employees;
|
|
using Marco.Pms.Model.Entitlements;
|
|
using Marco.Pms.Model.OrganizationModel;
|
|
using Marco.Pms.Model.Utilities;
|
|
using Marco.Pms.Model.ViewModels.Activities;
|
|
using Marco.Pms.Model.ViewModels.Master;
|
|
using Marco.Pms.Model.ViewModels.Organization;
|
|
using Marco.Pms.Model.ViewModels.Projects;
|
|
using Marco.Pms.Services.Helpers;
|
|
using Marco.Pms.Services.Service.ServiceInterfaces;
|
|
using MarcoBMS.Services.Service;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Linq.Dynamic.Core;
|
|
|
|
namespace Marco.Pms.Services.Service
|
|
{
|
|
public class OrganizationService : IOrganizationService
|
|
{
|
|
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
|
private readonly ApplicationDbContext _context;
|
|
private readonly IServiceScopeFactory _serviceScope;
|
|
private readonly IMapper _mapper;
|
|
private readonly ILoggingService _logger;
|
|
|
|
private static readonly Guid PMCProvider = Guid.Parse("b1877a3b-8832-47b1-bbe3-dc7e98672f49");
|
|
private static readonly Guid ServiceProvider = Guid.Parse("5ee49bcd-b6d3-482f-9aaf-484afe04abec");
|
|
private static readonly Guid SubContractorProvider = Guid.Parse("a283356a-9b02-4029-afb7-e65c703efdd4");
|
|
private static readonly Guid superTenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26");
|
|
public OrganizationService(IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
|
ApplicationDbContext context,
|
|
IServiceScopeFactory serviceScope,
|
|
ILoggingService logger,
|
|
IMapper mapper)
|
|
{
|
|
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
|
|
_context = context ?? throw new ArgumentNullException(nameof(context));
|
|
_serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
|
}
|
|
|
|
#region =================================================================== Get Functions ===================================================================
|
|
|
|
public async Task<ApiResponse<object>> GetOrganizarionListAsync(string? searchString, long? sprid, bool active, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
|
|
{
|
|
_logger.LogDebug("Fetching organization list. SearchString: {SearchString}, SPRID: {SPRID}, Active: {Active}, Page: {PageNumber}, Size: {PageSize}",
|
|
searchString ?? "", sprid ?? 0, active, pageNumber, pageSize);
|
|
|
|
// Base query filtering by active status
|
|
IQueryable<Organization> organizationQuery = _context.Organizations.Where(o => o.IsActive == active);
|
|
|
|
if (sprid.HasValue)
|
|
{
|
|
// Filter by SPRID if provided
|
|
organizationQuery = organizationQuery.Where(o => o.SPRID == sprid.Value);
|
|
_logger.LogDebug("Filtering organizations by SPRID: {SPRID}", sprid.Value);
|
|
}
|
|
else
|
|
{
|
|
// Get organization IDs mapped to current tenant that are active
|
|
var organizationIdsTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.TenantOrgMappings
|
|
.Where(to => to.TenantId == tenantId && to.IsActive)
|
|
.Select(to => to.OrganizationId)
|
|
.ToListAsync();
|
|
});
|
|
|
|
var tenantTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.Tenants
|
|
.FirstOrDefaultAsync(t => t.Id == tenantId && t.IsActive);
|
|
});
|
|
|
|
await Task.WhenAll(organizationIdsTask, tenantTask);
|
|
|
|
var organizationIds = organizationIdsTask.Result;
|
|
var tenant = tenantTask.Result;
|
|
|
|
if (tenant == null)
|
|
{
|
|
_logger.LogWarning("Tenant {TenantId} is not found", tenantId);
|
|
return ApiResponse<object>.ErrorResponse("Tenant not found", "Tenant not found in database", 404);
|
|
}
|
|
|
|
organizationIds.Add(tenant.OrganizationId);
|
|
|
|
organizationIds = organizationIds.Distinct().ToList();
|
|
|
|
organizationQuery = organizationQuery.Where(o => organizationIds.Contains(o.Id));
|
|
_logger.LogDebug("Filtering organizations by tenant's mapped IDs count: {Count}", organizationIds.Count);
|
|
|
|
if (!string.IsNullOrWhiteSpace(searchString))
|
|
{
|
|
// Filter by search string on organization name -- extend here if needed
|
|
organizationQuery = organizationQuery.Where(o => o.Name.Contains(searchString));
|
|
_logger.LogDebug("Filtering organizations by search string: {SearchString}", searchString);
|
|
}
|
|
}
|
|
|
|
// Get total count for pagination
|
|
var totalCount = await organizationQuery.CountAsync();
|
|
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
|
|
|
|
// Fetch page of organizations sorted by name
|
|
var organizations = await organizationQuery
|
|
.OrderBy(o => o.Name)
|
|
.Skip((pageNumber - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.ToListAsync();
|
|
|
|
if (!organizations.Any() && !sprid.HasValue)
|
|
{
|
|
organizations = await _context.Tenants.AsNoTracking()
|
|
.Include(t => t.Organization)
|
|
.Where(t => t.Id == tenantId && t.OrganizationId == loggedInEmployee.OrganizationId && t.Organization != null && t.IsActive)
|
|
.Select(t => t.Organization!).ToListAsync();
|
|
}
|
|
|
|
// Collect creator and updater employee IDs
|
|
var createdByIds = organizations.Where(o => o.CreatedById != null).Select(o => o.CreatedById!.Value).Distinct().ToList();
|
|
var updatedByIds = organizations.Where(o => o.UpdatedById != null).Select(o => o.UpdatedById!.Value).Distinct().ToList();
|
|
|
|
// Fetch corresponding employee details in one query
|
|
var employeeIds = createdByIds.Union(updatedByIds).ToList();
|
|
var employees = await _context.Employees.Where(e => employeeIds.Contains(e.Id)).ToListAsync();
|
|
|
|
// Map data to view models including created and updated by employees
|
|
var vm = organizations.Select(o =>
|
|
{
|
|
var orgVm = _mapper.Map<OrganizationVM>(o);
|
|
orgVm.CreatedBy = employees.Where(e => e.Id == o.CreatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault();
|
|
orgVm.UpdatedBy = employees.Where(e => e.Id == o.UpdatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault();
|
|
return orgVm;
|
|
}).ToList();
|
|
|
|
var response = new
|
|
{
|
|
CurrentPage = pageNumber,
|
|
TotalPages = totalPages,
|
|
TotalEntities = totalCount,
|
|
Data = vm,
|
|
};
|
|
|
|
_logger.LogInfo("Fetched {Count} organizations (Page {PageNumber} of {TotalPages})", vm.Count, pageNumber, totalPages);
|
|
|
|
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the organization list", 200);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves a paginated, searchable list of organizations.
|
|
/// Optimized for performance using DB Projections and AsNoTracking.
|
|
/// </summary>
|
|
/// <param name="searchString">Optional keyword to filter organizations by name.</param>
|
|
/// <param name="pageNumber">The requested page number (1-based).</param>
|
|
/// <param name="pageSize">The number of records per page (Max 50).</param>
|
|
/// <param name="loggedInEmployee">The current user context for security filtering.</param>
|
|
/// <param name="ct">Cancellation token to cancel operations if the client disconnects.</param>
|
|
/// <returns>A paginated list of BasicOrganizationVm.</returns>
|
|
public async Task<ApiResponse<object>> GetOrganizationBasicListAsync(Guid? id, string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, CancellationToken ct = default)
|
|
{
|
|
try
|
|
{
|
|
// 1. VALIDATION
|
|
_logger.LogInfo("Fetching Organization list. Page: {Page}, Size: {Size}, Search: {Search}, User: {UserId}",
|
|
pageNumber, pageSize, searchString ?? "<empty>", loggedInEmployee.Id);
|
|
|
|
// 2. QUERY BUILDING
|
|
// Use AsNoTracking() for read-only scenarios to reduce overhead.
|
|
var query = _context.Organizations.AsNoTracking()
|
|
.Where(o => o.IsActive);
|
|
|
|
// 3. SECURITY FILTER (Multi-Tenancy)
|
|
// Enterprise Rule: Always filter by the logged-in user's Tenant/Permissions.
|
|
// Assuming loggedInEmployee has a TenantId or OrganizationId
|
|
// query = query.Where(o => o.TenantId == loggedInEmployee.TenantId);
|
|
|
|
// 4. DYNAMIC FILTERING
|
|
if (!string.IsNullOrWhiteSpace(searchString))
|
|
{
|
|
var searchTrimmed = searchString.Trim();
|
|
query = query.Where(o => o.Name.Contains(searchTrimmed));
|
|
}
|
|
|
|
if (id.HasValue)
|
|
{
|
|
query = query.Where(o => o.Id == id.Value);
|
|
}
|
|
|
|
// 5. COUNT TOTALS (Efficiently)
|
|
// Count the total records matching the filter BEFORE applying pagination
|
|
var totalCount = await query.CountAsync(ct);
|
|
|
|
// 6. FETCH DATA (With Projection)
|
|
// CRITICAL OPTIMIZATION: Use .ProjectTo or .Select BEFORE .ToListAsync.
|
|
// This ensures SQL only fetches the columns needed for BasicOrganizationVm,
|
|
// rather than fetching the whole Entity and discarding data in memory.
|
|
var items = await query
|
|
.OrderBy(o => o.Name)
|
|
.Skip((pageNumber - 1) * pageSize)
|
|
.Take(pageSize)
|
|
.ProjectTo<BasicOrganizationVm>(_mapper.ConfigurationProvider) // Requires AutoMapper.QueryableExtensions
|
|
.ToListAsync(ct);
|
|
|
|
// 7. PREPARE RESPONSE
|
|
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
|
|
|
|
var pagedResult = new
|
|
{
|
|
CurrentPage = pageNumber,
|
|
PageSize = pageSize,
|
|
TotalPages = totalPages,
|
|
TotalCount = totalCount,
|
|
HasPrevious = pageNumber > 1,
|
|
HasNext = pageNumber < totalPages,
|
|
Data = items
|
|
};
|
|
|
|
_logger.LogInfo("Successfully fetched {Count} organizations out of {Total}.", items.Count, totalCount);
|
|
|
|
return ApiResponse<object>.SuccessResponse(pagedResult, "Organization list fetched successfully.", 200);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Handle client disconnection gracefully
|
|
_logger.LogWarning("Organization list fetch was cancelled by the client.");
|
|
return ApiResponse<object>.ErrorResponse("Request Cancelled", "The operation was cancelled.", 499);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to fetch organization list. User: {UserId}, Error: {Message}", loggedInEmployee.Id, ex.Message);
|
|
return ApiResponse<object>.ErrorResponse("Data Fetch Failed", "An unexpected error occurred while retrieving the organization list.", 500);
|
|
}
|
|
}
|
|
public async Task<ApiResponse<object>> GetOrganizationDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
|
|
{
|
|
_logger.LogDebug("Started fetching details for OrganizationId: {OrganizationId}", id);
|
|
|
|
try
|
|
{
|
|
|
|
// Fetch the organization entity by Id
|
|
var organization = await _context.Organizations.FirstOrDefaultAsync(o => o.Id == id);
|
|
if (organization == null)
|
|
{
|
|
_logger.LogWarning("Organization not found for OrganizationId: {OrganizationId}", id);
|
|
return ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found", 404);
|
|
}
|
|
|
|
// Fetch CreatedBy employee (with JobRole)
|
|
var createdByTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.Employees
|
|
.Include(e => e.JobRole)
|
|
.FirstOrDefaultAsync(e => e.Id == organization.CreatedById);
|
|
});
|
|
|
|
// Fetch UpdatedBy employee (with JobRole)
|
|
var updatedByTask = Task.Run(async () =>
|
|
{
|
|
if (organization.UpdatedById.HasValue)
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.Employees
|
|
.Include(e => e.JobRole)
|
|
.FirstOrDefaultAsync(e => e.Id == organization.UpdatedById);
|
|
}
|
|
return null;
|
|
});
|
|
|
|
// Fetch the organization's service mappings and corresponding services
|
|
var orgServiceMappingTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.OrgServiceMappings
|
|
.Include(os => os.Service)
|
|
.Where(os => os.OrganizationId == id).ToListAsync();
|
|
});
|
|
|
|
// Fetch active employees in the organization
|
|
var employeeListTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.Employees
|
|
.Where(e => e.OrganizationId == id && e.IsActive).ToListAsync();
|
|
});
|
|
|
|
await Task.WhenAll(createdByTask, updatedByTask, orgServiceMappingTask, employeeListTask);
|
|
|
|
var createdByEmployee = createdByTask.Result;
|
|
var updatedByEmployee = updatedByTask.Result;
|
|
var orgServiceMappings = orgServiceMappingTask.Result;
|
|
var employeeList = employeeListTask.Result;
|
|
|
|
var activeEmployeeCount = employeeList.Count;
|
|
var activeApplicationUserCount = employeeList.Count(e => e.HasApplicationAccess);
|
|
|
|
// Start query for projects mapped to this organization (including project and service info)
|
|
var baseProjectOrgMappingQuery = _context.ProjectOrgMappings
|
|
.Include(po => po.ProjectService)
|
|
.ThenInclude(ps => ps!.Service)
|
|
.Include(po => po.ProjectService)
|
|
.ThenInclude(ps => ps!.Project)
|
|
.Where(po => po.OrganizationId == id && po.ProjectService != null);
|
|
|
|
// If logged-in employee is not from the requested organization, restrict projects to those also mapped to their org
|
|
List<ProjectOrgMapping> projectOrgMappings;
|
|
if (loggedInEmployee.OrganizationId != id)
|
|
{
|
|
var projectIds = await _context.ProjectOrgMappings
|
|
.Include(po => po.ProjectService)
|
|
.Where(po => po.OrganizationId == loggedInEmployee.OrganizationId && po.ProjectService != null)
|
|
.Select(po => po.ProjectService!.ProjectId)
|
|
.ToListAsync();
|
|
|
|
projectOrgMappings = await baseProjectOrgMappingQuery
|
|
.Where(po => projectIds.Contains(po.ProjectService!.ProjectId))
|
|
.ToListAsync();
|
|
}
|
|
else
|
|
{
|
|
projectOrgMappings = await baseProjectOrgMappingQuery.ToListAsync();
|
|
}
|
|
|
|
// Map results to output view model
|
|
var response = _mapper.Map<OrganizationDetailsVM>(organization);
|
|
response.ActiveApplicationUserCount = activeApplicationUserCount;
|
|
response.ActiveEmployeeCount = activeEmployeeCount;
|
|
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(createdByEmployee);
|
|
response.UpdatedBy = _mapper.Map<BasicEmployeeVM>(updatedByEmployee);
|
|
response.Projects = _mapper.Map<List<ProjectServiceMappingVM>>(projectOrgMappings.Select(po => po.ProjectService).ToList());
|
|
response.Services = orgServiceMappings.Where(os => os.Service != null).Select(os => os.Service!).ToList();
|
|
|
|
_logger.LogInfo("Fetched organization details for OrganizationId: {OrganizationId}, Employee count: {EmployeeCount}, App user count: {AppUserCount}, Project count: {ProjectCount}",
|
|
id, activeEmployeeCount, activeApplicationUserCount, response.Projects.Count);
|
|
|
|
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the organization details", 200);
|
|
}
|
|
catch (DbUpdateException dbEx)
|
|
{
|
|
_logger.LogError(dbEx, "Database exception while fetching details for OrganizationId: {OrganizationId}", id);
|
|
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Unhandled exception while fetching details for OrganizationId: {OrganizationId}", id);
|
|
return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the active organization hierarchy list for a specified employee within a given tenant.
|
|
/// </summary>
|
|
/// <param name="employeeId">ID of the employee whose hierarchy is requested.</param>
|
|
/// <param name="loggedInEmployee">Logged-in employee making the request (for audit/logging).</param>
|
|
/// <param name="tenantId">Tenant ID for multi-tenant filtering.</param>
|
|
/// <param name="loggedOrganizationId">Organization ID of the logged-in employee (for access validation).</param>
|
|
/// <returns>ApiResponse containing the list of organization hierarchy view models or error details.</returns>
|
|
public async Task<ApiResponse<object>> GetOrganizationHierarchyListAsync(Guid employeeId, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
|
|
{
|
|
// Validate input IDs
|
|
if (tenantId == Guid.Empty || loggedOrganizationId == Guid.Empty)
|
|
{
|
|
_logger.LogWarning("Access denied: Invalid tenantId or loggedOrganizationId. TenantId: {TenantId}, OrganizationId: {OrganizationId}", tenantId, loggedOrganizationId);
|
|
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant or organization identifier.", 403);
|
|
}
|
|
|
|
try
|
|
{
|
|
// Query to get active organization hierarchies, including related navigation properties for richer data
|
|
var organizationHierarchies = await _context.OrganizationHierarchies
|
|
.Include(oh => oh.Employee).ThenInclude(e => e!.JobRole)
|
|
.Include(oh => oh.AssignedBy).ThenInclude(e => e!.JobRole)
|
|
.Include(oh => oh.ReportTo).ThenInclude(e => e!.JobRole)
|
|
.AsNoTracking()
|
|
.Where(oh => oh.EmployeeId == employeeId && oh.IsActive && oh.TenantId == tenantId)
|
|
.OrderByDescending(oh => oh.AssignedAt)
|
|
.ToListAsync();
|
|
|
|
// Check if any records found
|
|
if (!organizationHierarchies.Any())
|
|
{
|
|
_logger.LogWarning("No active organization hierarchy found for EmployeeId: {EmployeeId} in TenantId: {TenantId}.", employeeId, tenantId);
|
|
return ApiResponse<object>.SuccessResponse(new List<OrganizationHierarchyVM>(), "No active superiors found.", 200);
|
|
}
|
|
|
|
// Map entities to view models
|
|
var response = _mapper.Map<List<OrganizationHierarchyVM>>(organizationHierarchies);
|
|
|
|
_logger.LogInfo("Fetched {Count} active superiors for EmployeeId: {EmployeeId} in TenantId: {TenantId}.", response.Count, employeeId, tenantId);
|
|
|
|
return ApiResponse<object>.SuccessResponse(response, $"{response.Count} superior(s) fetched successfully.", 200);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "An error occurred while fetching organization hierarchy list for EmployeeId: {EmployeeId} in TenantId: {TenantId}.", employeeId, tenantId);
|
|
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while processing your request.", 500);
|
|
}
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region =================================================================== Post Functions ===================================================================
|
|
|
|
public async Task<ApiResponse<object>> CreateOrganizationAsync(CreateOrganizationDto model, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
|
|
{
|
|
using var scope = _serviceScope.CreateScope(); // Create scope for scoped services
|
|
await using var transaction = await _context.Database.BeginTransactionAsync();
|
|
|
|
try
|
|
{
|
|
|
|
// Concurrent permission check and organization existence check
|
|
var hasPermissionTask = Task.Run(async () =>
|
|
{
|
|
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
|
return await permissionService.HasPermission(PermissionsMaster.AddOrganization, loggedInEmployee.Id);
|
|
});
|
|
|
|
var isPrimaryOrganizationTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.Tenants.AnyAsync(t => t.OrganizationId == loggedInEmployee.OrganizationId);
|
|
});
|
|
|
|
await Task.WhenAll(hasPermissionTask, isPrimaryOrganizationTask);
|
|
|
|
bool hasPermission = hasPermissionTask.Result;
|
|
bool isPrimaryOrganization = isPrimaryOrganizationTask.Result;
|
|
|
|
// Check user access permission
|
|
if (!hasPermission && !isPrimaryOrganization)
|
|
{
|
|
_logger.LogWarning("User {EmployeeId} attempted to create a new organization without permission", loggedInEmployee.Id);
|
|
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to create new organization.", 403);
|
|
}
|
|
|
|
// Get last SPRID and increment for new organization
|
|
var lastOrganization = await _context.Organizations.OrderByDescending(sp => sp.SPRID).FirstOrDefaultAsync();
|
|
long lastSPRID = lastOrganization?.SPRID ?? 5400;
|
|
|
|
// Map DTO to entity and set defaults
|
|
Organization organization = _mapper.Map<Organization>(model);
|
|
organization.SPRID = lastSPRID + 1;
|
|
organization.CreatedAt = DateTime.UtcNow;
|
|
organization.CreatedById = loggedInEmployee.Id;
|
|
organization.IsActive = true;
|
|
|
|
_context.Organizations.Add(organization);
|
|
|
|
// Create mapping for organization tenant
|
|
var newOrganizationTenantMapping = new TenantOrgMapping
|
|
{
|
|
OrganizationId = organization.Id,
|
|
SPRID = organization.SPRID,
|
|
AssignedDate = DateTime.UtcNow,
|
|
IsActive = true,
|
|
AssignedById = loggedInEmployee.Id,
|
|
TenantId = tenantId
|
|
};
|
|
_context.TenantOrgMappings.Add(newOrganizationTenantMapping);
|
|
|
|
//// Prepare user creation for identity
|
|
//var user = new ApplicationUser
|
|
//{
|
|
// UserName = model.Email,
|
|
// Email = model.Email,
|
|
// EmailConfirmed = true
|
|
//};
|
|
|
|
//var configuration = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
|
//var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
|
|
|
|
////Create Identity user with a default password (recommend to improve password handling)
|
|
//var result = await userManager.CreateAsync(user, "User@123");
|
|
//if (!result.Succeeded)
|
|
//{
|
|
// _logger.LogWarning("Failed to create identity user for email {Email}: {Errors}", model.Email, result.Errors);
|
|
// return ApiResponse<object>.ErrorResponse("Failed to create user", result.Errors, 400);
|
|
//}
|
|
|
|
//// Get admin job role or fallback role of the tenant
|
|
//var jobRole = await _context.JobRoles.FirstOrDefaultAsync(jr => jr.Name == "Admin" && jr.TenantId == tenantId)
|
|
// ?? await _context.JobRoles.FirstOrDefaultAsync(jr => jr.TenantId == tenantId);
|
|
|
|
//// Parse full name safely (consider improving split logic for multi-part names)
|
|
//var fullName = model.ContactPerson.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
//Employee newEmployee = new Employee
|
|
//{
|
|
// FirstName = fullName.Length > 0 ? fullName[0] : string.Empty,
|
|
// LastName = fullName.Length > 1 ? fullName[^1] : string.Empty,
|
|
// Email = model.Email,
|
|
// PermanentAddress = model.Address,
|
|
// CurrentAddress = model.Address,
|
|
// PhoneNumber = model.ContactNumber,
|
|
// ApplicationUserId = user.Id,
|
|
// JobRoleId = jobRole?.Id ?? Guid.Empty,
|
|
// IsActive = true,
|
|
// IsSystem = false,
|
|
// IsPrimary = true,
|
|
// OrganizationId = organization.Id,
|
|
// HasApplicationAccess = true
|
|
//};
|
|
|
|
//_context.Employees.Add(newEmployee);
|
|
|
|
// Map organization services
|
|
if (model.ServiceIds?.Any() ?? false)
|
|
{
|
|
var serviceOrgMappings = model.ServiceIds.Select(s => new OrgServiceMapping
|
|
{
|
|
ServiceId = s,
|
|
OrganizationId = organization.Id
|
|
}).ToList();
|
|
|
|
_context.OrgServiceMappings.AddRange(serviceOrgMappings);
|
|
}
|
|
|
|
// Persist all changes
|
|
await _context.SaveChangesAsync();
|
|
|
|
await transaction.CommitAsync();
|
|
|
|
// Prepare response DTO
|
|
var response = _mapper.Map<OrganizationVM>(organization);
|
|
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
|
|
|
|
return ApiResponse<object>.SuccessResponse(response, "Successfully created the organization", 200);
|
|
}
|
|
catch (DbUpdateException dbEx)
|
|
{
|
|
await transaction.RollbackAsync();
|
|
_logger.LogError(dbEx, "Database exception occurred while creating organization");
|
|
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Unexpected exception occurred while creating organization");
|
|
return ApiResponse<object>.ErrorResponse("Internal error", "An unexpected error occurred", 500);
|
|
}
|
|
}
|
|
public async Task<ApiResponse<object>> AssignOrganizationToProjectAsync(AssignOrganizationDto model, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
|
|
{
|
|
_logger.LogDebug("Started assigning organization {OrganizationId} to project {ProjectId} with service IDs {@ServiceIds}",
|
|
model.OrganizationId, model.ProjectId, model.ServiceIds);
|
|
|
|
// Begin a database transaction
|
|
await using var transaction = await _context.Database.BeginTransactionAsync();
|
|
|
|
try
|
|
{
|
|
var today = DateTime.UtcNow.Date;
|
|
|
|
// Fetch all needed entities concurrently using the single context
|
|
var projectServicesTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.ProjectServiceMappings
|
|
.Where(sp => model.ServiceIds.Contains(sp.ServiceId) && sp.ProjectId == model.ProjectId && sp.IsActive).ToListAsync();
|
|
});
|
|
|
|
var projectOrgMappingsTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.ProjectOrgMappings
|
|
.Include(po => po.ProjectService)
|
|
.Where(po => po.ProjectService != null && model.ServiceIds.Contains(po.ProjectService.ServiceId) && po.ProjectService.ProjectId == model.ProjectId).ToListAsync();
|
|
});
|
|
|
|
var serviceTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.ServiceMasters.Where(s => model.ServiceIds.Contains(s.Id) && s.TenantId == tenantId).ToListAsync();
|
|
});
|
|
|
|
var orgTypeTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.OrgTypeMasters.FirstOrDefaultAsync(o => o.Id == model.OrganizationId);
|
|
});
|
|
|
|
var organizationTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.OrganizationId);
|
|
});
|
|
|
|
var parentOrgTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.ParentOrganizationId);
|
|
});
|
|
|
|
var projectTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.Projects.FirstOrDefaultAsync(p => p.Id == model.ProjectId);
|
|
});
|
|
|
|
var isPMCTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.Projects.AnyAsync(p => p.Id == model.ProjectId && p.PMCId == loggedInEmployee.OrganizationId);
|
|
});
|
|
|
|
var isServiceProviderTask = Task.Run(async () =>
|
|
{
|
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
|
return await context.ProjectOrgMappings.AnyAsync(p => p.Id == model.ProjectId && p.OrganizationId == loggedInEmployee.OrganizationId
|
|
&& p.OrganizationTypeId == ServiceProvider);
|
|
});
|
|
|
|
await Task.WhenAll(projectTask, organizationTask, parentOrgTask, serviceTask, orgTypeTask, projectServicesTask, projectOrgMappingsTask, isPMCTask, isServiceProviderTask);
|
|
|
|
var project = projectTask.Result;
|
|
var organization = organizationTask.Result;
|
|
var parentOrganization = parentOrgTask.Result;
|
|
var services = serviceTask.Result;
|
|
var organizationType = orgTypeTask.Result;
|
|
var projectServices = projectServicesTask.Result;
|
|
var projectOrganizations = projectOrgMappingsTask.Result;
|
|
var isPMC = isPMCTask.Result;
|
|
var isServiceProvider = isServiceProviderTask.Result;
|
|
|
|
// Validation checks
|
|
if (organization == null)
|
|
{
|
|
_logger.LogWarning("Organization with ID {OrganizationId} not found.", model.OrganizationId);
|
|
return ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found in database", 404);
|
|
}
|
|
if (project == null)
|
|
{
|
|
_logger.LogWarning("Project with ID {ProjectId} not found.", model.ProjectId);
|
|
return ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404);
|
|
}
|
|
if (services == null || !services.Any())
|
|
{
|
|
_logger.LogWarning("No services found for Service IDs {@ServiceIds}.", model.ServiceIds);
|
|
return ApiResponse<object>.ErrorResponse("Project Service not found", "Project Service not found in database", 404);
|
|
}
|
|
|
|
// Check whether mapping exists between service provider organization and tenant
|
|
var serviceProviderTenantMapping = await _context.TenantOrgMappings
|
|
.FirstOrDefaultAsync(spt => spt.OrganizationId == model.OrganizationId && spt.TenantId == project.TenantId && spt.IsActive);
|
|
|
|
if (serviceProviderTenantMapping == null)
|
|
{
|
|
var newMapping = new TenantOrgMapping
|
|
{
|
|
OrganizationId = organization.Id,
|
|
SPRID = organization.SPRID,
|
|
AssignedDate = today,
|
|
IsActive = true,
|
|
AssignedById = loggedInEmployee.Id,
|
|
TenantId = project.TenantId
|
|
};
|
|
_context.TenantOrgMappings.Add(newMapping);
|
|
_logger.LogInfo("Created new TenantOrgMapping for OrganizationId {OrganizationId} and TenantId {TenantId}",
|
|
organization.Id, project.TenantId);
|
|
}
|
|
|
|
// Access control validations
|
|
if (isPMC && model.OrganizationTypeId != ServiceProvider && model.OrganizationTypeId != SubContractorProvider)
|
|
{
|
|
_logger.LogWarning("PMCs cannot assign organization type {OrganizationTypeId}. UserId: {UserId}",
|
|
model.OrganizationTypeId, loggedInEmployee.Id);
|
|
return ApiResponse<object>.ErrorResponse("Access Denied", "You don't have access to assign this type of organization", 403);
|
|
}
|
|
if (isServiceProvider && model.OrganizationTypeId == ServiceProvider)
|
|
{
|
|
_logger.LogWarning("Service providers cannot assign organization type {OrganizationTypeId}. UserId: {UserId}",
|
|
model.OrganizationTypeId, loggedInEmployee.Id);
|
|
return ApiResponse<object>.ErrorResponse("Access Denied", "You don't have access to assign this type of organization", 403);
|
|
}
|
|
|
|
var newProjectOrgMappings = new List<ProjectOrgMapping>();
|
|
var newProjectServiceMappings = new List<ProjectServiceMapping>();
|
|
|
|
// Loop through each service to create mappings
|
|
foreach (var serviceId in model.ServiceIds)
|
|
{
|
|
var service = services.FirstOrDefault(s => s.Id == serviceId);
|
|
if (service == null)
|
|
{
|
|
_logger.LogWarning("Service with ID {ServiceId} not found.", serviceId);
|
|
return ApiResponse<object>.ErrorResponse("Service not found", "Service not found in database", 404);
|
|
}
|
|
|
|
var projectService = projectServices.FirstOrDefault(ps => ps.ServiceId == serviceId);
|
|
if (projectService == null)
|
|
{
|
|
projectService = new ProjectServiceMapping
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
ProjectId = project.Id,
|
|
ServiceId = serviceId,
|
|
TenantId = project.TenantId,
|
|
PlannedStartDate = project.StartDate ?? today,
|
|
PlannedEndDate = project.EndDate ?? today,
|
|
ActualStartDate = today,
|
|
IsActive = true
|
|
};
|
|
newProjectServiceMappings.Add(projectService);
|
|
}
|
|
|
|
// Check if the organization is already assigned for this service
|
|
var existingAssignment = projectOrganizations.FirstOrDefault(po => po.ProjectService != null
|
|
&& po.ProjectService.ProjectId == project.Id
|
|
&& po.ProjectService.ServiceId == serviceId
|
|
&& po.OrganizationId == model.OrganizationId);
|
|
|
|
if (existingAssignment != null)
|
|
{
|
|
_logger.LogWarning("Organization {OrganizationId} is already assigned to project {ProjectId} for service {ServiceId}.",
|
|
model.OrganizationId, project.Id, serviceId);
|
|
return ApiResponse<object>.ErrorResponse("Organization already assigned", "Organization is already assigned to this project and service", 409);
|
|
}
|
|
|
|
// Prepare new project-org mapping
|
|
var projectOrgMapping = new ProjectOrgMapping
|
|
{
|
|
ProjectServiceId = projectService.Id,
|
|
OrganizationId = model.OrganizationId,
|
|
OrganizationTypeId = model.OrganizationTypeId,
|
|
ParentOrganizationId = model.ParentOrganizationId ?? loggedInEmployee.OrganizationId,
|
|
AssignedDate = today,
|
|
AssignedById = loggedInEmployee.Id,
|
|
TenantId = project.TenantId
|
|
};
|
|
newProjectOrgMappings.Add(projectOrgMapping);
|
|
}
|
|
|
|
// Save new project service mappings if any
|
|
if (newProjectServiceMappings.Any())
|
|
{
|
|
_context.ProjectServiceMappings.AddRange(newProjectServiceMappings);
|
|
await _context.SaveChangesAsync();
|
|
_logger.LogInfo("Added {Count} new ProjectServiceMappings for ProjectId {ProjectId}.", newProjectServiceMappings.Count, project.Id);
|
|
}
|
|
|
|
// Save new project organization mappings
|
|
_context.ProjectOrgMappings.AddRange(newProjectOrgMappings);
|
|
await _context.SaveChangesAsync();
|
|
|
|
// Commit transaction
|
|
await transaction.CommitAsync();
|
|
|
|
_logger.LogInfo("Assigned organization {OrganizationId} to project {ProjectId} successfully.", model.OrganizationId, model.ProjectId);
|
|
|
|
// Prepare response view models
|
|
var organizationVm = _mapper.Map<BasicOrganizationVm>(organization);
|
|
var parentOrganizationVm = _mapper.Map<BasicOrganizationVm>(parentOrganization);
|
|
var projectVm = _mapper.Map<BasicProjectVM>(project);
|
|
|
|
var response = services.Select(s => new AssignOrganizationVm
|
|
{
|
|
Project = projectVm,
|
|
OrganizationType = organizationType,
|
|
Organization = organizationVm,
|
|
ParentOrganization = parentOrganizationVm,
|
|
Service = _mapper.Map<ServiceMasterVM>(s)
|
|
}).ToList();
|
|
|
|
await AssignApplicationRoleToOrganization(organization.Id, project.TenantId, loggedOrganizationId);
|
|
|
|
return ApiResponse<object>.SuccessResponse(response, "Organization successfully assigned to the project", 200);
|
|
}
|
|
catch (DbUpdateException dbEx)
|
|
{
|
|
await transaction.RollbackAsync();
|
|
_logger.LogError(dbEx, "Database exception occurred while assigning organization {OrganizationId} to project {ProjectId}",
|
|
model.OrganizationId, model.ProjectId);
|
|
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await transaction.RollbackAsync();
|
|
_logger.LogError(ex, "Unhandled exception occurred while assigning organization {OrganizationId} to project {ProjectId}",
|
|
model.OrganizationId, model.ProjectId);
|
|
return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500);
|
|
}
|
|
}
|
|
public async Task<ApiResponse<object>> AssignOrganizationToTenantAsync(Guid organizationId, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
|
|
{
|
|
_logger.LogInfo("Started assigning organization {OrganizationId} to tenant {TenantId}", organizationId, tenantId);
|
|
|
|
// Begin a database transaction
|
|
await using var transaction = await _context.Database.BeginTransactionAsync();
|
|
|
|
try
|
|
{
|
|
// Fetch existing tenant-organization mapping if any
|
|
var organizationTenantMapping = await _context.TenantOrgMappings
|
|
.FirstOrDefaultAsync(spt => spt.OrganizationId == organizationId && spt.TenantId == tenantId && spt.IsActive);
|
|
|
|
// Fetch the organization details
|
|
var organization = await _context.Organizations.FirstOrDefaultAsync(o => o.Id == organizationId);
|
|
|
|
// Validate organization existence
|
|
if (organization == null)
|
|
{
|
|
_logger.LogWarning("Organization with ID {OrganizationId} not found.", organizationId);
|
|
return ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found in database", 404);
|
|
}
|
|
|
|
if (organizationTenantMapping != null)
|
|
{
|
|
_logger.LogInfo("Organization {OrganizationId} is already assigned to tenant {TenantId}. No action taken.", organizationId, tenantId);
|
|
// Commit transaction anyway to complete scope cleanly (optional)
|
|
await transaction.RollbackAsync();
|
|
return ApiResponse<object>.ErrorResponse("Organization is already assigned to tenant", "Organization is already assigned to tenant", 409);
|
|
}
|
|
|
|
// Create new tenant-organization mapping if none exists
|
|
var newMapping = new TenantOrgMapping
|
|
{
|
|
OrganizationId = organization.Id,
|
|
SPRID = organization.SPRID,
|
|
AssignedDate = DateTime.UtcNow,
|
|
IsActive = true,
|
|
AssignedById = loggedInEmployee.Id,
|
|
TenantId = tenantId
|
|
};
|
|
_context.TenantOrgMappings.Add(newMapping);
|
|
await _context.SaveChangesAsync();
|
|
await transaction.CommitAsync();
|
|
|
|
_logger.LogInfo("Assigned organization {OrganizationId} to tenant {TenantId} successfully.", organizationId, tenantId);
|
|
|
|
|
|
// Prepare response view model
|
|
var response = _mapper.Map<BasicOrganizationVm>(organization);
|
|
|
|
await AssignApplicationRoleToOrganization(organization.Id, tenantId, loggedOrganizationId);
|
|
|
|
return ApiResponse<object>.SuccessResponse(response, "Organization has been assigned to tenant", 200);
|
|
}
|
|
catch (DbUpdateException dbEx)
|
|
{
|
|
await transaction.RollbackAsync();
|
|
_logger.LogError(dbEx, "Database exception occurred while assigning organization {OrganizationId} to tenant {TenantId}.", organizationId, tenantId);
|
|
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception has occurred", 500);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await transaction.RollbackAsync();
|
|
_logger.LogError(ex, "Unhandled exception occurred while assigning organization {OrganizationId} to tenant {TenantId}.", organizationId, tenantId);
|
|
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)
|
|
{
|
|
// 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<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 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<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)
|
|
{
|
|
_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);
|
|
}
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
#region =================================================================== Put Functions ===================================================================
|
|
|
|
public async Task<ApiResponse<object>> UpdateOrganiationAsync(Guid id, UpdateOrganizationDto model, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
|
|
{
|
|
|
|
try
|
|
{
|
|
|
|
_logger.LogDebug("Started updating organization OrganizationId: {OrganizationId} by EmployeeId: {EmployeeId}",
|
|
id, loggedInEmployee.Id);
|
|
|
|
// Check if the user is a tenant-level employee and restrict editing to their own org
|
|
var isTenantEmployee = await _context.Tenants.AnyAsync(t => t.Id == tenantId && t.OrganizationId == loggedInEmployee.OrganizationId);
|
|
if (!isTenantEmployee && loggedInEmployee.OrganizationId != id)
|
|
{
|
|
_logger.LogWarning("Access denied. Tenant-level employee {EmployeeId} attempted to update another organization (OrganizationId: {OrganizationId})",
|
|
loggedInEmployee.Id, id);
|
|
return ApiResponse<object>.ErrorResponse("Access Denied", "User does not have permission to update the organization", 403);
|
|
}
|
|
|
|
// Fetch the active organization entity
|
|
var organization = await _context.Organizations.FirstOrDefaultAsync(o => o.Id == id && o.IsActive);
|
|
if (organization == null)
|
|
{
|
|
_logger.LogWarning("Organization with Id {OrganizationId} not found or inactive.", id);
|
|
return ApiResponse<object>.ErrorResponse("Organization not found", "Organization not found", 404);
|
|
}
|
|
|
|
// Update basic organization fields
|
|
organization.Name = model.Name;
|
|
organization.ContactPerson = model.ContactPerson;
|
|
organization.Address = model.Address;
|
|
organization.GSTNumber = model.GSTNumber;
|
|
organization.ContactNumber = model.ContactNumber;
|
|
organization.UpdatedById = loggedInEmployee.Id;
|
|
organization.UpdatedAt = DateTime.UtcNow;
|
|
|
|
//// Fetch the primary active employee of the organization
|
|
//var employee = await _context.Employees.FirstOrDefaultAsync(e => e.OrganizationId == id && e.IsPrimary && e.IsActive);
|
|
//if (employee == null)
|
|
//{
|
|
// _logger.LogWarning("Primary employee not found for OrganizationId: {OrganizationId}", id);
|
|
// return ApiResponse<object>.ErrorResponse("Primary employee not found", "Primary employee not found", 404);
|
|
//}
|
|
|
|
//// Split contact person's name into first and last names
|
|
//var fullName = (model.ContactPerson ?? string.Empty).Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
//employee.FirstName = fullName.Length > 0 ? fullName[0] : string.Empty;
|
|
//employee.LastName = fullName.Length > 1 ? fullName[^1] : string.Empty;
|
|
//employee.CurrentAddress = model.Address;
|
|
//employee.PermanentAddress = model.Address;
|
|
//employee.PhoneNumber = model.ContactNumber;
|
|
|
|
// Update organization's service mappings if service IDs are provided
|
|
if (model.ServiceIds?.Any() ?? false)
|
|
{
|
|
// Fetch existing service mappings (as no tracking for diff logic)
|
|
var orgServiceMappings = await _context.OrgServiceMappings
|
|
.AsNoTracking()
|
|
.Where(os => os.OrganizationId == id)
|
|
.ToListAsync();
|
|
|
|
var existedServiceIds = orgServiceMappings.Select(os => os.ServiceId).ToList();
|
|
|
|
// Determine new service mappings to add
|
|
var newServiceIds = model.ServiceIds.Except(existedServiceIds).ToList();
|
|
var orgServicesToDelete = orgServiceMappings
|
|
.Where(s => !model.ServiceIds.Contains(s.ServiceId))
|
|
.ToList();
|
|
|
|
// Add new service mappings
|
|
if (newServiceIds.Any())
|
|
{
|
|
var newMappings = newServiceIds.Select(sid => new OrgServiceMapping
|
|
{
|
|
OrganizationId = id,
|
|
ServiceId = sid
|
|
});
|
|
await _context.OrgServiceMappings.AddRangeAsync(newMappings);
|
|
}
|
|
|
|
// Remove deleted service mappings
|
|
if (orgServicesToDelete.Any())
|
|
{
|
|
_context.OrgServiceMappings.RemoveRange(orgServicesToDelete);
|
|
}
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
var response = _mapper.Map<OrganizationVM>(organization);
|
|
|
|
var createdByEmployee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == organization.CreatedById);
|
|
response.CreatedBy = _mapper.Map<BasicEmployeeVM>(createdByEmployee);
|
|
response.UpdatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
|
|
|
|
_logger.LogInfo("Successfully updated organization OrganizationId: {OrganizationId} by EmployeeId: {EmployeeId}",
|
|
id, loggedInEmployee.Id);
|
|
|
|
return ApiResponse<object>.SuccessResponse(response, "Organization updated Successfully", 200);
|
|
}
|
|
catch (DbUpdateException dbEx)
|
|
{
|
|
_logger.LogError(dbEx, "Database exception occurred while updating OrganizationId: {OrganizationId}", id);
|
|
return ApiResponse<object>.ErrorResponse("Internal error", "A database exception occurred", 500);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Unhandled exception occurred while updating OrganizationId: {OrganizationId}", id);
|
|
return ApiResponse<object>.ErrorResponse("Internal error", "An internal exception occurred", 500);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region =================================================================== Delete Functions ===================================================================
|
|
|
|
//public async Task<ApiResponse<object>> DeleteServiceProviderAsync(Guid id, bool active, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
|
|
//{
|
|
// using var scope = _serviceScope.CreateScope();
|
|
|
|
// var message = active ? "Restore" : "Delete";
|
|
|
|
// var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
|
// var client = await _context.Clients.FirstOrDefaultAsync(c => c.PrimaryEmployeeId == loggedInEmployee.Id && c.TenantId == tenantId);
|
|
|
|
// if (!(loggedInEmployee.ApplicationUser?.IsRootUser ?? false) && !loggedInEmployee.IsPrimary && client == null)
|
|
// {
|
|
// return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", $"You do not have permission to {message}d service provider.", 403));
|
|
// }
|
|
|
|
// var serviceProvider = await _context.ServiceProviders.FirstOrDefaultAsync(sp => sp.Id == id);
|
|
// if (serviceProvider == null)
|
|
// {
|
|
// return NotFound(ApiResponse<object>.ErrorResponse("Service Provider not Found", "Service Provider not Found in database", 404));
|
|
// }
|
|
// if (serviceProvider.IsActive == active)
|
|
// {
|
|
// return BadRequest(ApiResponse<object>.ErrorResponse($"Service Provider is already {message}d", $"Service Provider is already {message}d", 400));
|
|
// }
|
|
|
|
// var employeeIds = await _context.Employees.Where(e => e.ServiceProviderId == id).Select(e => e.Id).ToListAsync();
|
|
|
|
// var isPendingTask = await _context.TaskMembers.AnyAsync(tm => employeeIds.Contains(tm.EmployeeId));
|
|
|
|
// if (isPendingTask && !active)
|
|
// {
|
|
// return BadRequest(ApiResponse<object>.ErrorResponse("There is an unfinshed task, Service provider cannot be deleted", "There is an unfinshed task, Service provider cannot be deleted", 400));
|
|
// }
|
|
|
|
// serviceProvider.IsActive = active;
|
|
|
|
// if (!active)
|
|
// {
|
|
// var servicePeroviderTenant = await _context.ServiceProviderTenantMappings.AsNoTracking().Where(spt => spt.ServiceProviderId == id && spt.IsActive).ToListAsync();
|
|
// var newServiceProviderTenant = servicePeroviderTenant.Select(spt =>
|
|
// {
|
|
// spt.IsActive = false;
|
|
// return spt;
|
|
|
|
// }).ToList();
|
|
// _context.ServiceProviderTenantMappings.UpdateRange(newServiceProviderTenant);
|
|
// }
|
|
|
|
// await _context.SaveChangesAsync();
|
|
// return Ok(ApiResponse<object>.SuccessResponse(new { }, $"Service Provider is {message}d", 200));
|
|
//}
|
|
|
|
#endregion
|
|
|
|
#region =================================================================== Helper Functions ===================================================================
|
|
|
|
private async Task AssignApplicationRoleToOrganization(Guid organizationId, Guid tenantId, Guid loggedOrganizationId)
|
|
{
|
|
if (loggedOrganizationId == organizationId)
|
|
{
|
|
return;
|
|
}
|
|
using var scope = _serviceScope.CreateScope();
|
|
|
|
var rootEmployee = await _context.Employees
|
|
.Include(e => e.ApplicationUser)
|
|
.FirstOrDefaultAsync(e => e.ApplicationUser != null && e.ApplicationUser.IsRootUser.HasValue && e.ApplicationUser.IsRootUser.Value && e.OrganizationId == organizationId && e.IsPrimary);
|
|
if (rootEmployee == null)
|
|
{
|
|
return;
|
|
}
|
|
string serviceProviderRoleName = "Service Provider Role";
|
|
|
|
var serviceProviderRole = await _context.ApplicationRoles.FirstOrDefaultAsync(ar => ar.Role == serviceProviderRoleName && ar.TenantId == tenantId);
|
|
if (serviceProviderRole == null)
|
|
{
|
|
serviceProviderRole = new Model.Roles.ApplicationRole
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Role = serviceProviderRoleName,
|
|
Description = serviceProviderRoleName,
|
|
IsSystem = true,
|
|
TenantId = tenantId
|
|
};
|
|
_context.ApplicationRoles.Add(serviceProviderRole);
|
|
|
|
var rolePermissionMappigs = new List<RolePermissionMappings> {
|
|
new RolePermissionMappings
|
|
{
|
|
ApplicationRoleId = serviceProviderRole.Id,
|
|
FeaturePermissionId = PermissionsMaster.ViewProject
|
|
},
|
|
new RolePermissionMappings
|
|
{
|
|
ApplicationRoleId = serviceProviderRole.Id,
|
|
FeaturePermissionId = PermissionsMaster.ViewProjectInfra
|
|
},
|
|
new RolePermissionMappings
|
|
{
|
|
ApplicationRoleId = serviceProviderRole.Id,
|
|
FeaturePermissionId = PermissionsMaster.ViewTask
|
|
},
|
|
new RolePermissionMappings
|
|
{
|
|
ApplicationRoleId = serviceProviderRole.Id,
|
|
FeaturePermissionId = PermissionsMaster.ViewAllEmployees
|
|
},
|
|
new RolePermissionMappings
|
|
{
|
|
ApplicationRoleId = serviceProviderRole.Id,
|
|
FeaturePermissionId = PermissionsMaster.TeamAttendance
|
|
},
|
|
new RolePermissionMappings
|
|
{
|
|
ApplicationRoleId = serviceProviderRole.Id,
|
|
FeaturePermissionId = PermissionsMaster.AssignRoles
|
|
},
|
|
new RolePermissionMappings
|
|
{
|
|
ApplicationRoleId = serviceProviderRole.Id,
|
|
FeaturePermissionId = PermissionsMaster.ManageProjectInfra
|
|
},
|
|
new RolePermissionMappings
|
|
{
|
|
ApplicationRoleId = serviceProviderRole.Id,
|
|
FeaturePermissionId = PermissionsMaster.AssignAndReportProgress
|
|
},
|
|
new RolePermissionMappings
|
|
{
|
|
ApplicationRoleId = serviceProviderRole.Id,
|
|
FeaturePermissionId = PermissionsMaster.AddAndEditTask
|
|
}
|
|
};
|
|
_context.RolePermissionMappings.AddRange(rolePermissionMappigs);
|
|
}
|
|
_context.EmployeeRoleMappings.Add(new EmployeeRoleMapping
|
|
{
|
|
EmployeeId = rootEmployee.Id,
|
|
RoleId = serviceProviderRole.Id,
|
|
IsEnabled = true,
|
|
TenantId = tenantId
|
|
});
|
|
|
|
var _cache = scope.ServiceProvider.GetRequiredService<CacheUpdateHelper>();
|
|
await _cache.ClearAllPermissionIdsByEmployeeID(rootEmployee.Id, tenantId);
|
|
}
|
|
#endregion
|
|
}
|
|
}
|