Optimized the Manage infra API in Project Controller

This commit is contained in:
ashutosh.nehete 2025-07-16 18:15:43 +05:30
parent 57b7f941e6
commit eabd31f8cf
13 changed files with 488 additions and 339 deletions

View File

@ -95,6 +95,13 @@ namespace Marco.Pms.CacheHelper
var result = await _projetCollection.DeleteOneAsync(filter); var result = await _projetCollection.DeleteOneAsync(filter);
return result.DeletedCount > 0; return result.DeletedCount > 0;
} }
public async Task<bool> RemoveProjectsFromCacheAsync(List<Guid> projectIds)
{
var stringIds = projectIds.Select(id => id.ToString()).ToList();
var filter = Builders<ProjectMongoDB>.Filter.In(p => p.Id, stringIds);
var result = await _projetCollection.DeleteManyAsync(filter);
return result.DeletedCount > 0;
}
// ------------------------------------------------------- Project InfraStructure ------------------------------------------------------- // ------------------------------------------------------- Project InfraStructure -------------------------------------------------------

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
namespace Marco.Pms.Model.Dtos.Project namespace Marco.Pms.Model.Dtos.Project
{ {
public class BuildingDot public class BuildingDto
{ {
[Key] [Key]
public Guid? Id { get; set; } public Guid? Id { get; set; }

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
namespace Marco.Pms.Model.Dtos.Project namespace Marco.Pms.Model.Dtos.Project
{ {
public class FloorDot public class FloorDto
{ {
public Guid? Id { get; set; } public Guid? Id { get; set; }

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations;
namespace Marco.Pms.Model.Dtos.Project namespace Marco.Pms.Model.Dtos.Project
{ {
public class WorkAreaDot public class WorkAreaDto
{ {
[Key] [Key]
public Guid? Id { get; set; } public Guid? Id { get; set; }

View File

@ -5,7 +5,7 @@ namespace Marco.Pms.Model.Mapper
{ {
public static class BuildingMapper 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 return new Building
{ {
@ -20,7 +20,7 @@ namespace Marco.Pms.Model.Mapper
public static class FloorMapper 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 return new Floor
{ {
@ -34,7 +34,7 @@ namespace Marco.Pms.Model.Mapper
public static class WorAreaMapper 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 return new WorkArea
{ {

View File

@ -0,0 +1,8 @@
namespace Marco.Pms.Model.Utilities
{
public class ServiceResponse
{
public object? Notification { get; set; }
public ApiResponse<object> Response { get; set; } = ApiResponse<object>.ErrorResponse("");
}
}

View File

@ -1,10 +1,8 @@
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Dtos.Project;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Service; using Marco.Pms.Services.Service;
using Marco.Pms.Services.Service.ServiceInterfaces; using Marco.Pms.Services.Service.ServiceInterfaces;
@ -359,6 +357,30 @@ namespace MarcoBMS.Services.Controllers
#region =================================================================== Project Infrastructre Manage APIs =================================================================== #region =================================================================== Project Infrastructre Manage APIs ===================================================================
[HttpPost("manage-infra")]
public async Task<IActionResult> ManageProjectInfra(List<InfraDto> 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<object>.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")] [HttpPost("task")]
public async Task<IActionResult> CreateProjectTask([FromBody] List<WorkItemDto> workItemDtos) public async Task<IActionResult> CreateProjectTask([FromBody] List<WorkItemDto> workItemDtos)
{ {
@ -439,134 +461,6 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Task deleted successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(new { }, "Task deleted successfully", 200));
} }
[HttpPost("manage-infra")]
public async Task<IActionResult> ManageProjectInfra(List<InfraDot> infraDots)
{
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var responseData = new InfraVM { };
string responseMessage = "";
string message = "";
List<Guid> projectIds = new List<Guid>();
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<object>.SuccessResponse(responseData, responseMessage, 200));
}
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400));
}
#endregion #endregion
} }

View File

@ -478,6 +478,18 @@ namespace Marco.Pms.Services.Helpers
} }
} }
public async Task RemoveProjectsAsync(List<Guid> 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 --------------------------------------- // ------------------------------------ Project Infrastructure Cache ---------------------------------------

View File

@ -51,6 +51,9 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<ProjectsAllocationDto, ProjectAllocation>(); CreateMap<ProjectsAllocationDto, ProjectAllocation>();
CreateMap<ProjectAllocation, ProjectAllocationVM>(); CreateMap<ProjectAllocation, ProjectAllocationVM>();
CreateMap<BuildingDto, Building>();
CreateMap<FloorDto, Floor>();
CreateMap<WorkAreaDto, WorkArea>();
CreateMap<WorkItemDto, WorkItem>() CreateMap<WorkItemDto, WorkItem>()
.ForMember( .ForMember(
dest => dest.Description, dest => dest.Description,

View File

@ -1033,83 +1033,360 @@ namespace Marco.Pms.Services.Service
#region =================================================================== Project Infrastructre Manage APIs =================================================================== #region =================================================================== Project Infrastructre Manage APIs ===================================================================
public async Task<ApiResponse<object>> CreateProjectTask1(List<WorkItemDto> workItemDtos, Guid tenantId, Employee loggedInEmployee) public async Task<ApiResponse<object>> ManageProjectInfra(List<InfraDto> infraDots, Guid tenantId, Employee loggedInEmployee)
{ {
_logger.LogInfo("CreateProjectTask called with {Count} items", workItemDtos?.Count ?? 0); var responseData = new InfraVM { };
string responseMessage = "";
// Validate request
if (workItemDtos == null || !workItemDtos.Any())
{
_logger.LogWarning("No work items provided in the request.");
return ApiResponse<object>.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400);
}
var workItemsToCreate = new List<WorkItem>();
var workItemsToUpdate = new List<WorkItem>();
var responseList = new List<WorkItemVM>();
string message = ""; string message = "";
List<Guid> workAreaIds = new List<Guid>(); List<Guid> projectIds = new List<Guid>();
var workItemIds = workItemDtos.Where(wi => wi.Id != null && wi.Id != Guid.Empty).Select(wi => wi.Id).ToList(); if (infraDots != null)
var workItems = await _context.WorkItems.AsNoTracking().Where(wi => workItemIds.Contains(wi.Id)).ToListAsync();
foreach (var itemDto in workItemDtos)
{ {
var workItem = _mapper.Map<WorkItem>(itemDto); foreach (var item in infraDots)
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)
{ {
// Update existing if (item.Building != null)
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)
{ {
double plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork;
double completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork; Building building = _mapper.Map<Building>(item.Building);
await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork); 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<Floor>(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<WorkArea>(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<object>.SuccessResponse(responseData, responseMessage, 200);
}
return ApiResponse<object>.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400);
}
public async Task<ServiceResponse> ManageProjectInfraAsync(List<InfraDto> infraDtos, Guid tenantId, Employee loggedInEmployee)
{
// 1. Guard Clause: Handle null or empty input gracefully.
if (infraDtos == null || !infraDtos.Any())
{
return new ServiceResponse
{ {
// Create new Response = ApiResponse<object>.ErrorResponse("Invalid details.", "No infrastructure details were provided.", 400)
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); var responseData = new InfraVM();
var messages = new List<string>();
var projectIds = new HashSet<Guid>(); // Use HashSet for automatic duplicate handling.
var cacheUpdateTasks = new List<Task>();
// --- 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<object>.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<object>.SuccessResponse(responseData, finalResponseMessage, 200)
};
}
/// <summary>
/// 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.
/// </summary>
public async Task<ApiResponse<object>> ManageProjectInfraAsync1(List<InfraDto> 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<object>.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<Guid>();
// 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<object>.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<Building>();
foreach (var dto in buildingsToCreateDto)
{ {
WorkItemId = workItem.Id, var newBuilding = _mapper.Map<Building>(dto);
WorkItem = workItem newBuilding.TenantId = tenantId;
}); createdBuildings.Add(newBuilding);
workAreaIds.Add(workItem.WorkAreaId); }
foreach (var dto in buildingsToUpdateDto) { if (existingBuildings.TryGetValue(dto.Id!.Value, out var b)) _mapper.Map(dto, b); }
// Process Floors
var createdFloors = new List<Floor>();
foreach (var dto in floorsToCreateDto)
{
var newFloor = _mapper.Map<Floor>(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<WorkArea>();
foreach (var dto in workAreasToCreateDto)
{
var newWorkArea = _mapper.Map<WorkArea>(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<object>.SuccessResponse(responseVm, "Infrastructure changes processed successfully.", 200);
} }
// Apply DB changes catch (Exception ex)
if (workItemsToCreate.Any())
{ {
_logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count); _logger.LogError(ex, "An unexpected error occurred in ManageProjectInfraAsync.");
await _context.WorkItems.AddRangeAsync(workItemsToCreate); return ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500);
await _cache.ManageWorkItemDetails(workItemsToCreate);
} }
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<object>.SuccessResponse(responseList, message, 200);
} }
/// <summary> /// <summary>
@ -1211,12 +1488,10 @@ namespace Marco.Pms.Services.Service
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
_logger.LogInfo("Successfully saved {CreatedCount} new and {UpdatedCount} updated work items.", workItemsToCreate.Count, workItemsToModify.Count); _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(); var allAffectedItems = workItemsToCreate.Concat(workItemsToModify).ToList();
_ = Task.Run(async () =>
{ await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems);
await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems);
});
} }
} }
catch (DbUpdateException ex) catch (DbUpdateException ex)
@ -1291,133 +1566,6 @@ namespace Marco.Pms.Services.Service
// return Ok(ApiResponse<object>.SuccessResponse(new { }, "Task deleted successfully", 200)); // return Ok(ApiResponse<object>.SuccessResponse(new { }, "Task deleted successfully", 200));
//} //}
//public async Task<IActionResult> ManageProjectInfra(List<InfraDot> infraDots)
//{
// var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// var responseData = new InfraVM { };
// string responseMessage = "";
// string message = "";
// List<Guid> projectIds = new List<Guid>();
// 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<object>.SuccessResponse(responseData, responseMessage, 200));
// }
// return BadRequest(ApiResponse<object>.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400));
//}
#endregion #endregion
#region =================================================================== Helper Functions =================================================================== #region =================================================================== Helper Functions ===================================================================
@ -1663,6 +1811,82 @@ namespace Marco.Pms.Services.Service
} }
} }
private void ProcessBuilding(BuildingDto dto, Guid tenantId, InfraVM responseData, List<string> messages, ISet<Guid> projectIds, List<Task> cacheTasks)
{
Building building = _mapper.Map<Building>(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<string> messages, ISet<Guid> projectIds, List<Task> cacheTasks, IDictionary<Guid, Building> buildings)
{
Floor floor = _mapper.Map<Floor>(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<string> messages, ISet<Guid> projectIds, List<Task> cacheTasks, IDictionary<Guid, Floor> floors)
{
WorkArea workArea = _mapper.Map<WorkArea>(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 #endregion
} }
} }

View File

@ -21,6 +21,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<List<ProjectAllocationVM>>> AssigneProjectsToEmployeeAsync(List<ProjectsAllocationDto> projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<List<ProjectAllocationVM>>> AssigneProjectsToEmployeeAsync(List<ProjectsAllocationDto> projectAllocationDtos, Guid employeeId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee);
Task<ServiceResponse> ManageProjectInfraAsync(List<InfraDto> infraDtos, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<List<WorkItemVM>>> CreateProjectTaskAsync(List<WorkItemDto> workItemDtos, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<List<WorkItemVM>>> CreateProjectTaskAsync(List<WorkItemDto> workItemDtos, Guid tenantId, Employee loggedInEmployee);
} }