From eabd31f8cfe529c64d153d7431052f7746f37666 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 16 Jul 2025 18:15:43 +0530 Subject: [PATCH] Optimized the Manage infra API in Project Controller --- Marco.Pms.CacheHelper/ProjectCache.cs | 7 + .../{BuildingDot.cs => BuildingDto.cs} | 2 +- .../Projects/{FloorDot.cs => FloorDto.cs} | 2 +- Marco.Pms.Model/Dtos/Projects/InfraDot.cs | 9 - Marco.Pms.Model/Dtos/Projects/InfraDto.cs | 9 + .../{WorkAreaDot.cs => WorkAreaDto.cs} | 2 +- Marco.Pms.Model/Mapper/InfraMapper.cs | 6 +- Marco.Pms.Model/Utilities/ServiceResponse.cs | 8 + .../Controllers/ProjectController.cs | 154 +---- .../Helpers/CacheUpdateHelper.cs | 12 + .../MappingProfiles/MappingProfile.cs | 3 + Marco.Pms.Services/Service/ProjectServices.cs | 612 ++++++++++++------ .../ServiceInterfaces/IProjectServices.cs | 1 + 13 files changed, 488 insertions(+), 339 deletions(-) rename Marco.Pms.Model/Dtos/Projects/{BuildingDot.cs => BuildingDto.cs} (92%) rename Marco.Pms.Model/Dtos/Projects/{FloorDot.cs => FloorDto.cs} (92%) delete mode 100644 Marco.Pms.Model/Dtos/Projects/InfraDot.cs create mode 100644 Marco.Pms.Model/Dtos/Projects/InfraDto.cs rename Marco.Pms.Model/Dtos/Projects/{WorkAreaDot.cs => WorkAreaDto.cs} (91%) create mode 100644 Marco.Pms.Model/Utilities/ServiceResponse.cs diff --git a/Marco.Pms.CacheHelper/ProjectCache.cs b/Marco.Pms.CacheHelper/ProjectCache.cs index 833e1a0..9417724 100644 --- a/Marco.Pms.CacheHelper/ProjectCache.cs +++ b/Marco.Pms.CacheHelper/ProjectCache.cs @@ -95,6 +95,13 @@ namespace Marco.Pms.CacheHelper var result = await _projetCollection.DeleteOneAsync(filter); return result.DeletedCount > 0; } + public async Task RemoveProjectsFromCacheAsync(List projectIds) + { + var stringIds = projectIds.Select(id => id.ToString()).ToList(); + var filter = Builders.Filter.In(p => p.Id, stringIds); + var result = await _projetCollection.DeleteManyAsync(filter); + return result.DeletedCount > 0; + } // ------------------------------------------------------- Project InfraStructure ------------------------------------------------------- diff --git a/Marco.Pms.Model/Dtos/Projects/BuildingDot.cs b/Marco.Pms.Model/Dtos/Projects/BuildingDto.cs similarity index 92% rename from Marco.Pms.Model/Dtos/Projects/BuildingDot.cs rename to Marco.Pms.Model/Dtos/Projects/BuildingDto.cs index a5b160b..e6a7b89 100644 --- a/Marco.Pms.Model/Dtos/Projects/BuildingDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/BuildingDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace Marco.Pms.Model.Dtos.Project { - public class BuildingDot + public class BuildingDto { [Key] public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Dtos/Projects/FloorDot.cs b/Marco.Pms.Model/Dtos/Projects/FloorDto.cs similarity index 92% rename from Marco.Pms.Model/Dtos/Projects/FloorDot.cs rename to Marco.Pms.Model/Dtos/Projects/FloorDto.cs index a3d1c86..3dbe06f 100644 --- a/Marco.Pms.Model/Dtos/Projects/FloorDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/FloorDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace Marco.Pms.Model.Dtos.Project { - public class FloorDot + public class FloorDto { public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Dtos/Projects/InfraDot.cs b/Marco.Pms.Model/Dtos/Projects/InfraDot.cs deleted file mode 100644 index 7c16c09..0000000 --- a/Marco.Pms.Model/Dtos/Projects/InfraDot.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Marco.Pms.Model.Dtos.Project -{ - public class InfraDot - { - public BuildingDot? Building { get; set; } - public FloorDot? Floor { get; set; } - public WorkAreaDot? WorkArea { get; set; } - } -} diff --git a/Marco.Pms.Model/Dtos/Projects/InfraDto.cs b/Marco.Pms.Model/Dtos/Projects/InfraDto.cs new file mode 100644 index 0000000..09d1462 --- /dev/null +++ b/Marco.Pms.Model/Dtos/Projects/InfraDto.cs @@ -0,0 +1,9 @@ +namespace Marco.Pms.Model.Dtos.Project +{ + public class InfraDto + { + public BuildingDto? Building { get; set; } + public FloorDto? Floor { get; set; } + public WorkAreaDto? WorkArea { get; set; } + } +} diff --git a/Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs b/Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs similarity index 91% rename from Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs rename to Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs index 604ee3e..ffc80c4 100644 --- a/Marco.Pms.Model/Dtos/Projects/WorkAreaDot.cs +++ b/Marco.Pms.Model/Dtos/Projects/WorkAreaDto.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations; namespace Marco.Pms.Model.Dtos.Project { - public class WorkAreaDot + public class WorkAreaDto { [Key] public Guid? Id { get; set; } diff --git a/Marco.Pms.Model/Mapper/InfraMapper.cs b/Marco.Pms.Model/Mapper/InfraMapper.cs index 89097d1..5364494 100644 --- a/Marco.Pms.Model/Mapper/InfraMapper.cs +++ b/Marco.Pms.Model/Mapper/InfraMapper.cs @@ -5,7 +5,7 @@ namespace Marco.Pms.Model.Mapper { public static class BuildingMapper { - public static Building ToBuildingFromBuildingDto(this BuildingDot model, Guid tenantId) + public static Building ToBuildingFromBuildingDto(this BuildingDto model, Guid tenantId) { return new Building { @@ -20,7 +20,7 @@ namespace Marco.Pms.Model.Mapper public static class FloorMapper { - public static Floor ToFloorFromFloorDto(this FloorDot model, Guid tenantId) + public static Floor ToFloorFromFloorDto(this FloorDto model, Guid tenantId) { return new Floor { @@ -34,7 +34,7 @@ namespace Marco.Pms.Model.Mapper public static class WorAreaMapper { - public static WorkArea ToWorkAreaFromWorkAreaDto(this WorkAreaDot model, Guid tenantId) + public static WorkArea ToWorkAreaFromWorkAreaDto(this WorkAreaDto model, Guid tenantId) { return new WorkArea { diff --git a/Marco.Pms.Model/Utilities/ServiceResponse.cs b/Marco.Pms.Model/Utilities/ServiceResponse.cs new file mode 100644 index 0000000..a76c45c --- /dev/null +++ b/Marco.Pms.Model/Utilities/ServiceResponse.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.Utilities +{ + public class ServiceResponse + { + public object? Notification { get; set; } + public ApiResponse Response { get; set; } = ApiResponse.ErrorResponse(""); + } +} diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index a10fc66..71ef1a5 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,10 +1,8 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; -using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service; using Marco.Pms.Services.Service.ServiceInterfaces; @@ -359,6 +357,30 @@ namespace MarcoBMS.Services.Controllers #region =================================================================== Project Infrastructre Manage APIs =================================================================== + [HttpPost("manage-infra")] + public async Task ManageProjectInfra(List infraDtos) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var serviceResponse = await _projectServices.ManageProjectInfraAsync(infraDtos, tenantId, loggedInEmployee); + var response = serviceResponse.Response; + var notification = serviceResponse.Notification; + if (notification != null) + { + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + + } + [HttpPost("task")] public async Task CreateProjectTask([FromBody] List workItemDtos) { @@ -439,134 +461,6 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); } - [HttpPost("manage-infra")] - public async Task ManageProjectInfra(List infraDots) - { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - var responseData = new InfraVM { }; - string responseMessage = ""; - string message = ""; - List projectIds = new List(); - if (infraDots != null) - { - foreach (var item in infraDots) - { - if (item.Building != null) - { - - Building building = item.Building.ToBuildingFromBuildingDto(tenantId); - building.TenantId = tenantId; - - if (item.Building.Id == null) - { - //create - _context.Buildings.Add(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Added Successfully"; - message = "Building Added"; - await _cache.AddBuildngInfra(building.ProjectId, building); - } - else - { - //update - _context.Buildings.Update(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Updated Successfully"; - message = "Building Updated"; - await _cache.UpdateBuildngInfra(building.ProjectId, building); - } - projectIds.Add(building.ProjectId); - } - if (item.Floor != null) - { - Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); - floor.TenantId = tenantId; - bool isCreated = false; - - if (item.Floor.Id == null) - { - //create - _context.Floor.Add(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Added Successfully"; - message = "Floor Added"; - isCreated = true; - } - else - { - //update - _context.Floor.Update(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Updated Successfully"; - message = "Floor Updated"; - } - Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - var projectId = building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {building?.Name}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, floor: floor); - } - else - { - await _cache.UpdateBuildngInfra(projectId, floor: floor); - } - } - if (item.WorkArea != null) - { - WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); - workArea.TenantId = tenantId; - bool isCreated = false; - - if (item.WorkArea.Id == null) - { - //create - _context.WorkAreas.Add(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Added Successfully"; - message = "Work Area Added"; - isCreated = true; - } - else - { - //update - _context.WorkAreas.Update(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Updated Successfully"; - message = "Work Area Updated"; - } - Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - var projectId = floor?.Building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - else - { - await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - } - } - message = $"{message} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}"; - var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - - await _signalR.SendNotificationAsync(notification); - return Ok(ApiResponse.SuccessResponse(responseData, responseMessage, 200)); - } - return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); - - } - #endregion } diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 9a01b83..b0b1e06 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -478,6 +478,18 @@ namespace Marco.Pms.Services.Helpers } } + public async Task RemoveProjectsAsync(List projectIds) + { + try + { + var response = await _projectCache.RemoveProjectsFromCacheAsync(projectIds); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occured while deleting project list from to Cache"); + + } + } // ------------------------------------ Project Infrastructure Cache --------------------------------------- diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 50d2ea9..bf3777c 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -51,6 +51,9 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); CreateMap() .ForMember( dest => dest.Description, diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 6d811fc..32e1285 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -1033,83 +1033,360 @@ namespace Marco.Pms.Services.Service #region =================================================================== Project Infrastructre Manage APIs =================================================================== - public async Task> CreateProjectTask1(List workItemDtos, Guid tenantId, Employee loggedInEmployee) + public async Task> ManageProjectInfra(List infraDots, Guid tenantId, Employee loggedInEmployee) { - _logger.LogInfo("CreateProjectTask called with {Count} items", workItemDtos?.Count ?? 0); - - // Validate request - if (workItemDtos == null || !workItemDtos.Any()) - { - _logger.LogWarning("No work items provided in the request."); - return ApiResponse.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400); - } - - var workItemsToCreate = new List(); - var workItemsToUpdate = new List(); - var responseList = new List(); + var responseData = new InfraVM { }; + string responseMessage = ""; string message = ""; - List workAreaIds = new List(); - var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); - var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync(); - - foreach (var itemDto in workItemDtos) + List projectIds = new List(); + if (infraDots != null) { - var workItem = _mapper.Map(itemDto); - workItem.TenantId = tenantId; - var workArea = await _context.WorkAreas.Include(a => a.Floor).FirstOrDefaultAsync(a => a.Id == workItem.WorkAreaId) ?? new WorkArea(); - - Building building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == (workArea.Floor != null ? workArea.Floor.BuildingId : Guid.Empty)) ?? new Building(); - - if (itemDto.Id != null && itemDto.Id != Guid.Empty) + foreach (var item in infraDots) { - // Update existing - workItemsToUpdate.Add(workItem); - message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - var existingWorkItem = workItems.FirstOrDefault(wi => wi.Id == workItem.Id); - if (existingWorkItem != null) + if (item.Building != null) { - double plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork; - double completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; - await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); + + Building building = _mapper.Map(item.Building); + building.TenantId = tenantId; + + if (item.Building.Id == null) + { + //create + _context.Buildings.Add(building); + await _context.SaveChangesAsync(); + responseData.building = building; + responseMessage = "Buliding Added Successfully"; + message = "Building Added"; + await _cache.AddBuildngInfra(building.ProjectId, building); + } + else + { + //update + _context.Buildings.Update(building); + await _context.SaveChangesAsync(); + responseData.building = building; + responseMessage = "Buliding Updated Successfully"; + message = "Building Updated"; + await _cache.UpdateBuildngInfra(building.ProjectId, building); + } + projectIds.Add(building.ProjectId); + } + if (item.Floor != null) + { + Floor floor = _mapper.Map(item.Floor); + floor.TenantId = tenantId; + bool isCreated = false; + + if (item.Floor.Id == null) + { + //create + _context.Floor.Add(floor); + await _context.SaveChangesAsync(); + responseData.floor = floor; + responseMessage = "Floor Added Successfully"; + message = "Floor Added"; + isCreated = true; + } + else + { + //update + _context.Floor.Update(floor); + await _context.SaveChangesAsync(); + responseData.floor = floor; + responseMessage = "Floor Updated Successfully"; + message = "Floor Updated"; + } + Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); + var projectId = building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); + message = $"{message} in Building: {building?.Name}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, floor: floor); + } + else + { + await _cache.UpdateBuildngInfra(projectId, floor: floor); + } + } + if (item.WorkArea != null) + { + WorkArea workArea = _mapper.Map(item.WorkArea); + workArea.TenantId = tenantId; + bool isCreated = false; + + if (item.WorkArea.Id == null) + { + //create + _context.WorkAreas.Add(workArea); + await _context.SaveChangesAsync(); + responseData.workArea = workArea; + responseMessage = "Work Area Added Successfully"; + message = "Work Area Added"; + isCreated = true; + } + else + { + //update + _context.WorkAreas.Update(workArea); + await _context.SaveChangesAsync(); + responseData.workArea = workArea; + responseMessage = "Work Area Updated Successfully"; + message = "Work Area Updated"; + } + Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); + var projectId = floor?.Building?.ProjectId ?? Guid.Empty; + projectIds.Add(projectId); + message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; + if (isCreated) + { + await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } + else + { + await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); + } } } - else + message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; + + return ApiResponse.SuccessResponse(responseData, responseMessage, 200); + } + return ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400); + + } + + public async Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee) + { + // 1. Guard Clause: Handle null or empty input gracefully. + if (infraDtos == null || !infraDtos.Any()) + { + return new ServiceResponse { - // Create new - workItem.Id = Guid.NewGuid(); - workItemsToCreate.Add(workItem); - message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, workItem.PlannedWork, workItem.CompletedWork); + Response = ApiResponse.ErrorResponse("Invalid details.", "No infrastructure details were provided.", 400) + }; + } + + var responseData = new InfraVM(); + var messages = new List(); + var projectIds = new HashSet(); // Use HashSet for automatic duplicate handling. + var cacheUpdateTasks = new List(); + + // --- Pre-fetch parent entities to avoid N+1 query problem --- + // 2. Gather all parent IDs needed for validation and context. + var requiredBuildingIds = infraDtos + .Where(i => i.Floor?.BuildingId != null) + .Select(i => i.Floor!.BuildingId) + .Distinct() + .ToList(); + + var requiredFloorIds = infraDtos + .Where(i => i.WorkArea?.FloorId != null) + .Select(i => i.WorkArea!.FloorId) + .Distinct() + .ToList(); + + // 3. Fetch all required parent entities in single batch queries. + var buildingsDict = await _context.Buildings + .Where(b => requiredBuildingIds.Contains(b.Id)) + .ToDictionaryAsync(b => b.Id); + + var floorsDict = await _context.Floor + .Include(f => f.Building) // Eagerly load Building for later use + .Where(f => requiredFloorIds.Contains(f.Id)) + .ToDictionaryAsync(f => f.Id); + // --- End Pre-fetching --- + + // 4. Process all entities and add them to the context's change tracker. + foreach (var item in infraDtos) + { + if (item.Building != null) + { + ProcessBuilding(item.Building, tenantId, responseData, messages, projectIds, cacheUpdateTasks); + } + if (item.Floor != null) + { + ProcessFloor(item.Floor, tenantId, responseData, messages, projectIds, cacheUpdateTasks, buildingsDict); + } + if (item.WorkArea != null) + { + ProcessWorkArea(item.WorkArea, tenantId, responseData, messages, projectIds, cacheUpdateTasks, floorsDict); + } + } + + // 5. Save all changes to the database in a single transaction. + var changedRecordCount = await _context.SaveChangesAsync(); + + // If no changes were actually made, we can exit early. + if (changedRecordCount == 0) + { + return new ServiceResponse + { + Response = ApiResponse.SuccessResponse(responseData, "No changes detected in the provided infrastructure details.", 200) + }; + } + + // 6. Execute all cache updates concurrently after the DB save is successful. + await Task.WhenAll(cacheUpdateTasks); + + // 7. Consolidate messages and create notification payload. + string finalResponseMessage = messages.LastOrDefault() ?? "Infrastructure managed successfully."; + string logMessage = $"{string.Join(", ", messages)} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds.ToList(), Message = logMessage }; + + // TODO: Dispatch the 'notification' object to your notification service. + + return new ServiceResponse + { + Notification = notification, + Response = ApiResponse.SuccessResponse(responseData, finalResponseMessage, 200) + }; + } + + /// + /// Manages a batch of infrastructure changes (creates/updates for Buildings, Floors, and WorkAreas). + /// This method is optimized to perform all database operations in a single, atomic transaction. + /// + public async Task> ManageProjectInfraAsync1(List infraDtos, Guid tenantId, Employee loggedInEmployee) + { + // --- Step 1: Input Validation --- + if (infraDtos == null || !infraDtos.Any()) + { + _logger.LogWarning("ManageProjectInfraAsync called with null or empty DTO list."); + return ApiResponse.ErrorResponse("Invalid details.", "Infrastructure data cannot be empty.", 400); + } + + _logger.LogInfo("Begin ManageProjectInfraAsync for {DtoCount} items, TenantId: {TenantId}, User: {UserId}", infraDtos.Count, tenantId, loggedInEmployee.Id); + + // --- Step 2: Categorize DTOs by Type and Action --- + var buildingsToCreateDto = infraDtos.Where(i => i.Building != null && i.Building.Id == null).Select(i => i.Building!).ToList(); + var buildingsToUpdateDto = infraDtos.Where(i => i.Building != null && i.Building.Id != null).Select(i => i.Building!).ToList(); + var floorsToCreateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id == null).Select(i => i.Floor!).ToList(); + var floorsToUpdateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id != null).Select(i => i.Floor!).ToList(); + var workAreasToCreateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id == null).Select(i => i.WorkArea!).ToList(); + var workAreasToUpdateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id != null).Select(i => i.WorkArea!).ToList(); + + _logger.LogDebug("Categorized DTOs..."); + + try + { + // --- Step 3: Fetch all required existing data in bulk --- + + // Fetch existing entities to be updated + var buildingIdsToUpdate = buildingsToUpdateDto.Select(d => d.Id!.Value).ToList(); + var existingBuildings = await _context.Buildings.Where(b => buildingIdsToUpdate.Contains(b.Id) && b.TenantId == tenantId).ToDictionaryAsync(b => b.Id); + + var floorIdsToUpdate = floorsToUpdateDto.Select(d => d.Id!.Value).ToList(); + var existingFloors = await _context.Floor.Include(f => f.Building).Where(f => floorIdsToUpdate.Contains(f.Id) && f.TenantId == tenantId).ToDictionaryAsync(f => f.Id); + + var workAreaIdsToUpdate = workAreasToUpdateDto.Select(d => d.Id!.Value).ToList(); + var existingWorkAreas = await _context.WorkAreas.Include(wa => wa.Floor!.Building).Where(wa => workAreaIdsToUpdate.Contains(wa.Id) && wa.TenantId == tenantId).ToDictionaryAsync(wa => wa.Id); + + // Fetch parent entities for items being created to get their ProjectIds + var buildingIdsForNewFloors = floorsToCreateDto.Select(f => f.BuildingId).ToList(); + var parentBuildingsForNewFloors = await _context.Buildings.Where(b => buildingIdsForNewFloors.Contains(b.Id)).ToDictionaryAsync(b => b.Id); + + var floorIdsForNewWorkAreas = workAreasToCreateDto.Select(wa => wa.FloorId).ToList(); + var parentFloorsForNewWorkAreas = await _context.Floor.Include(f => f.Building).Where(f => floorIdsForNewWorkAreas.Contains(f.Id)).ToDictionaryAsync(f => f.Id); + + _logger.LogInfo("Fetched existing entities and parents for new items."); + + // --- Step 4: Aggregate all affected ProjectIds for Security Check --- + var affectedProjectIds = new HashSet(); + + // From buildings being created/updated + buildingsToCreateDto.ForEach(b => affectedProjectIds.Add(b.ProjectId)); + foreach (var b in existingBuildings.Values) { affectedProjectIds.Add(b.ProjectId); } + + // From floors being created/updated + foreach (var f in floorsToCreateDto) { if (parentBuildingsForNewFloors.TryGetValue(f.BuildingId, out var b)) affectedProjectIds.Add(b.ProjectId); } + foreach (var f in existingFloors.Values) { if (f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } + + // From work areas being created/updated + foreach (var wa in workAreasToCreateDto) { if (parentFloorsForNewWorkAreas.TryGetValue(wa.FloorId, out var f) && f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } + foreach (var wa in existingWorkAreas.Values) { if (wa.Floor?.Building != null) affectedProjectIds.Add(wa.Floor.Building.ProjectId); } + + // Security Check against the complete list of affected projects + var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProjectInfra, loggedInEmployee.Id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} trying to manage infrastructure for projects.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to manage infrastructure for one or more of the specified projects.", 403); } - responseList.Add(new WorkItemVM + // --- Step 5: Process all logic IN MEMORY, tracking changes --- + + // Process Buildings + var createdBuildings = new List(); + foreach (var dto in buildingsToCreateDto) { - WorkItemId = workItem.Id, - WorkItem = workItem - }); - workAreaIds.Add(workItem.WorkAreaId); + var newBuilding = _mapper.Map(dto); + newBuilding.TenantId = tenantId; + createdBuildings.Add(newBuilding); + } + foreach (var dto in buildingsToUpdateDto) { if (existingBuildings.TryGetValue(dto.Id!.Value, out var b)) _mapper.Map(dto, b); } + // Process Floors + var createdFloors = new List(); + foreach (var dto in floorsToCreateDto) + { + var newFloor = _mapper.Map(dto); + newFloor.TenantId = tenantId; + createdFloors.Add(newFloor); + } + foreach (var dto in floorsToUpdateDto) { if (existingFloors.TryGetValue(dto.Id!.Value, out var f)) _mapper.Map(dto, f); } + + // Process WorkAreas + var createdWorkAreas = new List(); + foreach (var dto in workAreasToCreateDto) + { + var newWorkArea = _mapper.Map(dto); + newWorkArea.TenantId = tenantId; + createdWorkAreas.Add(newWorkArea); + } + foreach (var dto in workAreasToUpdateDto) { if (existingWorkAreas.TryGetValue(dto.Id!.Value, out var wa)) _mapper.Map(dto, wa); } + + // --- Step 6: Save all database changes in a SINGLE TRANSACTION --- + if (createdBuildings.Any()) _context.Buildings.AddRange(createdBuildings); + if (createdFloors.Any()) _context.Floor.AddRange(createdFloors); + if (createdWorkAreas.Any()) _context.WorkAreas.AddRange(createdWorkAreas); + + if (_context.ChangeTracker.HasChanges()) + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Database save successful."); + } + + // --- Step 7: Update Cache using the aggregated ProjectIds (Non-blocking) --- + var finalProjectIds = affectedProjectIds.ToList(); + if (finalProjectIds.Any()) + { + _ = Task.Run(async () => + { + try + { + _logger.LogInfo("Queuing background cache update for {ProjectCount} projects.", finalProjectIds.Count); + // Assuming your cache service has a method to handle this. + await _cache.RemoveProjectsAsync(finalProjectIds); + _logger.LogInfo("Background cache update task completed for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during the background cache update task for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); + } + }); + } + + // --- Step 8: Prepare and return a clear response --- + var responseVm = new { /* ... as before ... */ }; + return ApiResponse.SuccessResponse(responseVm, "Infrastructure changes processed successfully.", 200); } - // Apply DB changes - if (workItemsToCreate.Any()) + catch (Exception ex) { - _logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); - await _context.WorkItems.AddRangeAsync(workItemsToCreate); - await _cache.ManageWorkItemDetails(workItemsToCreate); + _logger.LogError(ex, "An unexpected error occurred in ManageProjectInfraAsync."); + return ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500); } - - if (workItemsToUpdate.Any()) - { - _logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count); - _context.WorkItems.UpdateRange(workItemsToUpdate); - await _cache.ManageWorkItemDetails(workItemsToUpdate); - } - - await _context.SaveChangesAsync(); - - _logger.LogInfo("CreateProjectTask completed successfully. Created: {Created}, Updated: {Updated}", workItemsToCreate.Count, workItemsToUpdate.Count); - - return ApiResponse.SuccessResponse(responseList, message, 200); } /// @@ -1211,12 +1488,10 @@ namespace Marco.Pms.Services.Service await _context.SaveChangesAsync(); _logger.LogInfo("Successfully saved {CreatedCount} new and {UpdatedCount} updated work items.", workItemsToCreate.Count, workItemsToModify.Count); - // --- Step 5: Update Cache and SignalR AFTER successful DB save (non-blocking) --- + // --- Step 5: Update Cache and SignalR AFTER successful DB save --- var allAffectedItems = workItemsToCreate.Concat(workItemsToModify).ToList(); - _ = Task.Run(async () => - { - await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems); - }); + + await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems); } } catch (DbUpdateException ex) @@ -1291,133 +1566,6 @@ namespace Marco.Pms.Services.Service // return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); //} - //public async Task ManageProjectInfra(List infraDots) - //{ - // var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - // var responseData = new InfraVM { }; - // string responseMessage = ""; - // string message = ""; - // List projectIds = new List(); - // if (infraDots != null) - // { - // foreach (var item in infraDots) - // { - // if (item.Building != null) - // { - - // Building building = item.Building.ToBuildingFromBuildingDto(tenantId); - // building.TenantId = tenantId; - - // if (item.Building.Id == null) - // { - // //create - // _context.Buildings.Add(building); - // await _context.SaveChangesAsync(); - // responseData.building = building; - // responseMessage = "Buliding Added Successfully"; - // message = "Building Added"; - // await _cache.AddBuildngInfra(building.ProjectId, building); - // } - // else - // { - // //update - // _context.Buildings.Update(building); - // await _context.SaveChangesAsync(); - // responseData.building = building; - // responseMessage = "Buliding Updated Successfully"; - // message = "Building Updated"; - // await _cache.UpdateBuildngInfra(building.ProjectId, building); - // } - // projectIds.Add(building.ProjectId); - // } - // if (item.Floor != null) - // { - // Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); - // floor.TenantId = tenantId; - // bool isCreated = false; - - // if (item.Floor.Id == null) - // { - // //create - // _context.Floor.Add(floor); - // await _context.SaveChangesAsync(); - // responseData.floor = floor; - // responseMessage = "Floor Added Successfully"; - // message = "Floor Added"; - // isCreated = true; - // } - // else - // { - // //update - // _context.Floor.Update(floor); - // await _context.SaveChangesAsync(); - // responseData.floor = floor; - // responseMessage = "Floor Updated Successfully"; - // message = "Floor Updated"; - // } - // Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - // var projectId = building?.ProjectId ?? Guid.Empty; - // projectIds.Add(projectId); - // message = $"{message} in Building: {building?.Name}"; - // if (isCreated) - // { - // await _cache.AddBuildngInfra(projectId, floor: floor); - // } - // else - // { - // await _cache.UpdateBuildngInfra(projectId, floor: floor); - // } - // } - // if (item.WorkArea != null) - // { - // WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); - // workArea.TenantId = tenantId; - // bool isCreated = false; - - // if (item.WorkArea.Id == null) - // { - // //create - // _context.WorkAreas.Add(workArea); - // await _context.SaveChangesAsync(); - // responseData.workArea = workArea; - // responseMessage = "Work Area Added Successfully"; - // message = "Work Area Added"; - // isCreated = true; - // } - // else - // { - // //update - // _context.WorkAreas.Update(workArea); - // await _context.SaveChangesAsync(); - // responseData.workArea = workArea; - // responseMessage = "Work Area Updated Successfully"; - // message = "Work Area Updated"; - // } - // Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - // var projectId = floor?.Building?.ProjectId ?? Guid.Empty; - // projectIds.Add(projectId); - // message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; - // if (isCreated) - // { - // await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - // } - // else - // { - // await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - // } - // } - // } - // message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - // var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - - // await _signalR.SendNotificationAsync(notification); - // return Ok(ApiResponse.SuccessResponse(responseData, responseMessage, 200)); - // } - // return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); - - //} - #endregion #region =================================================================== Helper Functions =================================================================== @@ -1663,6 +1811,82 @@ namespace Marco.Pms.Services.Service } } + private void ProcessBuilding(BuildingDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks) + { + Building building = _mapper.Map(dto); + building.TenantId = tenantId; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.Buildings.Add(building); + messages.Add("Building Added"); + cacheTasks.Add(_cache.AddBuildngInfra(building.ProjectId, building)); + } + else + { + _context.Buildings.Update(building); + messages.Add("Building Updated"); + cacheTasks.Add(_cache.UpdateBuildngInfra(building.ProjectId, building)); + } + + responseData.building = building; + projectIds.Add(building.ProjectId); + } + + private void ProcessFloor(FloorDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks, IDictionary buildings) + { + Floor floor = _mapper.Map(dto); + floor.TenantId = tenantId; + + // Use the pre-fetched dictionary for parent lookup. + Building? parentBuilding = buildings.TryGetValue(dto.BuildingId, out var b) ? b : null; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.Floor.Add(floor); + messages.Add($"Floor Added in Building: {parentBuilding?.Name ?? "Unknown"}"); + cacheTasks.Add(_cache.AddBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, floor: floor)); + } + else + { + _context.Floor.Update(floor); + messages.Add($"Floor Updated in Building: {parentBuilding?.Name ?? "Unknown"}"); + cacheTasks.Add(_cache.UpdateBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, floor: floor)); + } + + responseData.floor = floor; + if (parentBuilding != null) projectIds.Add(parentBuilding.ProjectId); + } + + private void ProcessWorkArea(WorkAreaDto dto, Guid tenantId, InfraVM responseData, List messages, ISet projectIds, List cacheTasks, IDictionary floors) + { + WorkArea workArea = _mapper.Map(dto); + workArea.TenantId = tenantId; + + // Use the pre-fetched dictionary for parent lookup. + Floor? parentFloor = floors.TryGetValue(dto.FloorId, out var f) ? f : null; + var parentBuilding = parentFloor?.Building; + + bool isNew = dto.Id == null; + if (isNew) + { + _context.WorkAreas.Add(workArea); + messages.Add($"Work Area Added in Building: {parentBuilding?.Name ?? "Unknown"}, on Floor: {parentFloor?.FloorName ?? "Unknown"}"); + cacheTasks.Add(_cache.AddBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, workArea: workArea, buildingId: parentBuilding?.Id)); + } + else + { + _context.WorkAreas.Update(workArea); + messages.Add($"Work Area Updated in Building: {parentBuilding?.Name ?? "Unknown"}, on Floor: {parentFloor?.FloorName ?? "Unknown"}"); + cacheTasks.Add(_cache.UpdateBuildngInfra(parentBuilding?.ProjectId ?? Guid.Empty, workArea: workArea, buildingId: parentBuilding?.Id)); + } + + responseData.workArea = workArea; + if (parentBuilding != null) projectIds.Add(parentBuilding.ProjectId); + } + #endregion } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index 2db004d..f1c89cc 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -21,6 +21,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task>> AssigneProjectsToEmployeeAsync(List projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee); Task> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee); Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee); + Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee); Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee); }