Optimized the manage task API in projectController
This commit is contained in:
parent
c3da83d165
commit
c79cbf32ea
@ -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);
|
||||||
|
@ -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; }
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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}")]
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
214
Marco.Pms.Services/Helpers/GeneralHelper.cs
Normal file
214
Marco.Pms.Services/Helpers/GeneralHelper.cs
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 =======================================================
|
||||||
|
@ -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>();
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user