Added the new API to get detailed list of services and enhanced the get work-items list to filter by service

This commit is contained in:
ashutosh.nehete 2025-09-21 11:36:09 +05:30
parent 0ba4ccf45c
commit 06c5457981
16 changed files with 341 additions and 183 deletions

View File

@ -407,10 +407,19 @@ namespace Marco.Pms.Helpers
#region=================================================================== WorkItem Cache Helper ===================================================================
public async Task<List<WorkItemMongoDB>> GetWorkItemsByWorkAreaIdsFromCache(List<Guid> workAreaIds)
public async Task<List<WorkItemMongoDB>> GetWorkItemsByWorkAreaIdsFromCache(List<Guid> workAreaIds, List<Guid> serviceIds)
{
var stringWorkAreaIds = workAreaIds.Select(wa => wa.ToString()).ToList();
var filter = Builders<WorkItemMongoDB>.Filter.In(w => w.WorkAreaId, stringWorkAreaIds);
var filterBuilder = Builders<WorkItemMongoDB>.Filter;
var filter = filterBuilder.Empty;
filter &= filterBuilder.In(w => w.WorkAreaId, stringWorkAreaIds);
if (serviceIds.Any())
{
var stringServiceIds = serviceIds.Select(s => s.ToString()).ToList();
filter &= filterBuilder.In(w => w.ActivityMaster!.ActivityGroupMaster!.Service!.Id, stringServiceIds);
}
var workItems = await _taskCollection // replace with your actual collection name
.Find(filter)
@ -449,9 +458,17 @@ namespace Marco.Pms.Helpers
}
}
}
public async Task<List<WorkItemMongoDB>> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId)
public async Task<List<WorkItemMongoDB>> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId, List<Guid> serviceIds)
{
var filter = Builders<WorkItemMongoDB>.Filter.Eq(p => p.WorkAreaId, workAreaId.ToString());
var filterBuilder = Builders<WorkItemMongoDB>.Filter;
var filter = filterBuilder.Empty;
filter &= filterBuilder.Eq(p => p.WorkAreaId, workAreaId.ToString());
if (serviceIds.Any())
{
var stringServiceIds = serviceIds.Select(s => s.ToString()).ToList();
filter &= filterBuilder.In(w => w.ActivityMaster!.ActivityGroupMaster!.Service!.Id, stringServiceIds);
}
var options = new UpdateOptions { IsUpsert = true };
var workItems = await _taskCollection

View File

@ -0,0 +1,10 @@
namespace Marco.Pms.Model.MongoDBModels.Masters
{
public class ActivityGroupMasterMongoDB
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public ServiceMasterMongoDB? Service { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.MongoDBModels.Masters
{
public class ServiceMasterMongoDB
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
}

View File

@ -1,9 +1,12 @@
namespace Marco.Pms.Model.MongoDBModels.Project
using Marco.Pms.Model.MongoDBModels.Masters;
namespace Marco.Pms.Model.MongoDBModels.Project
{
public class ActivityMasterMongoDB
{
public string Id { get; set; } = string.Empty;
public string? ActivityName { get; set; }
public string? UnitOfMeasurement { get; set; }
public ActivityGroupMasterMongoDB? ActivityGroupMaster { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.Master
{
public class ActivityGroupDetailsListVM
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public bool IsSystem { get; set; }
public bool IsActive { get; set; }
public List<ActivityVM>? Activities { get; set; }
}
}

View File

@ -0,0 +1,12 @@
namespace Marco.Pms.Model.ViewModels.Master
{
public class ServiceDetailsListVM
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public bool IsSystem { get; set; }
public bool IsActive { get; set; }
public List<ActivityGroupDetailsListVM>? ActivityGroups { get; set; }
}
}

View File

@ -2,12 +2,10 @@
using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Forum;
using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Master;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Forum;
using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Services.Service.ServiceInterfaces;
@ -96,6 +94,14 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(response.StatusCode, response);
}
[HttpGet("service/all/list")]
public async Task<IActionResult> GetServiceDetailsList([FromQuery] Guid? serviceId)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.GetServiceDetailsListAsync(serviceId, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPost("service/create")]
public async Task<IActionResult> CreateService([FromBody] ServiceMasterDto serviceMasterDto)
{
@ -125,10 +131,10 @@ namespace Marco.Pms.Services.Controllers
#region =================================================================== Activity Group APIs ===================================================================
[HttpGet("activity-group/list")]
public async Task<IActionResult> GetActivityGroups()
public async Task<IActionResult> GetActivityGroups([FromQuery] Guid? serviceId)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.GetActivityGroupsAsync(loggedInEmployee, tenantId);
var response = await _masterService.GetActivityGroupsAsync(serviceId, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
@ -162,152 +168,35 @@ namespace Marco.Pms.Services.Controllers
[HttpGet]
[Route("activities")]
public async Task<IActionResult> GetActivitiesMaster()
public async Task<IActionResult> GetActivitiesMaster([FromQuery] Guid? activityGroupId)
{
Guid tenantId = _userHelper.GetTenantId();
var activities = await _context.ActivityMasters.Where(c => c.TenantId == tenantId && c.IsActive == true).ToListAsync();
List<ActivityVM> activitiesVM = new List<ActivityVM>();
foreach (var activity in activities)
{
var checkList = await _context.ActivityCheckLists.Where(c => c.TenantId == tenantId && c.ActivityId == activity.Id).ToListAsync();
List<CheckListVM> checkListVM = new List<CheckListVM>();
if (checkList != null)
{
foreach (ActivityCheckList check in checkList)
{
var checkVM = check.ToCheckListVMFromActivityCheckList(activity.Id, false);
checkListVM.Add(checkVM);
}
}
ActivityVM activityVM = activity.ToActivityVMFromActivityMaster(checkListVM);
activitiesVM.Add(activityVM);
}
_logger.LogInfo("{count} activity records fetched successfully from tenant {tenantId}", activitiesVM.Count, tenantId);
return Ok(ApiResponse<object>.SuccessResponse(activitiesVM, System.String.Format("{0} activity records fetched successfully", activitiesVM.Count), 200));
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.GetActivitiesMasterAsync(activityGroupId, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPost("activity")]
public async Task<IActionResult> CreateActivity([FromBody] CreateActivityMasterDto createActivity)
{
Guid tenantId = _userHelper.GetTenantId();
var employee = await _userHelper.GetCurrentEmployeeAsync();
if (employee.TenantId != tenantId)
{
_logger.LogWarning("User from tenant {employeeTenantId} tries to access data from tenant {tenantId}", employee.TenantId ?? Guid.Empty, tenantId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Current tenant did not match with user's tenant", "Current tenant did not match with user's tenant", 401));
}
var activityMaster = createActivity.ToActivityMasterFromCreateActivityMasterDto(tenantId);
_context.ActivityMasters.Add(activityMaster);
await _context.SaveChangesAsync();
List<CheckListVM> checkListVM = new List<CheckListVM>();
if (createActivity.CheckList != null)
{
List<ActivityCheckList> activityCheckList = new List<ActivityCheckList>();
foreach (var check in createActivity.CheckList)
{
ActivityCheckList checkList = check.ToActivityCheckListFromCreateCheckListDto(tenantId, activityMaster.Id);
activityCheckList.Add(checkList);
}
_context.ActivityCheckLists.AddRange(activityCheckList);
await _context.SaveChangesAsync();
foreach (ActivityCheckList check in activityCheckList)
{
var checkVM = check.ToCheckListVMFromActivityCheckList(activityMaster.Id, false);
checkListVM.Add(checkVM);
}
}
ActivityVM activityVM = activityMaster.ToActivityVMFromActivityMaster(checkListVM);
_logger.LogInfo("activity created successfully from tenant {tenantId}", tenantId);
return Ok(ApiResponse<object>.SuccessResponse(activityVM, "activity created successfully", 200));
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.CreateActivityAsync(createActivity, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpPost("activity/edit/{id}")]
public async Task<IActionResult> UpdateActivity(Guid id, [FromBody] CreateActivityMasterDto createActivity)
{
Guid tenantId = _userHelper.GetTenantId();
var employee = await _userHelper.GetCurrentEmployeeAsync();
ActivityMaster? activity = await _context.ActivityMasters.FirstOrDefaultAsync(x => x.Id == id && x.IsActive == true && x.TenantId == tenantId);
if (activity != null && createActivity.UnitOfMeasurement != null && createActivity.ActivityName != null)
{
activity.ActivityName = createActivity.ActivityName;
activity.UnitOfMeasurement = createActivity.UnitOfMeasurement;
List<ActivityCheckList> activityCheckLists = await _context.ActivityCheckLists.AsNoTracking().Where(c => c.ActivityId == activity.Id).ToListAsync();
List<CheckListVM> checkListVM = new List<CheckListVM>();
if (createActivity.CheckList != null)
{
var newCheckIds = createActivity.CheckList.Select(c => c.Id);
List<ActivityCheckList> updateCheckList = new List<ActivityCheckList>();
List<ActivityCheckList> deleteCheckList = new List<ActivityCheckList>();
if (newCheckIds.Contains(null))
{
foreach (var check in createActivity.CheckList)
{
if (check.Id == null)
{
ActivityCheckList checkList = check.ToActivityCheckListFromCreateCheckListDto(tenantId, activity.Id);
updateCheckList.Add(checkList);
}
}
}
foreach (var check in activityCheckLists)
{
if (newCheckIds.Contains(check.Id))
{
var updatedCheck = createActivity.CheckList.Find(c => c.Id == check.Id);
ActivityCheckList checkList = updatedCheck != null ? updatedCheck.ToActivityCheckListFromCreateCheckListDto(tenantId, activity.Id) : new ActivityCheckList();
updateCheckList.Add(checkList);
}
else
{
deleteCheckList.Add(check);
}
}
_context.ActivityCheckLists.UpdateRange(updateCheckList);
if (deleteCheckList != null)
{
_context.ActivityCheckLists.RemoveRange(deleteCheckList);
}
await _context.SaveChangesAsync();
foreach (ActivityCheckList check in updateCheckList)
{
var checkVM = check.ToCheckListVMFromActivityCheckList(activity.Id, false);
checkListVM.Add(checkVM);
}
}
else if (activityCheckLists != null)
{
_context.ActivityCheckLists.RemoveRange(activityCheckLists);
await _context.SaveChangesAsync();
}
ActivityVM activityVM = activity.ToActivityVMFromActivityMaster(checkListVM);
_logger.LogInfo("activity updated successfully from tenant {tenantId}", tenantId);
return Ok(ApiResponse<object>.SuccessResponse(activityVM, "activity updated successfully", 200));
}
_logger.LogWarning("Activity {ActivityId} not found", id);
return NotFound(ApiResponse<object>.ErrorResponse("Activity not found", "Activity not found", 404));
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.UpdateActivityAsync(id, createActivity, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
[HttpDelete("activity/delete/{id}")]
public async Task<IActionResult> DeleteActivity(Guid Id)
public async Task<IActionResult> DeleteActivity(Guid id, [FromQuery] bool active = false)
{
Guid tenantId = _userHelper.GetTenantId();
var activity = await _context.ActivityMasters.FirstOrDefaultAsync(a => a.Id == Id && a.TenantId == tenantId);
if (activity != null)
{
activity.IsActive = false;
}
await _context.SaveChangesAsync();
_logger.LogInfo("Activity Deleted Successfully from tenant {tenantId}", tenantId);
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Activity Deleted Successfully", 200));
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _masterService.DeleteActivityAsync(id, active, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response);
}
#endregion

View File

@ -342,7 +342,7 @@ namespace MarcoBMS.Services.Controllers
}
[HttpGet("tasks/{workAreaId}")]
public async Task<IActionResult> GetWorkItems(Guid workAreaId)
public async Task<IActionResult> GetWorkItems(Guid workAreaId, [FromQuery] Guid? serviceId)
{
// --- Step 1: Input Validation ---
if (!ModelState.IsValid)
@ -354,7 +354,7 @@ namespace MarcoBMS.Services.Controllers
// --- Step 2: Prepare data without I/O ---
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _projectServices.GetWorkItemsAsync(workAreaId, tenantId, loggedInEmployee);
var response = await _projectServices.GetWorkItemsAsync(workAreaId, serviceId, tenantId, loggedInEmployee);
return StatusCode(response.StatusCode, response);
}
[HttpGet("tasks-employee/{employeeId}")]

View File

@ -599,11 +599,11 @@ namespace Marco.Pms.Services.Helpers
#region ======================================================= WorkItem Cache =======================================================
public async Task<List<WorkItemMongoDB>?> GetWorkItemsByWorkAreaIds(List<Guid> workAreaIds)
public async Task<List<WorkItemMongoDB>?> GetWorkItemsByWorkAreaIds(List<Guid> workAreaIds, List<Guid> serviceIds)
{
try
{
var response = await _projectCache.GetWorkItemsByWorkAreaIdsFromCache(workAreaIds);
var response = await _projectCache.GetWorkItemsByWorkAreaIdsFromCache(workAreaIds, serviceIds);
if (response.Count > 0)
{
return response;
@ -640,11 +640,11 @@ namespace Marco.Pms.Services.Helpers
_logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message);
}
}
public async Task<List<WorkItemMongoDB>?> GetWorkItemDetailsByWorkArea(Guid workAreaId)
public async Task<List<WorkItemMongoDB>?> GetWorkItemDetailsByWorkArea(Guid workAreaId, List<Guid> serviceIds)
{
try
{
var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId);
var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId, serviceIds);
if (workItems.Count > 0)
{
return workItems;

View File

@ -141,8 +141,11 @@ namespace Marco.Pms.Services.Helpers
// Task 1: Fetch the WorkItem entities and their related data.
var workItemsTask = _context.WorkItems
.Include(wi => wi.ActivityMaster)
.ThenInclude(am => am!.ActivityGroup)
.ThenInclude(ag => ag!.Service)
.Include(wi => wi.WorkCategoryMaster)
.Where(wi => wi.WorkAreaId == workAreaId)
.Where(wi => wi.WorkAreaId == workAreaId && wi.ActivityMaster != null && wi.ActivityMaster.ActivityGroup != null
&& wi.ActivityMaster.ActivityGroup.Service != null && wi.WorkCategoryMaster != null)
.AsNoTracking()
.ToListAsync();
@ -189,7 +192,19 @@ namespace Marco.Pms.Services.Helpers
{
Id = wi.ActivityMaster.Id.ToString(),
ActivityName = wi.ActivityMaster.ActivityName,
UnitOfMeasurement = wi.ActivityMaster.UnitOfMeasurement
UnitOfMeasurement = wi.ActivityMaster.UnitOfMeasurement,
ActivityGroupMaster = new ActivityGroupMasterMongoDB
{
Id = wi.ActivityMaster.ActivityGroup!.Id.ToString(),
Name = wi.ActivityMaster.ActivityGroup.Name,
Description = wi.ActivityMaster.ActivityGroup.Description,
Service = new ServiceMasterMongoDB
{
Id = wi.ActivityMaster.ActivityGroup.Service!.Id.ToString(),
Name = wi.ActivityMaster.ActivityGroup.Service.Name,
Description = wi.ActivityMaster.ActivityGroup.Service.Description
}
}
} : null,
WorkCategoryMaster = wi.WorkCategoryMaster != null ? new WorkCategoryMasterMongoDB
{

View File

@ -148,7 +148,7 @@ namespace Marco.Pms.Services.Helpers
var areaIds = areas.Select(a => Guid.Parse(a.Id)).ToList();
// fetch Work Items
workItems = await _cache.GetWorkItemsByWorkAreaIds(areaIds);
workItems = await _cache.GetWorkItemsByWorkAreaIds(areaIds, new List<Guid>());
if (workItems == null || !workItems.Any())
{
workItems = await _context.WorkItems

View File

@ -269,7 +269,7 @@ namespace Marco.Pms.Services.MappingProfiles
#region ======================================================= Activity Group Master =======================================================
CreateMap<ServiceMasterDto, ActivityGroupMaster>()
CreateMap<ActivityGroupDto, ActivityGroupMaster>()
.ForMember(
dest => dest.Id,
// Explicitly and safely convert nullable Guid to non-nullable Guid

View File

@ -261,6 +261,118 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("An error occurred while fetching services", ex.Message, 500);
}
}
/// <summary>
/// Asynchronously retrieves a structured list of services, their activity groups, activities, and associated checklists for a specific tenant.
/// This method fetches data in multiple steps and processes it in-memory, preserving the original business logic.
/// </summary>
/// <param name="loggedInEmployee">The employee currently logged in. This parameter is available for future authorization or logging extensions.</param>
/// <param name="tenantId">The unique identifier of the tenant for which to retrieve the data.</param>
/// <returns>An ApiResponse containing a list of ServiceDetailsListVM or an error message in case of failure.</returns>
public async Task<ApiResponse<object>> GetServiceDetailsListAsync(Guid? serviceId, Employee loggedInEmployee, Guid tenantId)
{
// Log the initiation of the request with structured parameters for better traceability.
_logger.LogInfo("Attempting to fetch service details list for TenantId: {TenantId}", tenantId);
try
{
// --- Step 1: Fetch all relevant activities for the tenant ---
// This query retrieves all active 'ActivityMaster' records for the given tenant that are properly linked to an ActivityGroup and a Service.
// Eager loading (.Include/.ThenInclude) is used to bring in related ActivityGroup and Service entities to prevent N+1 query problems later.
var activityQuery = _context.ActivityMasters
.Include(a => a.ActivityGroup)
.ThenInclude(ag => ag!.Service)
.Where(a => a.TenantId == tenantId && a.IsActive);
if (serviceId.HasValue)
{
activityQuery = activityQuery.Where(a => a.ActivityGroup != null && a.ActivityGroup.Service != null && a.ActivityGroup.Service.Id == serviceId);
}
var activities = await activityQuery
.ToListAsync();
_logger.LogInfo("Step 1 complete: Fetched {ActivityCount} activities for TenantId: {TenantId}", activities.Count, tenantId);
// --- Step 2: Fetch all checklists related to the retrieved activities ---
// To avoid fetching all checklists in the database, we first collect the IDs of the activities from the previous step.
var activityIds = activities.Select(a => a.Id).ToList();
// This second database query fetches only the 'ActivityCheckList' records associated with the relevant activities.
var checkLists = await _context.ActivityCheckLists
.Where(c => c.TenantId == tenantId && activityIds.Contains(c.ActivityId))
.ToListAsync();
_logger.LogInfo("Step 2 complete: Fetched {ChecklistCount} checklists for TenantId: {TenantId}", checkLists.Count, tenantId);
// --- Step 3: Group checklists by activity in memory for efficient lookup ---
// To quickly find all checklists for a given activity, we group the flat list of checklists into a dictionary.
// The key is the ActivityId, and the value is the list of associated checklists.
var groupedChecklists = checkLists
.GroupBy(c => c.ActivityId)
.ToDictionary(g => g.Key, g => g.ToList());
// --- Step 4: Build the hierarchical ViewModel structure in memory ---
// First, get distinct lists of services and activity groups from the already fetched 'activities' collection.
var services = activities.Select(a => a.ActivityGroup!.Service!).Distinct().ToList();
var activityGroupList = activities.Select(a => a.ActivityGroup!).Distinct().ToList();
// Now, construct the final nested ViewModel.
// This part iterates through the distinct services and builds the response object by filtering the in-memory collections.
List<ServiceDetailsListVM> Vm = services.Select(s =>
{
var response = new ServiceDetailsListVM
{
Id = s.Id,
Name = s.Name,
Description = s.Description,
IsActive = s.IsActive,
IsSystem = s.IsSystem,
ActivityGroups = activityGroupList
.Where(ag => ag.ServiceId == s.Id) // Find groups for the current service
.Select(ag => new ActivityGroupDetailsListVM
{
Id = ag.Id,
Name = ag.Name,
Description = ag.Description,
IsActive = ag.IsActive,
IsSystem = ag.IsSystem,
Activities = activities
.Where(a => a.ActivityGroupId == ag.Id) // Find activities for the current group
.Select(a =>
{
// Retrieve the checklists for the current activity from our dictionary lookup.
var checklistForActivity = groupedChecklists.ContainsKey(a.Id)
? groupedChecklists[a.Id]
: new List<ActivityCheckList>();
return new ActivityVM
{
Id = a.Id,
ActivityName = a.ActivityName,
UnitOfMeasurement = a.UnitOfMeasurement,
// BUG FIX: Correctly mapping properties from the activity ('a') itself, not the parent service ('s').
IsActive = a.IsActive,
IsSystem = a.IsSystem,
CheckLists = _mapper.Map<List<CheckListVM>>(checklistForActivity)
};
}).ToList()
}).ToList()
};
return response;
}).ToList();
_logger.LogInfo("Successfully processed and mapped {ServiceCount} services for TenantId: {TenantId}", Vm.Count, tenantId);
return ApiResponse<object>.SuccessResponse(Vm, "Service details list fetched successfully", 200);
}
catch (Exception ex)
{
// If any part of the process fails, log the detailed exception and return a standardized error response.
_logger.LogError(ex, "An error occurred while fetching service details for TenantId: {TenantId}", tenantId);
return ApiResponse<object>.ErrorResponse("An internal server error occurred while fetching service details.", 500);
}
}
public async Task<ApiResponse<object>> CreateServiceAsync(ServiceMasterDto serviceMasterDto, Employee loggedInEmployee, Guid tenantId)
{
_logger.LogInfo("CreateService called with Name: {ServiceName}", serviceMasterDto.Name);
@ -407,16 +519,23 @@ namespace Marco.Pms.Services.Service
#region =================================================================== Activity Group APIs ===================================================================
public async Task<ApiResponse<object>> GetActivityGroupsAsync(Employee loggedInEmployee, Guid tenantId)
public async Task<ApiResponse<object>> GetActivityGroupsAsync(Guid? serviceId, Employee loggedInEmployee, Guid tenantId)
{
_logger.LogInfo("GetActivityGroups called");
try
{
// Step 1: Fetch all activity groups for the tenant
var activityGroups = await _context.ActivityGroupMasters
var activityGroupQuery = _context.ActivityGroupMasters
.Include(ag => ag.Service)
.Where(ag => ag.TenantId == tenantId && ag.IsActive)
.Where(ag => ag.TenantId == tenantId && ag.IsActive);
if (serviceId.HasValue)
{
activityGroupQuery = activityGroupQuery.Where(ag => ag.ServiceId == serviceId);
}
var activityGroups = await activityGroupQuery
.Select(ag => _mapper.Map<ActivityGroupMasterVM>(ag))
.ToListAsync();
@ -586,17 +705,25 @@ namespace Marco.Pms.Services.Service
#endregion
#region =================================================================== Activity APIs ===================================================================
public async Task<ApiResponse<object>> GetActivitiesMasterAsync(Employee loggedInEmployee, Guid tenantId)
public async Task<ApiResponse<object>> GetActivitiesMasterAsync(Guid? activityGroupId, Employee loggedInEmployee, Guid tenantId)
{
_logger.LogInfo("GetActivitiesMaster called");
try
{
// Step 1: Fetch all active activities for the tenant
var activities = await _context.ActivityMasters
var activityQuery = _context.ActivityMasters
.Include(c => c.ActivityGroup)
.ThenInclude(ag => ag!.Service)
.Where(c => c.TenantId == tenantId && c.IsActive)
.Where(c => c.TenantId == tenantId && c.IsActive);
if (activityGroupId.HasValue)
{
activityQuery = activityQuery.Where(a => a.ActivityGroupId == activityGroupId);
}
var activities = await activityQuery
.ToListAsync();
if (activities.Count == 0)

View File

@ -1160,7 +1160,7 @@ namespace Marco.Pms.Services.Service
/// <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)
public async Task<ApiResponse<object>> GetWorkItemsAsync(Guid workAreaId, Guid? serviceId, Guid tenantId, Employee loggedInEmployee)
{
_logger.LogInfo("GetWorkItems called for WorkAreaId: {WorkAreaId} by User: {UserId}", workAreaId, loggedInEmployee.Id);
@ -1170,8 +1170,78 @@ namespace Marco.Pms.Services.Service
try
{
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var projectId = await _context.WorkAreas
.Where(wa => wa.Id == workAreaId && wa.TenantId == tenantId && wa.Floor != null && wa.Floor.Building != null)
.Select(wa => wa.Floor!.Building!.ProjectId)
.FirstOrDefaultAsync();
if (projectId == Guid.Empty)
{
_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 project = await _context.Projects.FirstOrDefaultAsync(p => p.Id == projectId && p.TenantId == tenantId);
if (project == null)
{
return ApiResponse<object>.ErrorResponse("Project not found", "Project not found in database", 404);
}
var hasProjectAccessTask = Task.Run(async () =>
{
using var taskScope = _serviceScopeFactory.CreateScope();
var permission = taskScope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permission.HasProjectPermission(loggedInEmployee, projectId);
});
var hasGenericViewInfraPermissionTask = Task.Run(async () =>
{
using var taskScope = _serviceScopeFactory.CreateScope();
var permission = taskScope.ServiceProvider.GetRequiredService<PermissionServices>();
return await permission.HasPermission(PermissionsMaster.ViewProjectInfra, loggedInEmployee.Id, projectId);
});
await Task.WhenAll(hasProjectAccessTask, hasGenericViewInfraPermissionTask);
var hasProjectAccess = hasProjectAccessTask.Result;
var hasGenericViewInfraPermission = hasGenericViewInfraPermissionTask.Result;
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);
}
List<Guid> serviceIds = new List<Guid>();
if (!serviceId.HasValue)
{
if (project.PromoterId == loggedInEmployee.OrganizationId && project.PMCId == loggedInEmployee.OrganizationId)
{
var projectServices = await _context.ProjectServiceMappings
.Include(ps => ps.Service)
.Where(ps => ps.ProjectId == projectId && ps.Service != null && ps.TenantId == tenantId && ps.IsActive)
.ToListAsync();
serviceIds = projectServices.Select(ps => ps.ServiceId).Distinct().ToList();
}
else
{
var orgProjectMapping = await _context.ProjectOrgMappings
.Include(po => po.ProjectService)
.ThenInclude(ps => ps!.Service)
.Where(po => po.OrganizationId == loggedInEmployee.OrganizationId && po.ProjectService != null
&& po.ProjectService.IsActive && po.ProjectService.ProjectId == projectId && po.ProjectService.Service != null)
.ToListAsync();
serviceIds = orgProjectMapping.Select(po => po.ProjectService!.ServiceId).Distinct().ToList();
}
}
else
{
serviceIds.Add(serviceId.Value);
}
// --- Step 1: Cache-First Strategy ---
var cachedWorkItems = await _cache.GetWorkItemDetailsByWorkArea(workAreaId);
var cachedWorkItems = await _cache.GetWorkItemDetailsByWorkArea(workAreaId, serviceIds);
if (cachedWorkItems != null)
{
_logger.LogInfo("Cache HIT for WorkAreaId: {WorkAreaId}. Returning {Count} items from cache.", workAreaId, cachedWorkItems.Count);
@ -1180,28 +1250,6 @@ namespace Marco.Pms.Services.Service
_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, projectInfo.ProjectId);
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);
@ -1218,6 +1266,8 @@ namespace Marco.Pms.Services.Service
_logger.LogError(ex, "Background cache update failed for WorkAreaId: {WorkAreaId}", workAreaId);
}
var stringServiceIds = serviceIds.Select(s => s.ToString()).ToList();
workItemVMs = workItemVMs.Where(wi => stringServiceIds.Contains(wi.ActivityMaster!.ActivityGroupMaster!.Service!.Id)).ToList();
_logger.LogInfo("{Count} work items fetched successfully for WorkAreaId: {WorkAreaId}", workItemVMs.Count, workAreaId);
return ApiResponse<object>.SuccessResponse(workItemVMs, $"{workItemVMs.Count} tasks fetched successfully.", 200);

View File

@ -1,4 +1,5 @@
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
@ -20,15 +21,26 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
//Task<ApiResponse<object>> DeleteGlobalServiceAsync(Guid id, bool active, Employee loggedInEmployee, Guid tenantId);
#endregion
#region =================================================================== Service Master APIs ===================================================================
Task<ApiResponse<object>> GetServicesAsync(Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> GetServiceDetailsListAsync(Guid? serviceId, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> CreateServiceAsync(ServiceMasterDto serviceMasterDto, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> UpdateServiceAsync(Guid id, ServiceMasterDto serviceMasterDto, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> DeleteServiceAsync(Guid id, bool active, Employee loggedInEmployee, Guid tenantId);
#endregion
#region =================================================================== Activity APIs ===================================================================
Task<ApiResponse<object>> GetActivitiesMasterAsync(Guid? activityGroupId, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> CreateActivityAsync(CreateActivityMasterDto createActivity, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> UpdateActivityAsync(Guid id, CreateActivityMasterDto createActivity, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> DeleteActivityAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);
#endregion
#region =================================================================== Activity Group Master APIs ===================================================================
Task<ApiResponse<object>> GetActivityGroupsAsync(Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> GetActivityGroupsAsync(Guid? serviceId, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> CreateActivityGroupAsync(ActivityGroupDto activityGroupDto, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> UpdateActivityGroupAsync(Guid id, ActivityGroupDto activityGroupDto, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> DeleteActivityGroupAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId);

View File

@ -25,7 +25,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> GetProjectByEmployeeBasicAsync(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<object>> GetWorkItemsAsync(Guid workAreaId, Guid? serviceId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetTasksByEmployeeAsync(Guid employeeId, DateTime fromDate, DateTime toDate, Guid tenantId, Employee loggedInEmployee);
Task<ServiceResponse> ManageProjectInfraAsync(List<InfraDto> infraDtos, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<List<WorkItemVM>>> CreateProjectTaskAsync(List<WorkItemDto> workItemDtos, Guid tenantId, Employee loggedInEmployee);