Optimized the manage task API in projectController

This commit is contained in:
ashutosh.nehete 2025-07-16 15:08:53 +05:30
parent c90f39082a
commit 57b7f941e6
11 changed files with 826 additions and 301 deletions

View File

@ -406,45 +406,22 @@ namespace Marco.Pms.CacheHelper
return workItems; return workItems;
} }
public async Task ManageWorkItemDetailsToCache(List<WorkItem> workItems) public async Task ManageWorkItemDetailsToCache(List<WorkItemMongoDB> workItems)
{ {
var activityIds = workItems.Select(wi => wi.ActivityId).ToList(); foreach (WorkItemMongoDB workItem in workItems)
var workCategoryIds = workItems.Select(wi => wi.WorkCategoryId).ToList();
var workItemIds = workItems.Select(wi => wi.Id).ToList();
// fetching Activity master
var activities = await _context.ActivityMasters.Where(a => activityIds.Contains(a.Id)).ToListAsync() ?? new List<ActivityMaster>();
// Fetching Work Category
var workCategories = await _context.WorkCategoryMasters.Where(wc => workCategoryIds.Contains(wc.Id)).ToListAsync() ?? new List<WorkCategoryMaster>();
var task = await _context.TaskAllocations.Where(t => workItemIds.Contains(t.WorkItemId) && t.AssignmentDate == DateTime.UtcNow).ToListAsync();
var todaysAssign = task.Sum(t => t.PlannedTask);
foreach (WorkItem workItem in workItems)
{ {
var activity = activities.FirstOrDefault(a => a.Id == workItem.ActivityId) ?? new ActivityMaster();
var workCategory = workCategories.FirstOrDefault(a => a.Id == workItem.WorkCategoryId) ?? new WorkCategoryMaster();
var filter = Builders<WorkItemMongoDB>.Filter.Eq(p => p.Id, workItem.Id.ToString()); var filter = Builders<WorkItemMongoDB>.Filter.Eq(p => p.Id, workItem.Id.ToString());
var updates = Builders<WorkItemMongoDB>.Update.Combine( var updates = Builders<WorkItemMongoDB>.Update.Combine(
Builders<WorkItemMongoDB>.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()), Builders<WorkItemMongoDB>.Update.Set(r => r.WorkAreaId, workItem.WorkAreaId.ToString()),
Builders<WorkItemMongoDB>.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)), Builders<WorkItemMongoDB>.Update.Set(r => r.ParentTaskId, (workItem.ParentTaskId != null ? workItem.ParentTaskId.ToString() : null)),
Builders<WorkItemMongoDB>.Update.Set(r => r.PlannedWork, workItem.PlannedWork), Builders<WorkItemMongoDB>.Update.Set(r => r.PlannedWork, workItem.PlannedWork),
Builders<WorkItemMongoDB>.Update.Set(r => r.TodaysAssigned, todaysAssign), Builders<WorkItemMongoDB>.Update.Set(r => r.TodaysAssigned, workItem.TodaysAssigned),
Builders<WorkItemMongoDB>.Update.Set(r => r.CompletedWork, workItem.CompletedWork), Builders<WorkItemMongoDB>.Update.Set(r => r.CompletedWork, workItem.CompletedWork),
Builders<WorkItemMongoDB>.Update.Set(r => r.Description, workItem.Description), Builders<WorkItemMongoDB>.Update.Set(r => r.Description, workItem.Description),
Builders<WorkItemMongoDB>.Update.Set(r => r.TaskDate, workItem.TaskDate), Builders<WorkItemMongoDB>.Update.Set(r => r.TaskDate, workItem.TaskDate),
Builders<WorkItemMongoDB>.Update.Set(r => r.ExpireAt, DateTime.UtcNow.Date.AddDays(1)), Builders<WorkItemMongoDB>.Update.Set(r => r.ExpireAt, DateTime.UtcNow.Date.AddDays(1)),
Builders<WorkItemMongoDB>.Update.Set(r => r.ActivityMaster, new ActivityMasterMongoDB Builders<WorkItemMongoDB>.Update.Set(r => r.ActivityMaster, workItem.ActivityMaster),
{ Builders<WorkItemMongoDB>.Update.Set(r => r.WorkCategoryMaster, workItem.WorkCategoryMaster)
Id = activity.Id.ToString(),
ActivityName = activity.ActivityName,
UnitOfMeasurement = activity.UnitOfMeasurement
}),
Builders<WorkItemMongoDB>.Update.Set(r => r.WorkCategoryMaster, new WorkCategoryMasterMongoDB
{
Id = workCategory.Id.ToString(),
Name = workCategory.Name,
Description = workCategory.Description,
})
); );
var options = new UpdateOptions { IsUpsert = true }; var options = new UpdateOptions { IsUpsert = true };
var result = await _taskCollection.UpdateOneAsync(filter, updates, options); var result = await _taskCollection.UpdateOneAsync(filter, updates, options);

View File

@ -2,7 +2,7 @@
namespace Marco.Pms.Model.Dtos.Project namespace Marco.Pms.Model.Dtos.Project
{ {
public class WorkItemDot public class WorkItemDto
{ {
[Key] [Key]
public Guid? Id { get; set; } public Guid? Id { get; set; }

View File

@ -48,7 +48,7 @@ namespace Marco.Pms.Model.Mapper
} }
public static class WorkItemMapper public static class WorkItemMapper
{ {
public static WorkItem ToWorkItemFromWorkItemDto(this WorkItemDot model, Guid tenantId) public static WorkItem ToWorkItemFromWorkItemDto(this WorkItemDto model, Guid tenantId)
{ {
return new WorkItem return new WorkItem
{ {

View File

@ -1,9 +1,7 @@
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.Entitlements;
using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.MongoDBModels;
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.Model.ViewModels.Projects;
@ -325,188 +323,36 @@ namespace MarcoBMS.Services.Controllers
[HttpGet("infra-details/{projectId}")] [HttpGet("infra-details/{projectId}")]
public async Task<IActionResult> GetInfraDetails(Guid projectId) public async Task<IActionResult> GetInfraDetails(Guid projectId)
{ {
_logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId); // --- Step 1: Input Validation ---
if (!ModelState.IsValid)
// Step 1: Get logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Check project-specific permission
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId);
if (!hasProjectPermission)
{ {
_logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "You don't have access to this project", 403)); _logger.LogWarning("Get Project Infrastructure by ProjectId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request data provided.", errors, 400));
} }
// Step 3: Check 'ViewInfra' permission // --- Step 2: Prepare data without I/O ---
var hasViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id); Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (!hasViewInfraPermission) var response = await _projectServices.GetInfraDetailsAsync(projectId, tenantId, loggedInEmployee);
{ return StatusCode(response.StatusCode, response);
_logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "You don't have access to view infra", 403));
}
var result = await _cache.GetBuildingInfra(projectId);
if (result == null)
{
// Step 4: Fetch buildings for the project
var buildings = await _context.Buildings
.Where(b => b.ProjectId == projectId)
.ToListAsync();
var buildingIds = buildings.Select(b => b.Id).ToList();
// Step 5: Fetch floors associated with the buildings
var floors = await _context.Floor
.Where(f => buildingIds.Contains(f.BuildingId))
.ToListAsync();
var floorIds = floors.Select(f => f.Id).ToList();
// Step 6: Fetch work areas associated with the floors
var workAreas = await _context.WorkAreas
.Where(wa => floorIds.Contains(wa.FloorId))
.ToListAsync();
var workAreaIds = workAreas.Select(wa => wa.Id).ToList();
// Step 7: Fetch work items associated with the work area
var workItems = await _context.WorkItems
.Where(wi => workAreaIds.Contains(wi.WorkAreaId))
.ToListAsync();
// Step 8: Build the infra hierarchy (Building > Floors > Work Areas)
List<BuildingMongoDB> Buildings = new List<BuildingMongoDB>();
foreach (var building in buildings)
{
double buildingPlannedWorks = 0;
double buildingCompletedWorks = 0;
var selectedFloors = floors.Where(f => f.BuildingId == building.Id).ToList();
List<FloorMongoDB> Floors = new List<FloorMongoDB>();
foreach (var floor in selectedFloors)
{
double floorPlannedWorks = 0;
double floorCompletedWorks = 0;
var selectedWorkAreas = workAreas.Where(wa => wa.FloorId == floor.Id).ToList();
List<WorkAreaMongoDB> WorkAreas = new List<WorkAreaMongoDB>();
foreach (var workArea in selectedWorkAreas)
{
double workAreaPlannedWorks = 0;
double workAreaCompletedWorks = 0;
var selectedWorkItems = workItems.Where(wi => wi.WorkAreaId == workArea.Id).ToList();
foreach (var workItem in selectedWorkItems)
{
workAreaPlannedWorks += workItem.PlannedWork;
workAreaCompletedWorks += workItem.CompletedWork;
}
WorkAreaMongoDB workAreaMongo = new WorkAreaMongoDB
{
Id = workArea.Id.ToString(),
AreaName = workArea.AreaName,
PlannedWork = workAreaPlannedWorks,
CompletedWork = workAreaCompletedWorks
};
WorkAreas.Add(workAreaMongo);
floorPlannedWorks += workAreaPlannedWorks;
floorCompletedWorks += workAreaCompletedWorks;
}
FloorMongoDB floorMongoDB = new FloorMongoDB
{
Id = floor.Id.ToString(),
FloorName = floor.FloorName,
PlannedWork = floorPlannedWorks,
CompletedWork = floorCompletedWorks,
WorkAreas = WorkAreas
};
Floors.Add(floorMongoDB);
buildingPlannedWorks += floorPlannedWorks;
buildingCompletedWorks += floorCompletedWorks;
}
var buildingMongo = new BuildingMongoDB
{
Id = building.Id.ToString(),
BuildingName = building.Name,
Description = building.Description,
PlannedWork = buildingPlannedWorks,
CompletedWork = buildingCompletedWorks,
Floors = Floors
};
Buildings.Add(buildingMongo);
}
result = Buildings;
}
_logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, EmployeeId: {EmployeeId}, Buildings: {Count}",
projectId, loggedInEmployee.Id, result.Count);
return Ok(ApiResponse<object>.SuccessResponse(result, "Infra details fetched successfully", 200));
} }
[HttpGet("tasks/{workAreaId}")] [HttpGet("tasks/{workAreaId}")]
public async Task<IActionResult> GetWorkItems(Guid workAreaId) public async Task<IActionResult> GetWorkItems(Guid workAreaId)
{ {
_logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId}", workAreaId); // --- Step 1: Input Validation ---
if (!ModelState.IsValid)
// Step 1: Get the currently logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Check if the employee has ViewInfra permission
var hasViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id);
if (!hasViewInfraPermission)
{ {
_logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id); var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "You don't have permission to view infrastructure", 403)); _logger.LogWarning("Get Work Items by WorkAreaId called with invalid model state \n Errors: {Errors}", string.Join(", ", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request data provided.", errors, 400));
} }
// Step 3: Check if the specified Work Area exists // --- Step 2: Prepare data without I/O ---
var isWorkAreaExist = await _context.WorkAreas.AnyAsync(wa => wa.Id == workAreaId); Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (!isWorkAreaExist) var response = await _projectServices.GetWorkItemsAsync(workAreaId, tenantId, loggedInEmployee);
{ return StatusCode(response.StatusCode, response);
_logger.LogWarning("Work Area not found for WorkAreaId: {WorkAreaId}", workAreaId);
return NotFound(ApiResponse<object>.ErrorResponse("Work Area not found", "Work Area not found in database", 404));
}
// Step 4: Fetch WorkItems with related Activity and Work Category data
var workItemVMs = await _cache.GetWorkItemDetailsByWorkArea(workAreaId);
if (workItemVMs == null)
{
var workItems = await _context.WorkItems
.Include(wi => wi.ActivityMaster)
.Include(wi => wi.WorkCategoryMaster)
.Where(wi => wi.WorkAreaId == workAreaId)
.ToListAsync();
workItemVMs = workItems.Select(wi => new WorkItemMongoDB
{
Id = wi.Id.ToString(),
WorkAreaId = wi.WorkAreaId.ToString(),
ParentTaskId = wi.ParentTaskId.ToString(),
ActivityMaster = new ActivityMasterMongoDB
{
Id = wi.ActivityId.ToString(),
ActivityName = wi.ActivityMaster != null ? wi.ActivityMaster.ActivityName : null,
UnitOfMeasurement = wi.ActivityMaster != null ? wi.ActivityMaster.UnitOfMeasurement : null
},
WorkCategoryMaster = new WorkCategoryMasterMongoDB
{
Id = wi.WorkCategoryId.ToString() ?? "",
Name = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Name : "",
Description = wi.WorkCategoryMaster != null ? wi.WorkCategoryMaster.Description : ""
},
PlannedWork = wi.PlannedWork,
CompletedWork = wi.CompletedWork,
Description = wi.Description,
TaskDate = wi.TaskDate,
}).ToList();
await _cache.ManageWorkItemDetails(workItems);
}
_logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId);
// Step 5: Return result
return Ok(ApiResponse<object>.SuccessResponse(workItemVMs, $"{workItemVMs.Count} records of tasks fetched successfully", 200));
} }
#endregion #endregion
@ -514,107 +360,29 @@ namespace MarcoBMS.Services.Controllers
#region =================================================================== Project Infrastructre Manage APIs =================================================================== #region =================================================================== Project Infrastructre Manage APIs ===================================================================
[HttpPost("task")] [HttpPost("task")]
public async Task<IActionResult> CreateProjectTask(List<WorkItemDot> workItemDtos) public async Task<IActionResult> CreateProjectTask([FromBody] List<WorkItemDto> workItemDtos)
{ {
_logger.LogInfo("CreateProjectTask called with {Count} items", workItemDtos?.Count ?? 0); // --- Step 1: Input Validation ---
if (!ModelState.IsValid)
// Validate request
if (workItemDtos == null || !workItemDtos.Any())
{ {
_logger.LogWarning("No work items provided in the request."); var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400)); _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));
} }
var workItemsToCreate = new List<WorkItem>(); // --- Step 2: Prepare data without I/O ---
var workItemsToUpdate = new List<WorkItem>(); Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var responseList = new List<WorkItemVM>(); var response = await _projectServices.CreateProjectTaskAsync(workItemDtos, tenantId, loggedInEmployee);
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (response.Success)
string message = "";
List<Guid> workAreaIds = new List<Guid>();
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)
{ {
var workItem = itemDto.ToWorkItemFromWorkItemDto(tenantId); List<Guid> workAreaIds = response.Data.Select(pa => pa.WorkItem?.WorkAreaId ?? Guid.Empty).ToList();
var workArea = await _context.WorkAreas.Include(a => a.Floor).FirstOrDefaultAsync(a => a.Id == workItem.WorkAreaId) ?? new WorkArea(); string message = response.Message;
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
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);
double plannedWork = 0;
double completedWork = 0;
if (existingWorkItem != null)
{
if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork)
{
plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork;
completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork;
}
else if (existingWorkItem.PlannedWork == workItem.PlannedWork && existingWorkItem.CompletedWork != workItem.CompletedWork)
{
plannedWork = 0;
completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork;
}
else if (existingWorkItem.PlannedWork != workItem.PlannedWork && existingWorkItem.CompletedWork == workItem.CompletedWork)
{
plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork;
completedWork = 0;
}
await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork);
}
}
else
{
// 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);
}
responseList.Add(new WorkItemVM
{
WorkItemId = workItem.Id,
WorkItem = workItem
});
workAreaIds.Add(workItem.WorkAreaId);
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message };
await _signalR.SendNotificationAsync(notification);
} }
string responseMessage = ""; return StatusCode(response.StatusCode, response);
// Apply DB changes
if (workItemsToCreate.Any())
{
_logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count);
await _context.WorkItems.AddRangeAsync(workItemsToCreate);
responseMessage = "Task Added Successfully";
await _cache.ManageWorkItemDetails(workItemsToCreate);
}
if (workItemsToUpdate.Any())
{
_logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count);
_context.WorkItems.UpdateRange(workItemsToUpdate);
responseMessage = "Task Updated Successfully";
await _cache.ManageWorkItemDetails(workItemsToUpdate);
}
await _context.SaveChangesAsync();
_logger.LogInfo("CreateProjectTask completed successfully. Created: {Created}, Updated: {Updated}", workItemsToCreate.Count, workItemsToUpdate.Count);
var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message };
await _signalR.SendNotificationAsync(notification);
return Ok(ApiResponse<object>.SuccessResponse(responseList, responseMessage, 200));
} }
[HttpDelete("task/{id}")] [HttpDelete("task/{id}")]

