From 7928c6ca36d35913a17f50c095d05d1537a41cdd Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Tue, 11 Nov 2025 11:06:09 +0530 Subject: [PATCH] Added the OrganizationService and IOrganizationService --- .../Data/ApplicationDbContext.cs | 2 + .../OrganizationModel/OrgHierarchyLog.cs | 29 + .../OrganizationHierarchy.cs | 30 + .../Controllers/OrganizationController.cs | 959 +--------------- Marco.Pms.Services/Program.cs | 1 + .../Service/OrganizationService.cs | 1002 +++++++++++++++++ .../ServiceInterfaces/IOrganizationService.cs | 28 + 7 files changed, 1142 insertions(+), 909 deletions(-) create mode 100644 Marco.Pms.Model/OrganizationModel/OrgHierarchyLog.cs create mode 100644 Marco.Pms.Model/OrganizationModel/OrganizationHierarchy.cs create mode 100644 Marco.Pms.Services/Service/OrganizationService.cs create mode 100644 Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs diff --git a/Marco.Pms.DataAccess/Data/ApplicationDbContext.cs b/Marco.Pms.DataAccess/Data/ApplicationDbContext.cs index 946774c..259d455 100644 --- a/Marco.Pms.DataAccess/Data/ApplicationDbContext.cs +++ b/Marco.Pms.DataAccess/Data/ApplicationDbContext.cs @@ -203,6 +203,8 @@ namespace Marco.Pms.DataAccess.Data public DbSet TenantOrgMappings { get; set; } public DbSet OrgServiceMappings { get; set; } public DbSet ProjectOrgMappings { get; set; } + public DbSet OrganizationHierarchies { get; set; } + public DbSet OrgHierarchyLogs { get; set; } #endregion diff --git a/Marco.Pms.Model/OrganizationModel/OrgHierarchyLog.cs b/Marco.Pms.Model/OrganizationModel/OrgHierarchyLog.cs new file mode 100644 index 0000000..b5c3af5 --- /dev/null +++ b/Marco.Pms.Model/OrganizationModel/OrgHierarchyLog.cs @@ -0,0 +1,29 @@ +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Utilities; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Marco.Pms.Model.OrganizationModel +{ + public class OrgHierarchyLog : TenantRelation + { + public Guid Id { get; set; } + public Guid OrganizationHierarchyId { get; set; } + + [ValidateNever] + [ForeignKey("OrganizationHierarchyId")] + public OrganizationHierarchy? OrganizationHierarchy { get; set; } + public DateTime AssignedAt { get; set; } + public Guid AssignedById { get; set; } + + [ValidateNever] + [ForeignKey("AssignedById")] + public Employee? AssignedBy { get; set; } + public DateTime? ReAssignedAt { get; set; } + public Guid? ReAssignedById { get; set; } + + [ValidateNever] + [ForeignKey("ReAssignedById")] + public Employee? ReAssignedBy { get; set; } + } +} diff --git a/Marco.Pms.Model/OrganizationModel/OrganizationHierarchy.cs b/Marco.Pms.Model/OrganizationModel/OrganizationHierarchy.cs new file mode 100644 index 0000000..592e1c9 --- /dev/null +++ b/Marco.Pms.Model/OrganizationModel/OrganizationHierarchy.cs @@ -0,0 +1,30 @@ +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Utilities; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Marco.Pms.Model.OrganizationModel +{ + public class OrganizationHierarchy : TenantRelation + { + public Guid Id { get; set; } + public Guid EmployeeId { get; set; } + + [ValidateNever] + [ForeignKey("EmployeeId")] + public Employee? Employee { get; set; } + public Guid ReportToId { get; set; } + + [ValidateNever] + [ForeignKey("ReportToId")] + public Employee? ReportTo { get; set; } + public bool IsPrimary { get; set; } + public bool IsActive { get; set; } + public DateTime AssignedAt { get; set; } + public Guid AssignedById { get; set; } + + [ValidateNever] + [ForeignKey("AssignedById")] + public Employee? AssignedBy { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/OrganizationController.cs b/Marco.Pms.Services/Controllers/OrganizationController.cs index 697e06d..f3c48b3 100644 --- a/Marco.Pms.Services/Controllers/OrganizationController.cs +++ b/Marco.Pms.Services/Controllers/OrganizationController.cs @@ -1,23 +1,10 @@ -using AutoMapper; -using Marco.Pms.DataAccess.Data; +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; +using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Helpers; -using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using System.Net; namespace Marco.Pms.Services.Controllers { @@ -28,265 +15,50 @@ namespace Marco.Pms.Services.Controllers { private readonly IDbContextFactory _dbContextFactory; private readonly IServiceScopeFactory _serviceScope; + private readonly IOrganizationService _organizationService; private readonly UserHelper _userHelper; + private readonly ISignalRService _signalR; private readonly Guid tenantId; - private readonly IMapper _mapper; private readonly Guid loggedOrganizationId; - 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 OrganizationController(IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScope, - UserHelper userHelper, - ILoggingService logger, - IMapper mapper) + IOrganizationService organizationService, + ISignalRService signalR, + UserHelper userHelper) { _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope)); + _organizationService = organizationService ?? throw new ArgumentNullException(nameof(organizationService)); + _signalR = signalR ?? throw new ArgumentNullException(nameof(signalR)); _userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); loggedOrganizationId = _userHelper.GetCurrentOrganizationId(); tenantId = userHelper.GetTenantId(); } #region =================================================================== Get Functions =================================================================== [HttpGet("list")] - public async Task GetOrganizarionListAsync([FromQuery] string? searchString, [FromQuery] double? sprid, [FromQuery] bool active = true, + public async Task GetOrganizarionList([FromQuery] string? searchString, [FromQuery] double? sprid, [FromQuery] bool active = true, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20) { - _logger.LogDebug("Fetching organization list. SearchString: {SearchString}, SPRID: {SPRID}, Active: {Active}, Page: {PageNumber}, Size: {PageSize}", - searchString ?? "", sprid ?? 0, active, pageNumber, pageSize); - - await using var _context = await _dbContextFactory.CreateDbContextAsync(); - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // Base query filtering by active status - IQueryable 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 NotFound(ApiResponse.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(o); - orgVm.CreatedBy = employees.Where(e => e.Id == o.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault(); - orgVm.UpdatedBy = employees.Where(e => e.Id == o.UpdatedById).Select(e => _mapper.Map(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 Ok(ApiResponse.SuccessResponse(response, "Successfully fetched the organization list", 200)); + var response = await _organizationService.GetOrganizarionListAsync(searchString, sprid, active, pageNumber, pageSize, loggedInEmployee, tenantId, loggedOrganizationId); + return StatusCode(response.StatusCode, response); } [HttpGet("details/{id}")] - public async Task GetOrganizationDetailsAsync(Guid id) + public async Task GetOrganizationDetails(Guid id) { - _logger.LogDebug("Started fetching details for OrganizationId: {OrganizationId}", id); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _organizationService.GetOrganizationDetailsAsync(id, loggedInEmployee, tenantId, loggedOrganizationId); + return StatusCode(response.StatusCode, response); + } - await using var _context = await _dbContextFactory.CreateDbContextAsync(); - try - { - // Get the logged-in employee (for filter/permission checks) - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // 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 NotFound(ApiResponse.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 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(organization); - response.ActiveApplicationUserCount = activeApplicationUserCount; - response.ActiveEmployeeCount = activeEmployeeCount; - response.CreatedBy = _mapper.Map(createdByEmployee); - response.UpdatedBy = _mapper.Map(updatedByEmployee); - response.Projects = _mapper.Map>(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 Ok(ApiResponse.SuccessResponse(response, "Successfully fetched the organization details", 200)); - } - catch (DbUpdateException dbEx) - { - _logger.LogError(dbEx, "Database exception while fetching details for OrganizationId: {OrganizationId}", id); - return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "A database exception occurred", 500)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unhandled exception while fetching details for OrganizationId: {OrganizationId}", id); - return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An internal exception occurred", 500)); - } + [HttpGet("hierarchy/list/{employeeId}")] + public async Task GetOrganizationHierarchyList(Guid employeeId) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _organizationService.GetOrganizationHierarchyListAsync(employeeId, loggedInEmployee, tenantId, loggedOrganizationId); + return StatusCode(response.StatusCode, response); } #endregion @@ -294,478 +66,42 @@ namespace Marco.Pms.Services.Controllers #region =================================================================== Post Functions =================================================================== [HttpPost("create")] - public async Task CreateOrganizationAsync([FromBody] CreateOrganizationDto model) + public async Task CreateOrganization([FromBody] CreateOrganizationDto model) { - await using var _context = await _dbContextFactory.CreateDbContextAsync(); - using var scope = _serviceScope.CreateScope(); // Create scope for scoped services - await using var transaction = await _context.Database.BeginTransactionAsync(); - - try + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _organizationService.CreateOrganizationAsync(model, loggedInEmployee, tenantId, loggedOrganizationId); + if (response.Success) { - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // Concurrent permission check and organization existence check - var hasPermissionTask = Task.Run(async () => - { - var permissionService = scope.ServiceProvider.GetRequiredService(); - 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 StatusCode(403, ApiResponse.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(); - double lastSPRID = lastOrganization?.SPRID ?? 5400; - - // Map DTO to entity and set defaults - Organization organization = _mapper.Map(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(); - var emailSender = scope.ServiceProvider.GetRequiredService(); - var userManager = scope.ServiceProvider.GetRequiredService>(); - - // 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 BadRequest(ApiResponse.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(); - - // Send user registration email with password reset link - var token = await userManager.GeneratePasswordResetTokenAsync(user); - var resetLink = $"{configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; - if (!string.IsNullOrEmpty(newEmployee.FirstName)) - { - await emailSender.SendResetPasswordEmailOnRegister(user.Email, newEmployee.FirstName, resetLink); - } - - // Prepare response DTO - var response = _mapper.Map(organization); - response.CreatedBy = _mapper.Map(loggedInEmployee); - - return Ok(ApiResponse.SuccessResponse(response, "Successfully created the organization", 200)); - } - catch (DbUpdateException dbEx) - { - await transaction.RollbackAsync(); - _logger.LogError(dbEx, "Database exception occurred while creating organization"); - return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "A database exception occurred", 500)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unexpected exception occurred while creating organization"); - return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An unexpected error occurred", 500)); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Organization_Management", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } [HttpPost("assign/project")] - public async Task AssignOrganizationToProjectAsync([FromBody] AssignOrganizationDto model) + public async Task AssignOrganizationToProject([FromBody] AssignOrganizationDto model) { - _logger.LogDebug("Started assigning organization {OrganizationId} to project {ProjectId} with service IDs {@ServiceIds}", - model.OrganizationId, model.ProjectId, model.ServiceIds); - - // Create DbContext for the method scope - await using var _context = await _dbContextFactory.CreateDbContextAsync(); - - // Begin a database transaction - await using var transaction = await _context.Database.BeginTransactionAsync(); - - try + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _organizationService.AssignOrganizationToProjectAsync(model, loggedInEmployee, tenantId, loggedOrganizationId); + if (response.Success) { - // Get currently logged in employee - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - 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 NotFound(ApiResponse.ErrorResponse("Organization not found", "Organization not found in database", 404)); - } - if (project == null) - { - _logger.LogWarning("Project with ID {ProjectId} not found.", model.ProjectId); - return NotFound(ApiResponse.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 NotFound(ApiResponse.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 StatusCode(403, ApiResponse.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 StatusCode(403, ApiResponse.ErrorResponse("Access Denied", "You don't have access to assign this type of organization", 403)); - } - - var newProjectOrgMappings = new List(); - var newProjectServiceMappings = new List(); - - // 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 NotFound(ApiResponse.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 Conflict(ApiResponse.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(organization); - var parentOrganizationVm = _mapper.Map(parentOrganization); - var projectVm = _mapper.Map(project); - - var response = services.Select(s => new AssignOrganizationVm - { - Project = projectVm, - OrganizationType = organizationType, - Organization = organizationVm, - ParentOrganization = parentOrganizationVm, - Service = _mapper.Map(s) - }).ToList(); - - await AssignApplicationRoleToOrganization(organization.Id, project.TenantId); - - return Ok(ApiResponse.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 StatusCode(500, ApiResponse.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 StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An internal exception occurred", 500)); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Organization_Management", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } [HttpPost("assign/tenant/{organizationId}")] public async Task AssignOrganizationToTenantAsync(Guid organizationId) { - _logger.LogInfo("Started assigning organization {OrganizationId} to tenant {TenantId}", organizationId, tenantId); - - // Create a DbContext instance for this method scope - await using var _context = await _dbContextFactory.CreateDbContextAsync(); - - // Begin a database transaction - await using var transaction = await _context.Database.BeginTransactionAsync(); - - try + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _organizationService.AssignOrganizationToTenantAsync(organizationId, loggedInEmployee, tenantId, loggedOrganizationId); + if (response.Success) { - // Get currently logged in employee for auditing purposes - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // 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 NotFound(ApiResponse.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 StatusCode(409, ApiResponse.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(organization); - - await AssignApplicationRoleToOrganization(organization.Id, tenantId); - - return Ok(ApiResponse.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 StatusCode(500, ApiResponse.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 StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An internal exception has occurred", 500)); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Organization_Management", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } #endregion @@ -773,117 +109,16 @@ namespace Marco.Pms.Services.Controllers #region =================================================================== Put Functions =================================================================== [HttpPut("edit/{id}")] - public async Task UpdateOrganiationAsync(Guid id, [FromBody] UpdateOrganizationDto model) + public async Task UpdateOrganiation(Guid id, [FromBody] UpdateOrganizationDto model) { - await using var _context = await _dbContextFactory.CreateDbContextAsync(); - - try + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _organizationService.UpdateOrganiationAsync(id, model, loggedInEmployee, tenantId, loggedOrganizationId); + if (response.Success) { - // Get the current logged-in employee - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - _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 StatusCode(403, ApiResponse.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 NotFound(ApiResponse.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.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 NotFound(ApiResponse.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(organization); - - var createdByEmployee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == organization.CreatedById); - response.CreatedBy = _mapper.Map(createdByEmployee); - response.UpdatedBy = _mapper.Map(loggedInEmployee); - - _logger.LogInfo("Successfully updated organization OrganizationId: {OrganizationId} by EmployeeId: {EmployeeId}", - id, loggedInEmployee.Id); - - return Ok(ApiResponse.SuccessResponse(response, "Organization updated Successfully", 200)); - } - catch (DbUpdateException dbEx) - { - _logger.LogError(dbEx, "Database exception occurred while updating OrganizationId: {OrganizationId}", id); - return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "A database exception occurred", 500)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Unhandled exception occurred while updating OrganizationId: {OrganizationId}", id); - return StatusCode(500, ApiResponse.ErrorResponse("Internal error", "An internal exception occurred", 500)); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Organization_Management", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } #endregion @@ -944,99 +179,5 @@ namespace Marco.Pms.Services.Controllers //} #endregion - #region =================================================================== Helper Functions =================================================================== - - private async Task AssignApplicationRoleToOrganization(Guid organizationId, Guid tenantId) - { - if (loggedOrganizationId == organizationId) - { - return; - } - await using var _context = await _dbContextFactory.CreateDbContextAsync(); - 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 { - 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(); - await _cache.ClearAllPermissionIdsByEmployeeID(rootEmployee.Id, tenantId); - } - #endregion } } diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 44da1a4..6014953 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -185,6 +185,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); #endregion diff --git a/Marco.Pms.Services/Service/OrganizationService.cs b/Marco.Pms.Services/Service/OrganizationService.cs new file mode 100644 index 0000000..c4ec346 --- /dev/null +++ b/Marco.Pms.Services/Service/OrganizationService.cs @@ -0,0 +1,1002 @@ +using AutoMapper; +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.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using System.Net; + +namespace Marco.Pms.Services.Service +{ + public class OrganizationService : IOrganizationService + { + private readonly IDbContextFactory _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 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> GetOrganizarionListAsync(string? searchString, double? 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 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.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(o); + orgVm.CreatedBy = employees.Where(e => e.Id == o.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault(); + orgVm.UpdatedBy = employees.Where(e => e.Id == o.UpdatedById).Select(e => _mapper.Map(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.SuccessResponse(response, "Successfully fetched the organization list", 200); + } + public async Task> 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.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 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(organization); + response.ActiveApplicationUserCount = activeApplicationUserCount; + response.ActiveEmployeeCount = activeEmployeeCount; + response.CreatedBy = _mapper.Map(createdByEmployee); + response.UpdatedBy = _mapper.Map(updatedByEmployee); + response.Projects = _mapper.Map>(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.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.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.ErrorResponse("Internal error", "An internal exception occurred", 500); + } + } + public async Task> GetOrganizationHierarchyListAsync(Guid employeeId, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId) + { + return ApiResponse.SuccessResponse(new { }); + } + + #endregion + + #region =================================================================== Post Functions =================================================================== + + public async Task> 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(); + 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.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(); + double lastSPRID = lastOrganization?.SPRID ?? 5400; + + // Map DTO to entity and set defaults + Organization organization = _mapper.Map(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(); + var emailSender = scope.ServiceProvider.GetRequiredService(); + var userManager = scope.ServiceProvider.GetRequiredService>(); + + // 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.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(); + + // Send user registration email with password reset link + var token = await userManager.GeneratePasswordResetTokenAsync(user); + var resetLink = $"{configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}"; + if (!string.IsNullOrEmpty(newEmployee.FirstName)) + { + await emailSender.SendResetPasswordEmailOnRegister(user.Email, newEmployee.FirstName, resetLink); + } + + // Prepare response DTO + var response = _mapper.Map(organization); + response.CreatedBy = _mapper.Map(loggedInEmployee); + + return ApiResponse.SuccessResponse(response, "Successfully created the organization", 200); + } + catch (DbUpdateException dbEx) + { + await transaction.RollbackAsync(); + _logger.LogError(dbEx, "Database exception occurred while creating organization"); + return ApiResponse.ErrorResponse("Internal error", "A database exception occurred", 500); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected exception occurred while creating organization"); + return ApiResponse.ErrorResponse("Internal error", "An unexpected error occurred", 500); + } + } + public async Task> 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.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.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.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.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.ErrorResponse("Access Denied", "You don't have access to assign this type of organization", 403); + } + + var newProjectOrgMappings = new List(); + var newProjectServiceMappings = new List(); + + // 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.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.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(organization); + var parentOrganizationVm = _mapper.Map(parentOrganization); + var projectVm = _mapper.Map(project); + + var response = services.Select(s => new AssignOrganizationVm + { + Project = projectVm, + OrganizationType = organizationType, + Organization = organizationVm, + ParentOrganization = parentOrganizationVm, + Service = _mapper.Map(s) + }).ToList(); + + await AssignApplicationRoleToOrganization(organization.Id, project.TenantId, loggedOrganizationId); + + return ApiResponse.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.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.ErrorResponse("Internal error", "An internal exception occurred", 500); + } + } + public async Task> 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.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.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(organization); + + await AssignApplicationRoleToOrganization(organization.Id, tenantId, loggedOrganizationId); + + return ApiResponse.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.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.ErrorResponse("Internal error", "An internal exception has occurred", 500); + } + } + + #endregion + + #region =================================================================== Put Functions =================================================================== + + public async Task> 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.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.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.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.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(organization); + + var createdByEmployee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == organization.CreatedById); + response.CreatedBy = _mapper.Map(createdByEmployee); + response.UpdatedBy = _mapper.Map(loggedInEmployee); + + _logger.LogInfo("Successfully updated organization OrganizationId: {OrganizationId} by EmployeeId: {EmployeeId}", + id, loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(response, "Organization updated Successfully", 200); + } + catch (DbUpdateException dbEx) + { + _logger.LogError(dbEx, "Database exception occurred while updating OrganizationId: {OrganizationId}", id); + return ApiResponse.ErrorResponse("Internal error", "A database exception occurred", 500); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled exception occurred while updating OrganizationId: {OrganizationId}", id); + return ApiResponse.ErrorResponse("Internal error", "An internal exception occurred", 500); + } + } + + #endregion + + #region =================================================================== Delete Functions =================================================================== + + //public async Task> 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.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.ErrorResponse("Service Provider not Found", "Service Provider not Found in database", 404)); + // } + // if (serviceProvider.IsActive == active) + // { + // return BadRequest(ApiResponse.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.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.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 { + 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(); + await _cache.ClearAllPermissionIdsByEmployeeID(rootEmployee.Id, tenantId); + } + #endregion + } +} diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs new file mode 100644 index 0000000..c70ce37 --- /dev/null +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs @@ -0,0 +1,28 @@ +using Marco.Pms.Model.Dtos.Organization; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Utilities; + +namespace Marco.Pms.Services.Service.ServiceInterfaces +{ + public interface IOrganizationService + { + #region =================================================================== Get Functions =================================================================== + Task> GetOrganizarionListAsync(string? searchString, double? sprid, bool active, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); + Task> GetOrganizationDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); + Task> GetOrganizationHierarchyListAsync(Guid employeeId, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); + #endregion + + #region =================================================================== Post Functions =================================================================== + 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); + #endregion + + #region =================================================================== Put Functions =================================================================== + Task> UpdateOrganiationAsync(Guid id, UpdateOrganizationDto model, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); + #endregion + + #region =================================================================== Delete Functions =================================================================== + #endregion + } +}