diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index 71ef1a5..362c2af 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -1,7 +1,6 @@ using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; -using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service; @@ -11,7 +10,6 @@ using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis; -using Microsoft.EntityFrameworkCore; using MongoDB.Driver; namespace MarcoBMS.Services.Controllers @@ -410,55 +408,24 @@ namespace MarcoBMS.Services.Controllers [HttpDelete("task/{id}")] public async Task DeleteProjectTask(Guid id) { - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List workAreaIds = new List(); - WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); - if (task != null) + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) { - 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.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.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)); - - } + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); } - else + + // --- Step 2: Prepare data without I/O --- + Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var serviceResponse = await _projectServices.DeleteProjectTaskAsync(id, tenantId, loggedInEmployee); + var response = serviceResponse.Response; + var notification = serviceResponse.Notification; + if (notification != null) { - _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id); + await _signalR.SendNotificationAsync(notification); } - return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); + return StatusCode(response.StatusCode, response); } #endregion diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index 32e1285..d7ab2ac 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -1033,130 +1033,6 @@ namespace Marco.Pms.Services.Service #region =================================================================== Project Infrastructre Manage APIs =================================================================== - public async Task> ManageProjectInfra(List infraDots, Guid tenantId, Employee loggedInEmployee) - { - var responseData = new InfraVM { }; - string responseMessage = ""; - string message = ""; - List projectIds = new List(); - if (infraDots != null) - { - foreach (var item in infraDots) - { - if (item.Building != null) - { - - Building building = _mapper.Map(item.Building); - building.TenantId = tenantId; - - if (item.Building.Id == null) - { - //create - _context.Buildings.Add(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Added Successfully"; - message = "Building Added"; - await _cache.AddBuildngInfra(building.ProjectId, building); - } - else - { - //update - _context.Buildings.Update(building); - await _context.SaveChangesAsync(); - responseData.building = building; - responseMessage = "Buliding Updated Successfully"; - message = "Building Updated"; - await _cache.UpdateBuildngInfra(building.ProjectId, building); - } - projectIds.Add(building.ProjectId); - } - if (item.Floor != null) - { - Floor floor = _mapper.Map(item.Floor); - floor.TenantId = tenantId; - bool isCreated = false; - - if (item.Floor.Id == null) - { - //create - _context.Floor.Add(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Added Successfully"; - message = "Floor Added"; - isCreated = true; - } - else - { - //update - _context.Floor.Update(floor); - await _context.SaveChangesAsync(); - responseData.floor = floor; - responseMessage = "Floor Updated Successfully"; - message = "Floor Updated"; - } - Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId); - var projectId = building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {building?.Name}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, floor: floor); - } - else - { - await _cache.UpdateBuildngInfra(projectId, floor: floor); - } - } - if (item.WorkArea != null) - { - WorkArea workArea = _mapper.Map(item.WorkArea); - workArea.TenantId = tenantId; - bool isCreated = false; - - if (item.WorkArea.Id == null) - { - //create - _context.WorkAreas.Add(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Added Successfully"; - message = "Work Area Added"; - isCreated = true; - } - else - { - //update - _context.WorkAreas.Update(workArea); - await _context.SaveChangesAsync(); - responseData.workArea = workArea; - responseMessage = "Work Area Updated Successfully"; - message = "Work Area Updated"; - } - Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId); - var projectId = floor?.Building?.ProjectId ?? Guid.Empty; - projectIds.Add(projectId); - message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}"; - if (isCreated) - { - await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - else - { - await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId); - } - } - } - message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"; - var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message }; - - return ApiResponse.SuccessResponse(responseData, responseMessage, 200); - } - return ApiResponse.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400); - - } - public async Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee) { // 1. Guard Clause: Handle null or empty input gracefully. @@ -1244,151 +1120,6 @@ namespace Marco.Pms.Services.Service }; } - /// - /// Manages a batch of infrastructure changes (creates/updates for Buildings, Floors, and WorkAreas). - /// This method is optimized to perform all database operations in a single, atomic transaction. - /// - public async Task> ManageProjectInfraAsync1(List infraDtos, Guid tenantId, Employee loggedInEmployee) - { - // --- Step 1: Input Validation --- - if (infraDtos == null || !infraDtos.Any()) - { - _logger.LogWarning("ManageProjectInfraAsync called with null or empty DTO list."); - return ApiResponse.ErrorResponse("Invalid details.", "Infrastructure data cannot be empty.", 400); - } - - _logger.LogInfo("Begin ManageProjectInfraAsync for {DtoCount} items, TenantId: {TenantId}, User: {UserId}", infraDtos.Count, tenantId, loggedInEmployee.Id); - - // --- Step 2: Categorize DTOs by Type and Action --- - var buildingsToCreateDto = infraDtos.Where(i => i.Building != null && i.Building.Id == null).Select(i => i.Building!).ToList(); - var buildingsToUpdateDto = infraDtos.Where(i => i.Building != null && i.Building.Id != null).Select(i => i.Building!).ToList(); - var floorsToCreateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id == null).Select(i => i.Floor!).ToList(); - var floorsToUpdateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id != null).Select(i => i.Floor!).ToList(); - var workAreasToCreateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id == null).Select(i => i.WorkArea!).ToList(); - var workAreasToUpdateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id != null).Select(i => i.WorkArea!).ToList(); - - _logger.LogDebug("Categorized DTOs..."); - - try - { - // --- Step 3: Fetch all required existing data in bulk --- - - // Fetch existing entities to be updated - var buildingIdsToUpdate = buildingsToUpdateDto.Select(d => d.Id!.Value).ToList(); - var existingBuildings = await _context.Buildings.Where(b => buildingIdsToUpdate.Contains(b.Id) && b.TenantId == tenantId).ToDictionaryAsync(b => b.Id); - - var floorIdsToUpdate = floorsToUpdateDto.Select(d => d.Id!.Value).ToList(); - var existingFloors = await _context.Floor.Include(f => f.Building).Where(f => floorIdsToUpdate.Contains(f.Id) && f.TenantId == tenantId).ToDictionaryAsync(f => f.Id); - - var workAreaIdsToUpdate = workAreasToUpdateDto.Select(d => d.Id!.Value).ToList(); - var existingWorkAreas = await _context.WorkAreas.Include(wa => wa.Floor!.Building).Where(wa => workAreaIdsToUpdate.Contains(wa.Id) && wa.TenantId == tenantId).ToDictionaryAsync(wa => wa.Id); - - // Fetch parent entities for items being created to get their ProjectIds - var buildingIdsForNewFloors = floorsToCreateDto.Select(f => f.BuildingId).ToList(); - var parentBuildingsForNewFloors = await _context.Buildings.Where(b => buildingIdsForNewFloors.Contains(b.Id)).ToDictionaryAsync(b => b.Id); - - var floorIdsForNewWorkAreas = workAreasToCreateDto.Select(wa => wa.FloorId).ToList(); - var parentFloorsForNewWorkAreas = await _context.Floor.Include(f => f.Building).Where(f => floorIdsForNewWorkAreas.Contains(f.Id)).ToDictionaryAsync(f => f.Id); - - _logger.LogInfo("Fetched existing entities and parents for new items."); - - // --- Step 4: Aggregate all affected ProjectIds for Security Check --- - var affectedProjectIds = new HashSet(); - - // From buildings being created/updated - buildingsToCreateDto.ForEach(b => affectedProjectIds.Add(b.ProjectId)); - foreach (var b in existingBuildings.Values) { affectedProjectIds.Add(b.ProjectId); } - - // From floors being created/updated - foreach (var f in floorsToCreateDto) { if (parentBuildingsForNewFloors.TryGetValue(f.BuildingId, out var b)) affectedProjectIds.Add(b.ProjectId); } - foreach (var f in existingFloors.Values) { if (f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } - - // From work areas being created/updated - foreach (var wa in workAreasToCreateDto) { if (parentFloorsForNewWorkAreas.TryGetValue(wa.FloorId, out var f) && f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); } - foreach (var wa in existingWorkAreas.Values) { if (wa.Floor?.Building != null) affectedProjectIds.Add(wa.Floor.Building.ProjectId); } - - // Security Check against the complete list of affected projects - var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProjectInfra, loggedInEmployee.Id); - if (!hasPermission) - { - _logger.LogWarning("Access DENIED for user {UserId} trying to manage infrastructure for projects.", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to manage infrastructure for one or more of the specified projects.", 403); - } - - // --- Step 5: Process all logic IN MEMORY, tracking changes --- - - // Process Buildings - var createdBuildings = new List(); - foreach (var dto in buildingsToCreateDto) - { - var newBuilding = _mapper.Map(dto); - newBuilding.TenantId = tenantId; - createdBuildings.Add(newBuilding); - } - foreach (var dto in buildingsToUpdateDto) { if (existingBuildings.TryGetValue(dto.Id!.Value, out var b)) _mapper.Map(dto, b); } - - // Process Floors - var createdFloors = new List(); - foreach (var dto in floorsToCreateDto) - { - var newFloor = _mapper.Map(dto); - newFloor.TenantId = tenantId; - createdFloors.Add(newFloor); - } - foreach (var dto in floorsToUpdateDto) { if (existingFloors.TryGetValue(dto.Id!.Value, out var f)) _mapper.Map(dto, f); } - - // Process WorkAreas - var createdWorkAreas = new List(); - foreach (var dto in workAreasToCreateDto) - { - var newWorkArea = _mapper.Map(dto); - newWorkArea.TenantId = tenantId; - createdWorkAreas.Add(newWorkArea); - } - foreach (var dto in workAreasToUpdateDto) { if (existingWorkAreas.TryGetValue(dto.Id!.Value, out var wa)) _mapper.Map(dto, wa); } - - // --- Step 6: Save all database changes in a SINGLE TRANSACTION --- - if (createdBuildings.Any()) _context.Buildings.AddRange(createdBuildings); - if (createdFloors.Any()) _context.Floor.AddRange(createdFloors); - if (createdWorkAreas.Any()) _context.WorkAreas.AddRange(createdWorkAreas); - - if (_context.ChangeTracker.HasChanges()) - { - await _context.SaveChangesAsync(); - _logger.LogInfo("Database save successful."); - } - - // --- Step 7: Update Cache using the aggregated ProjectIds (Non-blocking) --- - var finalProjectIds = affectedProjectIds.ToList(); - if (finalProjectIds.Any()) - { - _ = Task.Run(async () => - { - try - { - _logger.LogInfo("Queuing background cache update for {ProjectCount} projects.", finalProjectIds.Count); - // Assuming your cache service has a method to handle this. - await _cache.RemoveProjectsAsync(finalProjectIds); - _logger.LogInfo("Background cache update task completed for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred during the background cache update task for projects: {ProjectIds}", string.Join(", ", finalProjectIds)); - } - }); - } - - // --- Step 8: Prepare and return a clear response --- - var responseVm = new { /* ... as before ... */ }; - return ApiResponse.SuccessResponse(responseVm, "Infrastructure changes processed successfully.", 200); - } - catch (Exception ex) - { - _logger.LogError(ex, "An unexpected error occurred in ManageProjectInfraAsync."); - return ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500); - } - } - /// /// Creates or updates a batch of work items. /// This method is optimized to perform all database operations in a single, atomic transaction. @@ -1512,60 +1243,88 @@ namespace Marco.Pms.Services.Service return ApiResponse>.SuccessResponse(responseList, message, 200); } + public async Task DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + // 1. Fetch the task and its parent data in a single query. + // This is still a major optimization, avoiding a separate query for the floor/building. + WorkItem? task = await _context.WorkItems + .AsNoTracking() // Use AsNoTracking because we will re-attach for deletion later. + .Include(t => t.WorkArea) + .ThenInclude(wa => wa!.Floor) + .ThenInclude(f => f!.Building) + .FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); - //public async Task DeleteProjectTask(Guid id) - //{ - // var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - // List workAreaIds = new List(); - // 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); + // 2. Guard Clause: Handle non-existent task. + if (task == null) + { + _logger.LogWarning("Attempted to delete a non-existent task with ID {WorkItemId}", id); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse("Task not found.", $"A task with ID {id} was not found.", 404) + }; + } - // var floorId = task.WorkArea?.FloorId; - // var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId); + // 3. Guard Clause: Prevent deletion if work has started. + if (task.CompletedWork > 0) + { + double percentage = Math.Round((task.CompletedWork / task.PlannedWork) * 100, 2); + _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted.", task.Id, percentage); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse($"Task is {percentage}% complete and cannot be deleted.", "Deletion failed because the task has progress.", 400) + }; + } + // 4. Guard Clause: Efficiently check if the task is assigned in a separate, optimized query. + // AnyAsync() is highly efficient and translates to a `SELECT TOP 1` or `EXISTS` in SQL. + bool isAssigned = await _context.TaskAllocations.AnyAsync(t => t.WorkItemId == id); + if (isAssigned) + { + _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id); + return new ServiceResponse + { + Response = ApiResponse.ErrorResponse("Task is currently assigned and cannot be deleted.", "Deletion failed because the task is assigned to an employee.", 400) + }; + } - // workAreaIds.Add(task.WorkAreaId); - // var projectId = floor?.Building?.ProjectId; + // --- Success Path: All checks passed, proceed with deletion --- - // 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.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.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)); + var building = task.WorkArea?.Floor?.Building; + var notification = new + { + LoggedInUserId = loggedInEmployee.Id, + Keyword = "WorkItem", + WorkAreaIds = new[] { task.WorkAreaId }, + Message = $"Task Deleted in Building: {building?.Name ?? "N/A"}, on Floor: {task.WorkArea?.Floor?.FloorName ?? "N/A"}, in Area: {task.WorkArea?.AreaName ?? "N/A"} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}" + }; - // } - // } - // else - // { - // _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id); - // } - // return Ok(ApiResponse.SuccessResponse(new { }, "Task deleted successfully", 200)); - //} + // 5. Perform the database deletion. + // We must attach a new instance or the original one without AsNoTracking. + // Since we used AsNoTracking, we create a 'stub' entity for deletion. + // This is more efficient than re-querying. + _context.WorkItems.Remove(new WorkItem { Id = task.Id }); + await _context.SaveChangesAsync(); + _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); + // 6. Perform cache operations concurrently. + var cacheTasks = new List + { + _cache.DeleteWorkItemByIdAsync(task.Id) + }; + + if (building?.ProjectId != null) + { + cacheTasks.Add(_cache.DeleteProjectByIdAsync(building.ProjectId)); + } + await Task.WhenAll(cacheTasks); + + // 7. Return the final success response. + return new ServiceResponse + { + Notification = notification, + Response = ApiResponse.SuccessResponse(new { id = task.Id }, "Task deleted successfully.", 200) + }; + } #endregion #region =================================================================== Helper Functions =================================================================== diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index f1c89cc..0c7c964 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -23,6 +23,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee); Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee); Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee); + Task DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee); } }