View File

@ -17,9 +17,10 @@ namespace Marco.Pms.Services.Helpers
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory; private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly GeneralHelper _generalHelper;
public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger, public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger,
IDbContextFactory<ApplicationDbContext> dbContextFactory, ApplicationDbContext context) IDbContextFactory<ApplicationDbContext> dbContextFactory, ApplicationDbContext context, GeneralHelper generalHelper)
{ {
_projectCache = projectCache; _projectCache = projectCache;
_employeeCache = employeeCache; _employeeCache = employeeCache;
@ -27,6 +28,7 @@ namespace Marco.Pms.Services.Helpers
_logger = logger; _logger = logger;
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_context = context; _context = context;
_generalHelper = generalHelper;
} }
// ------------------------------------ Project Details Cache --------------------------------------- // ------------------------------------ Project Details Cache ---------------------------------------
@ -563,6 +565,19 @@ namespace Marco.Pms.Services.Helpers
} }
} }
public async Task ManageWorkItemDetails(List<WorkItem> workItems) public async Task ManageWorkItemDetails(List<WorkItem> workItems)
{
try
{
var workAreaId = workItems.First().WorkAreaId;
var workItemDB = await _generalHelper.GetWorkItemsListFromDB(workAreaId);
await _projectCache.ManageWorkItemDetailsToCache(workItemDB);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message);
}
}
public async Task ManageWorkItemDetailsByVM(List<WorkItemMongoDB> workItems)
{ {
try try
{ {

View File

@ -0,0 +1,214 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.MongoDBModels;
using MarcoBMS.Services.Service;
using Microsoft.EntityFrameworkCore;
namespace Marco.Pms.Services.Helpers
{
public class GeneralHelper
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate
private readonly ILoggingService _logger;
public GeneralHelper(IDbContextFactory<ApplicationDbContext> dbContextFactory,
ApplicationDbContext context,
ILoggingService logger)
{
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<List<BuildingMongoDB>> GetProjectInfraFromDB(Guid projectId)
{
// Each task uses its own DbContext instance for thread safety. Projections are used for efficiency.
// Task to fetch Buildings, Floors, and WorkAreas using projections
var hierarchyTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
var buildings = await context.Buildings.AsNoTracking().Where(b => b.ProjectId == projectId).Select(b => new { b.Id, b.Name, b.Description }).ToListAsync();
var buildingIds = buildings.Select(b => b.Id).ToList();
var floors = await context.Floor.AsNoTracking().Where(f => buildingIds.Contains(f.BuildingId)).Select(f => new { f.Id, f.BuildingId, f.FloorName }).ToListAsync();
var floorIds = floors.Select(f => f.Id).ToList();
var workAreas = await context.WorkAreas.AsNoTracking().Where(wa => floorIds.Contains(wa.FloorId)).Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }).ToListAsync();
return (buildings, floors, workAreas);
});
// Task to get work summaries, AGGREGATED ON THE DATABASE SERVER
var workSummaryTask = Task.Run(async () =>
{
using var context = _dbContextFactory.CreateDbContext();
// This is the most powerful optimization. It avoids pulling all WorkItem rows.
return await context.WorkItems.AsNoTracking()
.Where(wi => wi.WorkArea != null && wi.WorkArea.Floor != null && wi.WorkArea.Floor.Building != null && wi.WorkArea.Floor.Building.ProjectId == projectId)
.GroupBy(wi => wi.WorkAreaId) // Group by the parent WorkArea
.Select(g => new
{
WorkAreaId = g.Key,
PlannedWork = g.Sum(i => i.PlannedWork),
CompletedWork = g.Sum(i => i.CompletedWork)
})
.ToDictionaryAsync(x => x.WorkAreaId); // Return a ready-to-use dictionary for fast lookups
});
await Task.WhenAll(hierarchyTask, workSummaryTask);
var (buildings, floors, workAreas) = await hierarchyTask;
var workSummariesByWorkAreaId = await workSummaryTask;
// --- Step 4: Build the hierarchy efficiently using Lookups ---
// Using lookups is much faster (O(1)) than repeated .Where() calls (O(n)).
var floorsByBuildingId = floors.ToLookup(f => f.BuildingId);
var workAreasByFloorId = workAreas.ToLookup(wa => wa.FloorId);
var buildingMongoList = new List<BuildingMongoDB>();
foreach (var building in buildings)
{
double buildingPlanned = 0, buildingCompleted = 0;
var floorMongoList = new List<FloorMongoDB>();
foreach (var floor in floorsByBuildingId[building.Id]) // Fast lookup
{
double floorPlanned = 0, floorCompleted = 0;
var workAreaMongoList = new List<WorkAreaMongoDB>();
foreach (var workArea in workAreasByFloorId[floor.Id]) // Fast lookup
{
// Get the pre-calculated summary from the dictionary. O(1) operation.
workSummariesByWorkAreaId.TryGetValue(workArea.Id, out var summary);
var waPlanned = summary?.PlannedWork ?? 0;
var waCompleted = summary?.CompletedWork ?? 0;
workAreaMongoList.Add(new WorkAreaMongoDB
{
Id = workArea.Id.ToString(),
AreaName = workArea.AreaName,
PlannedWork = waPlanned,
CompletedWork = waCompleted
});
floorPlanned += waPlanned;
floorCompleted += waCompleted;
}
floorMongoList.Add(new FloorMongoDB
{
Id = floor.Id.ToString(),
FloorName = floor.FloorName,
PlannedWork = floorPlanned,
CompletedWork = floorCompleted,
WorkAreas = workAreaMongoList
});
buildingPlanned += floorPlanned;
buildingCompleted += floorCompleted;
}
buildingMongoList.Add(new BuildingMongoDB
{
Id = building.Id.ToString(),
BuildingName = building.Name,
Description = building.Description,
PlannedWork = buildingPlanned,
CompletedWork = buildingCompleted,
Floors = floorMongoList
});
}
return buildingMongoList;
}
/// <summary>
/// Retrieves a list of work items for a specific work area, including a summary of tasks assigned for the current day.
/// This method is highly optimized to run database operations in parallel and perform aggregations on the server.
/// </summary>
/// <param name="workAreaId">The ID of the work area.</param>
/// <returns>A list of WorkItemMongoDB objects with calculated daily assignments.</returns>
public async Task<List<WorkItemMongoDB>> GetWorkItemsListFromDB(Guid workAreaId)
{
_logger.LogInfo("Fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId);
try
{
// --- Step 1: Run independent database queries in PARALLEL ---
// We can fetch the WorkItems and the aggregated TaskAllocations at the same time.
// Task 1: Fetch the WorkItem entities and their related data.
var workItemsTask = _context.WorkItems
.Include(wi => wi.ActivityMaster)
.Include(wi => wi.WorkCategoryMaster)
.Where(wi => wi.WorkAreaId == workAreaId)
.AsNoTracking()
.ToListAsync();
// Task 2: Fetch and AGGREGATE today's task allocations ON THE DATABASE SERVER.
var todaysAssignmentsTask = Task.Run(async () =>
{
// Correctly define "today's" date range to avoid precision issues.
var today = DateTime.UtcNow.Date;
var tomorrow = today.AddDays(1);
using var context = _dbContextFactory.CreateDbContext(); // Use a factory for thread safety
// This is the most powerful optimization:
// 1. It filters by WorkAreaId directly, making it independent of the first query.
// 2. It filters by a correct date range.
// 3. It groups and sums on the DB server, returning only a small summary.
return await context.TaskAllocations
.Where(t => t.WorkItem != null && t.WorkItem.WorkAreaId == workAreaId &&
t.AssignmentDate >= today && t.AssignmentDate < tomorrow)
.GroupBy(t => t.WorkItemId)
.Select(g => new
{
WorkItemId = g.Key,
TodaysAssigned = g.Sum(x => x.PlannedTask)
})
// Return a dictionary for instant O(1) lookups later.
.ToDictionaryAsync(x => x.WorkItemId, x => x.TodaysAssigned);
});
// Await both parallel database operations to complete.
await Task.WhenAll(workItemsTask, todaysAssignmentsTask);
// Retrieve the results from the completed tasks.
var workItemsFromDb = await workItemsTask;
var todaysAssignments = await todaysAssignmentsTask;
// --- Step 2: Map to the ViewModel/MongoDB model efficiently ---
var workItemVMs = workItemsFromDb.Select(wi => new WorkItemMongoDB
{
Id = wi.Id.ToString(),
WorkAreaId = wi.WorkAreaId.ToString(),
ParentTaskId = wi.ParentTaskId.ToString(),
ActivityMaster = wi.ActivityMaster != null ? new ActivityMasterMongoDB
{
Id = wi.ActivityMaster.Id.ToString(),
ActivityName = wi.ActivityMaster.ActivityName,
UnitOfMeasurement = wi.ActivityMaster.UnitOfMeasurement
} : null,
WorkCategoryMaster = wi.WorkCategoryMaster != null ? new WorkCategoryMasterMongoDB
{
Id = wi.WorkCategoryMaster.Id.ToString(),
Name = wi.WorkCategoryMaster.Name,
Description = wi.WorkCategoryMaster.Description
} : null,
PlannedWork = wi.PlannedWork,
CompletedWork = wi.CompletedWork,
Description = wi.Description,
TaskDate = wi.TaskDate,
// Use the fast dictionary lookup instead of the slow in-memory Where/Sum.
TodaysAssigned = todaysAssignments.GetValueOrDefault(wi.Id, 0)
}).ToList();
_logger.LogInfo("Successfully processed {WorkItemCount} work items for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId);
return workItemVMs;
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while fetching DB work items for WorkAreaId: {WorkAreaId}", workAreaId);
// Return an empty list or re-throw, depending on your application's error handling strategy.
return new List<WorkItemMongoDB>();
}
}
}
}

View File

@ -11,14 +11,12 @@ namespace MarcoBMS.Services.Helpers
public class ProjectsHelper public class ProjectsHelper
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly RolesHelper _rolesHelper;
private readonly CacheUpdateHelper _cache; private readonly CacheUpdateHelper _cache;
private readonly PermissionServices _permission; private readonly PermissionServices _permission;
public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache, PermissionServices permission) public ProjectsHelper(ApplicationDbContext context, CacheUpdateHelper cache, PermissionServices permission)
{ {
_context = context; _context = context;
_rolesHelper = rolesHelper;
_cache = cache; _cache = cache;
_permission = permission; _permission = permission;
} }

View File

@ -50,6 +50,11 @@ namespace Marco.Pms.Services.MappingProfiles
opt => opt.MapFrom(src => src.EmpID)); opt => opt.MapFrom(src => src.EmpID));
CreateMap<ProjectsAllocationDto, ProjectAllocation>(); CreateMap<ProjectsAllocationDto, ProjectAllocation>();
CreateMap<ProjectAllocation, ProjectAllocationVM>(); CreateMap<ProjectAllocation, ProjectAllocationVM>();
CreateMap<WorkItemDto, WorkItem>()
.ForMember(
dest => dest.Description,
opt => opt.MapFrom(src => src.Comment));
#endregion #endregion
#region ======================================================= Projects ======================================================= #region ======================================================= Projects =======================================================

