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;
|
||||
}
|
||||
public async Task ManageWorkItemDetailsToCache(List<WorkItem> workItems)
|
||||
public async Task ManageWorkItemDetailsToCache(List<WorkItemMongoDB> workItems)
|
||||
{
|
||||
var activityIds = workItems.Select(wi => wi.ActivityId).ToList();
|
||||
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)
|
||||
foreach (WorkItemMongoDB 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 updates = Builders<WorkItemMongoDB>.Update.Combine(
|
||||
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.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.Description, workItem.Description),
|
||||
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.ActivityMaster, new ActivityMasterMongoDB
|
||||
{
|
||||
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,
|
||||
})
|
||||
Builders<WorkItemMongoDB>.Update.Set(r => r.ActivityMaster, workItem.ActivityMaster),
|
||||
Builders<WorkItemMongoDB>.Update.Set(r => r.WorkCategoryMaster, workItem.WorkCategoryMaster)
|
||||
);
|
||||
var options = new UpdateOptions { IsUpsert = true };
|
||||
var result = await _taskCollection.UpdateOneAsync(filter, updates, options);
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Marco.Pms.Model.Dtos.Project
|
||||
{
|
||||
public class WorkItemDot
|
||||
public class WorkItemDto
|
||||
{
|
||||
[Key]
|
||||
public Guid? Id { get; set; }
|
@ -48,7 +48,7 @@ namespace Marco.Pms.Model.Mapper
|
||||
}
|
||||
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
|
||||
{
|
||||
|
@ -1,9 +1,7 @@
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Model.Dtos.Project;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Entitlements;
|
||||
using Marco.Pms.Model.Mapper;
|
||||
using Marco.Pms.Model.MongoDBModels;
|
||||
using Marco.Pms.Model.Projects;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Projects;
|
||||
@ -325,188 +323,36 @@ namespace MarcoBMS.Services.Controllers
|
||||
[HttpGet("infra-details/{projectId}")]
|
||||
public async Task<IActionResult> GetInfraDetails(Guid projectId)
|
||||
{
|
||||
_logger.LogInfo("GetInfraDetails called for ProjectId: {ProjectId}", projectId);
|
||||
|
||||
// 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)
|
||||
// --- Step 1: Input Validation ---
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
_logger.LogWarning("Project access denied for EmployeeId: {EmployeeId} on ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
|
||||
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "You don't have access to this project", 403));
|
||||
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
|
||||
_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
|
||||
var hasViewInfraPermission = await _permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id);
|
||||
if (!hasViewInfraPermission)
|
||||
{
|
||||
_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 2: Prepare data without I/O ---
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _projectServices.GetInfraDetailsAsync(projectId, tenantId, loggedInEmployee);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
|
||||
// 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}")]
|
||||
public async Task<IActionResult> GetWorkItems(Guid workAreaId)
|
||||
{
|
||||
_logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId}", workAreaId);
|
||||
|
||||
// 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)
|
||||
// --- Step 1: Input Validation ---
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
_logger.LogWarning("ViewInfra permission denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
|
||||
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "You don't have permission to view infrastructure", 403));
|
||||
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
|
||||
_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
|
||||
var isWorkAreaExist = await _context.WorkAreas.AnyAsync(wa => wa.Id == workAreaId);
|
||||
if (!isWorkAreaExist)
|
||||
{
|
||||
_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));
|
||||
// --- Step 2: Prepare data without I/O ---
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _projectServices.GetWorkItemsAsync(workAreaId, tenantId, loggedInEmployee);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -514,107 +360,29 @@ namespace MarcoBMS.Services.Controllers
|
||||
#region =================================================================== Project Infrastructre Manage APIs ===================================================================
|
||||
|
||||
[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);
|
||||
|
||||
// Validate request
|
||||
if (workItemDtos == null || !workItemDtos.Any())
|
||||
// --- Step 1: Input Validation ---
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
_logger.LogWarning("No work items provided in the request.");
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400));
|
||||
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
|
||||
_logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors));
|
||||
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request data provided.", errors, 400));
|
||||
}
|
||||
|
||||
var workItemsToCreate = new List<WorkItem>();
|
||||
var workItemsToUpdate = new List<WorkItem>();
|
||||
var responseList = new List<WorkItemVM>();
|
||||
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
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)
|
||||
// --- Step 2: Prepare data without I/O ---
|
||||
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||
var response = await _projectServices.CreateProjectTaskAsync(workItemDtos, tenantId, loggedInEmployee);
|
||||
if (response.Success)
|
||||
{
|
||||
var workItem = itemDto.ToWorkItemFromWorkItemDto(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);
|
||||
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);
|
||||
List<Guid> workAreaIds = response.Data.Select(pa => pa.WorkItem?.WorkAreaId ?? Guid.Empty).ToList();
|
||||
string message = response.Message;
|
||||
|
||||
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = message };
|
||||
await _signalR.SendNotificationAsync(notification);
|
||||
}
|
||||
string responseMessage = "";
|
||||
// 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);
|
||||
}
|
||||
return StatusCode(response.StatusCode, response);
|
||||
|
||||
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}")]
|
||||
|
@ -17,9 +17,10 @@ namespace Marco.Pms.Services.Helpers
|
||||
private readonly ILoggingService _logger;
|
||||
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly GeneralHelper _generalHelper;
|
||||
|
||||
public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger,
|
||||
IDbContextFactory<ApplicationDbContext> dbContextFactory, ApplicationDbContext context)
|
||||
IDbContextFactory<ApplicationDbContext> dbContextFactory, ApplicationDbContext context, GeneralHelper generalHelper)
|
||||
{
|
||||
_projectCache = projectCache;
|
||||
_employeeCache = employeeCache;
|
||||
@ -27,6 +28,7 @@ namespace Marco.Pms.Services.Helpers
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_context = context;
|
||||
_generalHelper = generalHelper;
|
||||
}
|
||||
|
||||
// ------------------------------------ Project Details Cache ---------------------------------------
|
||||
@ -563,6 +565,19 @@ namespace Marco.Pms.Services.Helpers
|
||||
}
|
||||
}
|
||||
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
|
||||
{
|
||||
|
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
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly RolesHelper _rolesHelper;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
private readonly PermissionServices _permission;
|
||||
|
||||
public ProjectsHelper(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache, PermissionServices permission)
|
||||
public ProjectsHelper(ApplicationDbContext context, CacheUpdateHelper cache, PermissionServices permission)
|
||||
{
|
||||
_context = context;
|
||||
_rolesHelper = rolesHelper;
|
||||
_cache = cache;
|
||||
_permission = permission;
|
||||
}
|
||||
|
@ -50,6 +50,11 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
opt => opt.MapFrom(src => src.EmpID));
|
||||
CreateMap<ProjectsAllocationDto, ProjectAllocation>();
|
||||
CreateMap<ProjectAllocation, ProjectAllocationVM>();
|
||||
|
||||
CreateMap<WorkItemDto, WorkItem>()
|
||||
.ForMember(
|
||||
dest => dest.Description,
|
||||
opt => opt.MapFrom(src => src.Comment));
|
||||
#endregion
|
||||
|
||||
#region ======================================================= Projects =======================================================
|
||||
|
@ -163,6 +163,7 @@ builder.Services.AddScoped<IProjectServices, ProjectServices>();
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
builder.Services.AddScoped<GeneralHelper>();
|
||||
builder.Services.AddScoped<UserHelper>();
|
||||
builder.Services.AddScoped<RolesHelper>();
|
||||
builder.Services.AddScoped<EmployeeHelper>();
|
||||
|
@ -29,6 +29,7 @@ namespace Marco.Pms.Services.Service
|
||||
private readonly PermissionServices _permission;
|
||||
private readonly CacheUpdateHelper _cache;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly GeneralHelper _generalHelper;
|
||||
public ProjectServices(
|
||||
IDbContextFactory<ApplicationDbContext> dbContextFactory,
|
||||
ApplicationDbContext context,
|
||||
@ -36,7 +37,8 @@ namespace Marco.Pms.Services.Service
|
||||
ProjectsHelper projectsHelper,
|
||||
PermissionServices permission,
|
||||
CacheUpdateHelper cache,
|
||||
IMapper mapper)
|
||||
IMapper mapper,
|
||||
GeneralHelper generalHelper)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
@ -45,6 +47,7 @@ namespace Marco.Pms.Services.Service
|
||||
_permission = permission ?? throw new ArgumentNullException(nameof(permission));
|
||||
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
||||
_generalHelper = generalHelper ?? throw new ArgumentNullException(nameof(generalHelper));
|
||||
}
|
||||
#region =================================================================== Project Get APIs ===================================================================
|
||||
|
||||
@ -898,6 +901,525 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
#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 ===================================================================
|
||||
|
||||
/// <summary>
|
||||
@ -1101,7 +1623,6 @@ namespace Marco.Pms.Services.Service
|
||||
return dbProject;
|
||||
}
|
||||
|
||||
// Helper method for background cache update
|
||||
private async Task UpdateCacheInBackground(Project project)
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -19,5 +19,9 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
Task<ApiResponse<List<ProjectAllocationVM>>> ManageAllocationAsync(List<ProjectAllocationDot> projectAllocationDots, 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<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