View File

@ -163,6 +163,7 @@ builder.Services.AddScoped<IProjectServices, ProjectServices>();
#endregion #endregion
#region Helpers #region Helpers
builder.Services.AddScoped<GeneralHelper>();
builder.Services.AddScoped<UserHelper>(); builder.Services.AddScoped<UserHelper>();
builder.Services.AddScoped<RolesHelper>(); builder.Services.AddScoped<RolesHelper>();
builder.Services.AddScoped<EmployeeHelper>(); builder.Services.AddScoped<EmployeeHelper>();

View File

@ -29,6 +29,7 @@ namespace Marco.Pms.Services.Service
private readonly PermissionServices _permission; private readonly PermissionServices _permission;
private readonly CacheUpdateHelper _cache; private readonly CacheUpdateHelper _cache;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly GeneralHelper _generalHelper;
public ProjectServices( public ProjectServices(
IDbContextFactory<ApplicationDbContext> dbContextFactory, IDbContextFactory<ApplicationDbContext> dbContextFactory,
ApplicationDbContext context, ApplicationDbContext context,
@ -36,7 +37,8 @@ namespace Marco.Pms.Services.Service
ProjectsHelper projectsHelper, ProjectsHelper projectsHelper,
PermissionServices permission, PermissionServices permission,
CacheUpdateHelper cache, CacheUpdateHelper cache,
IMapper mapper) IMapper mapper,
GeneralHelper generalHelper)
{ {
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
_context = context ?? throw new ArgumentNullException(nameof(context)); _context = context ?? throw new ArgumentNullException(nameof(context));
@ -45,6 +47,7 @@ namespace Marco.Pms.Services.Service
_permission = permission ?? throw new ArgumentNullException(nameof(permission)); _permission = permission ?? throw new ArgumentNullException(nameof(permission));
_cache = cache ?? throw new ArgumentNullException(nameof(cache)); _cache = cache ?? throw new ArgumentNullException(nameof(cache));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_generalHelper = generalHelper ?? throw new ArgumentNullException(nameof(generalHelper));
} }
#region =================================================================== Project Get APIs =================================================================== #region =================================================================== Project Get APIs ===================================================================
@ -898,6 +901,525 @@ namespace Marco.Pms.Services.Service
#endregion #endregion
#region =================================================================== Project InfraStructure Get APIs ===================================================================
/// <summary>
/// Retrieves the full infrastructure hierarchy (Buildings, Floors, Work Areas) for a project,
/// including aggregated work summaries.
/// </summary>
public async Task<ApiResponse<object>> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee)
{
_logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId);
try
{
// --- Step 1: Run independent permission checks in PARALLEL ---
var projectPermissionTask = _permission.HasProjectPermission(loggedInEmployee, projectId);
var viewInfraPermissionTask = _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id);
await Task.WhenAll(projectPermissionTask, viewInfraPermissionTask);
if (!await projectPermissionTask)
{
_logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
return ApiResponse<object>.ErrorResponse("Access denied", "You don't have access to this project", 403);
}
if (!await viewInfraPermissionTask)
{
_logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Access denied", "You don't have access to view this project's infrastructure", 403);
}
// --- Step 2: Cache-First Strategy ---
var cachedResult = await _cache.GetBuildingInfra(projectId);
if (cachedResult != null)
{
_logger.LogInfo("Cache HIT for infra details for ProjectId: {ProjectId}", projectId);
return ApiResponse<object>.SuccessResponse(cachedResult, "Infra details fetched successfully from cache.", 200);
}
_logger.LogInfo("Cache MISS for infra details for ProjectId: {ProjectId}. Fetching from database.", projectId);
// --- Step 3: Fetch all required data from the database ---
var buildingMongoList = await _generalHelper.GetProjectInfraFromDB(projectId);
// --- Step 5: Proactively update the cache ---
//await _cache.SetBuildingInfra(projectId, buildingMongoList);
_logger.LogInfo("Infra details fetched successfully for ProjectId: {ProjectId}, Buildings: {Count}", projectId, buildingMongoList.Count);
return ApiResponse<object>.SuccessResponse(buildingMongoList, "Infra details fetched successfully", 200);
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while fetching infra details for ProjectId: {ProjectId}", projectId);
return ApiResponse<object>.ErrorResponse("An internal server error occurred.", "An error occurred while processing your request.", 500);
}
}
/// <summary>
/// Retrieves a list of work items for a specific work area, ensuring the user has appropriate permissions.
/// </summary>
/// <param name="workAreaId">The ID of the work area.</param>
/// <param name="tenantId">The ID of the current tenant.</param>
/// <param name="loggedInEmployee">The current authenticated employee for permission checks.</param>
/// <returns>An ApiResponse containing a list of work items or an error.</returns>
public async Task<ApiResponse<object>> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee)
{
_logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId} by User: {UserId}", workAreaId, loggedInEmployee.Id);
try
{
// --- Step 1: Cache-First Strategy ---
var cachedWorkItems = await _cache.GetWorkItemDetailsByWorkArea(workAreaId);
if (cachedWorkItems != null)
{
_logger.LogInfo("Cache HIT for WorkAreaId: {WorkAreaId}. Returning {Count} items from cache.", workAreaId, cachedWorkItems.Count);
return ApiResponse<object>.SuccessResponse(cachedWorkItems, $"{cachedWorkItems.Count} tasks retrieved successfully from cache.", 200);
}
_logger.LogInfo("Cache MISS for WorkAreaId: {WorkAreaId}. Fetching from database.", workAreaId);
// --- Step 2: Security Check First ---
// This pattern remains the most robust: verify permissions before fetching a large list.
var projectInfo = await _context.WorkAreas
.Where(wa => wa.Id == workAreaId && wa.TenantId == tenantId && wa.Floor != null && wa.Floor.Building != null)
.Select(wa => new { wa.Floor!.Building!.ProjectId })
.FirstOrDefaultAsync();
if (projectInfo == null)
{
_logger.LogWarning("Work Area not found for WorkAreaId: {WorkAreaId}", workAreaId);
return ApiResponse<object>.ErrorResponse("Not Found", $"Work Area with ID {workAreaId} not found.", 404);
}
var hasProjectAccess = await _permission.HasProjectPermission(loggedInEmployee, projectInfo.ProjectId);
var hasGenericViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id);
if (!hasProjectAccess || !hasGenericViewInfraPermission)
{
_logger.LogWarning("Access DENIED for user {UserId} on WorkAreaId {WorkAreaId}.", loggedInEmployee.Id, workAreaId);
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have sufficient permissions to view these work items.", 403);
}
// --- Step 3: Fetch Full Entities for Caching and Mapping ---
var workItemVMs = await _generalHelper.GetWorkItemsListFromDB(workAreaId);
// --- Step 5: Proactively Update the Cache with the Correct Object Type ---
// We now pass the 'workItemsFromDb' list, which is the required List<WorkItem>.
try
{
await _cache.ManageWorkItemDetailsByVM(workItemVMs);
_logger.LogInfo("Successfully queued cache update for WorkAreaId: {WorkAreaId}", workAreaId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Background cache update failed for WorkAreaId: {WorkAreaId}", workAreaId);
}
_logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId);
return ApiResponse<object>.SuccessResponse(workItemVMs, $"{workItemVMs.Count} tasks fetched successfully.", 200);
}
catch (Exception ex)
{
// --- Step 6: Graceful Error Handling ---
_logger.LogError(ex, "An unexpected error occurred while getting work items for WorkAreaId: {WorkAreaId}", workAreaId);
return ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500);
}
}
#endregion
#region =================================================================== Project Infrastructre Manage APIs ===================================================================
public async Task<ApiResponse<object>> CreateProjectTask1(List<WorkItemDto> workItemDtos, 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<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 = "";
List<Guid> workAreaIds = new List<Guid>();
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)
{
var workItem = _mapper.Map<WorkItem>(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)
{
// 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)
{
double plannedWork = workItem.PlannedWork - existingWorkItem.PlannedWork;
double completedWork = workItem.CompletedWork - existingWorkItem.CompletedWork;
await _cache.UpdatePlannedAndCompleteWorksInBuilding(workArea.Id, plannedWork, completedWork);
}
}
else
{
// 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);
}
responseList.Add(new WorkItemVM
{
WorkItemId = workItem.Id,
WorkItem = workItem
});
workAreaIds.Add(workItem.WorkAreaId);
}
// Apply DB changes
if (workItemsToCreate.Any())
{
_logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count);
await _context.WorkItems.AddRangeAsync(workItemsToCreate);
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>
/// Creates or updates a batch of work items.
/// This method is optimized to perform all database operations in a single, atomic transaction.
/// </summary>
public async Task<ApiResponse<List<WorkItemVM>>> CreateProjectTaskAsync(List<WorkItemDto> workItemDtos, Guid tenantId, Employee loggedInEmployee)
{
_logger.LogInfo("CreateProjectTask called with {Count} items by user {UserId}", workItemDtos?.Count ?? 0, loggedInEmployee.Id);
// --- Step 1: Input Validation ---
if (workItemDtos == null || !workItemDtos.Any())
{
_logger.LogWarning("No work items provided in the request.");
return ApiResponse<List<WorkItemVM>>.ErrorResponse("Invalid details.", "Work Item details list cannot be empty.", 400);
}
// --- Step 2: Fetch all required existing data in bulk ---
var workAreaIds = workItemDtos.Select(d => d.WorkAreaID).Distinct().ToList();
var workItemIdsToUpdate = workItemDtos.Where(d => d.Id.HasValue).Select(d => d.Id!.Value).ToList();
// Fetch all relevant WorkAreas and their parent hierarchy in ONE query
var workAreasFromDb = await _context.WorkAreas
.Where(wa => wa.Floor != null && wa.Floor.Building != null && workAreaIds.Contains(wa.Id) && wa.TenantId == tenantId)
.Include(wa => wa.Floor!.Building) // Eagerly load the entire path
.ToDictionaryAsync(wa => wa.Id); // Dictionary for fast lookups
// Fetch all existing WorkItems that need updating in ONE query
var existingWorkItemsToUpdate = await _context.WorkItems
.Where(wi => workItemIdsToUpdate.Contains(wi.Id) && wi.TenantId == tenantId)
.ToDictionaryAsync(wi => wi.Id); // Dictionary for fast lookups
// --- (Placeholder) Security Check ---
// You MUST verify the user has permission to modify ALL WorkAreas in the batch.
var projectIdsInBatch = workAreasFromDb.Values.Select(wa => wa.Floor!.Building!.ProjectId).Distinct();
var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProjectInfra, loggedInEmployee.Id);
if (!hasPermission)
{
_logger.LogWarning("Access DENIED for user {UserId} trying to create/update tasks.", loggedInEmployee.Id);
return ApiResponse<List<WorkItemVM>>.ErrorResponse("Access Denied.", "You do not have permission to modify tasks in one or more of the specified work areas.", 403);
}
var workItemsToCreate = new List<WorkItem>();
var workItemsToModify = new List<WorkItem>();
var workDeltaForCache = new Dictionary<Guid, (double Planned, double Completed)>(); // WorkAreaId -> (Delta)
string message = "";
// --- Step 3: Process all logic IN MEMORY, tracking changes ---
foreach (var dto in workItemDtos)
{
if (!workAreasFromDb.TryGetValue(dto.WorkAreaID, out var workArea))
{
_logger.LogWarning("Skipping item because WorkAreaId {WorkAreaId} was not found or is invalid.", dto.WorkAreaID);
continue; // Skip this item as its parent WorkArea is invalid
}
if (dto.Id.HasValue && existingWorkItemsToUpdate.TryGetValue(dto.Id.Value, out var existingWorkItem))
{
// --- UPDATE Logic ---
var plannedDelta = dto.PlannedWork - existingWorkItem.PlannedWork;
var completedDelta = dto.CompletedWork - existingWorkItem.CompletedWork;
// Apply changes from DTO to the fetched entity to prevent data loss
_mapper.Map(dto, existingWorkItem);
workItemsToModify.Add(existingWorkItem);
// Track the change in work for cache update
workDeltaForCache[workArea.Id] = (
workDeltaForCache.GetValueOrDefault(workArea.Id).Planned + plannedDelta,
workDeltaForCache.GetValueOrDefault(workArea.Id).Completed + completedDelta
);
message = $"Task Updated in Building: {workArea.Floor?.Building?.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}";
}
else
{
// --- CREATE Logic ---
var newWorkItem = _mapper.Map<WorkItem>(dto);
newWorkItem.Id = Guid.NewGuid(); // Ensure new GUID is set
newWorkItem.TenantId = tenantId;
workItemsToCreate.Add(newWorkItem);
// Track the change in work for cache update
workDeltaForCache[workArea.Id] = (
workDeltaForCache.GetValueOrDefault(workArea.Id).Planned + newWorkItem.PlannedWork,
workDeltaForCache.GetValueOrDefault(workArea.Id).Completed + newWorkItem.CompletedWork
);
message = $"Task Added in Building: {workArea.Floor?.Building?.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}";
}
}
try
{
// --- Step 4: Save all database changes in a SINGLE TRANSACTION ---
if (workItemsToCreate.Any()) _context.WorkItems.AddRange(workItemsToCreate);
if (workItemsToModify.Any()) _context.WorkItems.UpdateRange(workItemsToModify); // EF Core handles individual updates correctly here
if (workItemsToCreate.Any() || workItemsToModify.Any())
{
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) ---
var allAffectedItems = workItemsToCreate.Concat(workItemsToModify).ToList();
_ = Task.Run(async () =>
{
await UpdateCacheAndNotify(workDeltaForCache, allAffectedItems);
});
}
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "A database error occurred while creating/updating tasks.");
return ApiResponse<List<WorkItemVM>>.ErrorResponse("Database Error", "Failed to save changes.", 500);
}
// --- Step 6: Prepare and return the response ---
var allProcessedItems = workItemsToCreate.Concat(workItemsToModify).ToList();
var responseList = allProcessedItems.Select(wi => new WorkItemVM
{
WorkItemId = wi.Id,
WorkItem = wi
}).ToList();
return ApiResponse<List<WorkItemVM>>.SuccessResponse(responseList, message, 200);
}
//public async Task<IActionResult> DeleteProjectTask(Guid id)
//{
// var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// List<Guid> workAreaIds = new List<Guid>();
// WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId);
// if (task != null)
// {
// if (task.CompletedWork == 0)
// {
// var assignedTask = await _context.TaskAllocations.Where(t => t.WorkItemId == id).ToListAsync();
// if (assignedTask.Count == 0)
// {
// _context.WorkItems.Remove(task);
// await _context.SaveChangesAsync();
// _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id);
// var floorId = task.WorkArea?.FloorId;
// var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId);
// workAreaIds.Add(task.WorkAreaId);
// var projectId = floor?.Building?.ProjectId;
// var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}" };
// await _signalR.SendNotificationAsync(notification);
// await _cache.DeleteWorkItemByIdAsync(task.Id);
// if (projectId != null)
// {
// await _cache.DeleteProjectByIdAsync(projectId.Value);
// }
// }
// else
// {
// _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id);
// return BadRequest(ApiResponse<object>.ErrorResponse("Task is currently assigned and cannot be deleted.", "Task is currently assigned and cannot be deleted.", 400));
// }
// }
// else
// {
// double percentage = (task.CompletedWork / task.PlannedWork) * 100;
// percentage = Math.Round(percentage, 2);
// _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted", task.Id, percentage);
// return BadRequest(ApiResponse<object>.ErrorResponse(System.String.Format("Task is {0}% complete and cannot be deleted", percentage), System.String.Format("Task is {0}% complete and cannot be deleted", percentage), 400));
// }
// }
// else
// {
// _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id);
// }
// 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
#region =================================================================== Helper Functions =================================================================== #region =================================================================== Helper Functions ===================================================================
/// <summary> /// <summary>
@ -1101,7 +1623,6 @@ namespace Marco.Pms.Services.Service
return dbProject; return dbProject;
} }
// Helper method for background cache update
private async Task UpdateCacheInBackground(Project project) private async Task UpdateCacheInBackground(Project project)
{ {
try try
@ -1120,6 +1641,28 @@ namespace Marco.Pms.Services.Service
} }
} }
private async Task UpdateCacheAndNotify(Dictionary<Guid, (double Planned, double Completed)> workDelta, List<WorkItem> affectedItems)
{
try
{
// Update planned/completed work totals
var cacheUpdateTasks = workDelta.Select(kvp =>
_cache.UpdatePlannedAndCompleteWorksInBuilding(kvp.Key, kvp.Value.Planned, kvp.Value.Completed));
await Task.WhenAll(cacheUpdateTasks);
_logger.LogInfo("Background cache work totals update completed for {AreaCount} areas.", workDelta.Count);
// Update the details of the individual work items in the cache
await _cache.ManageWorkItemDetails(affectedItems);
_logger.LogInfo("Background cache work item details update completed for {ItemCount} items.", affectedItems.Count);
// Add SignalR notification logic here if needed
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred during background cache update/notification.");
}
}
#endregion #endregion
} }
} }

View File

@ -19,5 +19,9 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<List<ProjectAllocationVM>>> ManageAllocationAsync(List<ProjectAllocationDot> projectAllocationDots, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<List<ProjectAllocationVM>>> ManageAllocationAsync(List<ProjectAllocationDot> projectAllocationDots, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetProjectsByEmployeeAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee);
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>> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<List<WorkItemVM>>> CreateProjectTaskAsync(List<WorkItemDto> workItemDtos, Guid tenantId, Employee loggedInEmployee);
} }
} }