diff --git a/Marco.Pms.Helpers/CacheHelper/ProjectCache.cs b/Marco.Pms.Helpers/CacheHelper/ProjectCache.cs index bee37af..ec5598b 100644 --- a/Marco.Pms.Helpers/CacheHelper/ProjectCache.cs +++ b/Marco.Pms.Helpers/CacheHelper/ProjectCache.cs @@ -407,10 +407,19 @@ namespace Marco.Pms.Helpers #region=================================================================== WorkItem Cache Helper =================================================================== - public async Task> GetWorkItemsByWorkAreaIdsFromCache(List workAreaIds) + public async Task> GetWorkItemsByWorkAreaIdsFromCache(List workAreaIds, List serviceIds) { var stringWorkAreaIds = workAreaIds.Select(wa => wa.ToString()).ToList(); - var filter = Builders.Filter.In(w => w.WorkAreaId, stringWorkAreaIds); + + var filterBuilder = Builders.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> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId) + public async Task> GetWorkItemDetailsByWorkAreaFromCache(Guid workAreaId, List serviceIds) { - var filter = Builders.Filter.Eq(p => p.WorkAreaId, workAreaId.ToString()); + var filterBuilder = Builders.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 diff --git a/Marco.Pms.Model/MongoDBModels/Masters/ActivityGroupMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/Masters/ActivityGroupMasterMongoDB.cs new file mode 100644 index 0000000..1b3169b --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/Masters/ActivityGroupMasterMongoDB.cs @@ -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; } + } +} diff --git a/Marco.Pms.Model/MongoDBModels/Masters/ServiceMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/Masters/ServiceMasterMongoDB.cs new file mode 100644 index 0000000..008b745 --- /dev/null +++ b/Marco.Pms.Model/MongoDBModels/Masters/ServiceMasterMongoDB.cs @@ -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; + } +} diff --git a/Marco.Pms.Model/MongoDBModels/Project/ActivityMasterMongoDB.cs b/Marco.Pms.Model/MongoDBModels/Project/ActivityMasterMongoDB.cs index ecb88c1..becb30f 100644 --- a/Marco.Pms.Model/MongoDBModels/Project/ActivityMasterMongoDB.cs +++ b/Marco.Pms.Model/MongoDBModels/Project/ActivityMasterMongoDB.cs @@ -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; } } } diff --git a/Marco.Pms.Model/ViewModels/Master/ActivityGroupDetailsListVM.cs b/Marco.Pms.Model/ViewModels/Master/ActivityGroupDetailsListVM.cs new file mode 100644 index 0000000..dc84e2e --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Master/ActivityGroupDetailsListVM.cs @@ -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? Activities { get; set; } + } +} diff --git a/Marco.Pms.Model/ViewModels/Master/ServiceDetailsListVM.cs b/Marco.Pms.Model/ViewModels/Master/ServiceDetailsListVM.cs new file mode 100644 index 0000000..5181ea1 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/Master/ServiceDetailsListVM.cs @@ -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? ActivityGroups { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/MasterController.cs b/Marco.Pms.Services/Controllers/MasterController.cs index 17d0620..393d423 100644 --- a/Marco.Pms.Services/Controllers/MasterController.cs +++ b/Marco.Pms.Services/Controllers/MasterController.cs @@ -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 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 CreateService([FromBody] ServiceMasterDto serviceMasterDto) { @@ -125,10 +131,10 @@ namespace Marco.Pms.Services.Controllers #region =================================================================== Activity Group APIs =================================================================== [HttpGet("activity-group/list")] - public async Task GetActivityGroups() + public async Task 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 GetActivitiesMaster() + public async Task GetActivitiesMaster([FromQuery] Guid? activityGroupId) { - Guid tenantId = _userHelper.GetTenantId(); - var activities = await _context.ActivityMasters.Where(c => c.TenantId == tenantId && c.IsActive == true).ToListAsync(); - List activitiesVM = new List(); - foreach (var activity in activities) - { - var checkList = await _context.ActivityCheckLists.Where(c => c.TenantId == tenantId && c.ActivityId == activity.Id).ToListAsync(); - List checkListVM = new List(); - 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.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 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.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 = new List(); - - if (createActivity.CheckList != null) - { - List activityCheckList = new List(); - 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.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 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 activityCheckLists = await _context.ActivityCheckLists.AsNoTracking().Where(c => c.ActivityId == activity.Id).ToListAsync(); - List checkListVM = new List(); - - if (createActivity.CheckList != null) - { - - var newCheckIds = createActivity.CheckList.Select(c => c.Id); - - List updateCheckList = new List(); - List deleteCheckList = new List(); - 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.SuccessResponse(activityVM, "activity updated successfully", 200)); - } - _logger.LogWarning("Activity {ActivityId} not found", id); - return NotFound(ApiResponse.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 DeleteActivity(Guid Id) + public async Task 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.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 diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index ea230e8..c48cc5f 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -342,7 +342,7 @@ namespace MarcoBMS.Services.Controllers } [HttpGet("tasks/{workAreaId}")] - public async Task GetWorkItems(Guid workAreaId) + public async Task 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}")] diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 60005ab..3584c8e 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -599,11 +599,11 @@ namespace Marco.Pms.Services.Helpers #region ======================================================= WorkItem Cache ======================================================= - public async Task?> GetWorkItemsByWorkAreaIds(List workAreaIds) + public async Task?> GetWorkItemsByWorkAreaIds(List workAreaIds, List 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?> GetWorkItemDetailsByWorkArea(Guid workAreaId) + public async Task?> GetWorkItemDetailsByWorkArea(Guid workAreaId, List serviceIds) { try { - var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId); + var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId, serviceIds); if (workItems.Count > 0) { return workItems; diff --git a/Marco.Pms.Services/Helpers/GeneralHelper.cs b/Marco.Pms.Services/Helpers/GeneralHelper.cs index 289c87e..c983b84 100644 --- a/Marco.Pms.Services/Helpers/GeneralHelper.cs +++ b/Marco.Pms.Services/Helpers/GeneralHelper.cs @@ -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 { diff --git a/Marco.Pms.Services/Helpers/ReportHelper.cs b/Marco.Pms.Services/Helpers/ReportHelper.cs index 5a174e6..49c3000 100644 --- a/Marco.Pms.Services/Helpers/ReportHelper.cs +++ b/Marco.Pms.Services/Helpers/ReportHelper.cs @@ -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()); if (workItems == null || !workItems.Any()) { workItems = await _context.WorkItems diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 4df070b..aa79f33 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -269,7 +269,7 @@ namespace Marco.Pms.Services.MappingProfiles #region ======================================================= Activity Group Master ======================================================= - CreateMap() + CreateMap() .ForMember( dest => dest.Id, // Explicitly and safely convert nullable Guid to non-nullable Guid diff --git a/Marco.Pms.Services/Service/MasterService.cs b/Marco.Pms.Services/Service/MasterService.cs index f8a9a73..1fae12b 100644 --- a/Marco.Pms.Services/Service/MasterService.cs +++ b/Marco.Pms.Services/Service/MasterService.cs @@ -261,6 +261,118 @@ namespace Marco.Pms.Services.Service return ApiResponse.ErrorResponse("An error occurred while fetching services", ex.Message, 500); } } + + /// + /// 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. + /// + /// The employee currently logged in. This parameter is available for future authorization or logging extensions. + /// The unique identifier of the tenant for which to retrieve the data. + /// An ApiResponse containing a list of ServiceDetailsListVM or an error message in case of failure. + public async Task> 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 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(); + + 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>(checklistForActivity) + }; + }).ToList() + }).ToList() + }; + return response; + }).ToList(); + + _logger.LogInfo("Successfully processed and mapped {ServiceCount} services for TenantId: {TenantId}", Vm.Count, tenantId); + + return ApiResponse.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.ErrorResponse("An internal server error occurred while fetching service details.", 500); + } + } public async Task> 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> GetActivityGroupsAsync(Employee loggedInEmployee, Guid tenantId) + public async Task> 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(ag)) .ToListAsync(); @@ -586,17 +705,25 @@ namespace Marco.Pms.Services.Service #endregion #region =================================================================== Activity APIs =================================================================== - public async Task> GetActivitiesMasterAsync(Employee loggedInEmployee, Guid tenantId) + public async Task> 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) diff --git a/Marco.Pms.Services/Service/ProjectServices.cs b/Marco.Pms.Services/Service/ProjectServices.cs index ab72c8e..f88516c 100644 --- a/Marco.Pms.Services/Service/ProjectServices.cs +++ b/Marco.Pms.Services/Service/ProjectServices.cs @@ -1160,7 +1160,7 @@ namespace Marco.Pms.Services.Service /// The ID of the current tenant. /// The current authenticated employee for permission checks. /// An ApiResponse containing a list of work items or an error. - public async Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee) + public async Task> 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(); + + 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.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.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(); + return await permission.HasProjectPermission(loggedInEmployee, projectId); + }); + var hasGenericViewInfraPermissionTask = Task.Run(async () => + { + using var taskScope = _serviceScopeFactory.CreateScope(); + var permission = taskScope.ServiceProvider.GetRequiredService(); + 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.ErrorResponse("Access Denied", "You do not have sufficient permissions to view these work items.", 403); + } + + List serviceIds = new List(); + 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.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.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.SuccessResponse(workItemVMs, $"{workItemVMs.Count} tasks fetched successfully.", 200); diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IMasterService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IMasterService.cs index f79d62f..a53edb0 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IMasterService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IMasterService.cs @@ -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> DeleteGlobalServiceAsync(Guid id, bool active, Employee loggedInEmployee, Guid tenantId); #endregion + #region =================================================================== Service Master APIs =================================================================== Task> GetServicesAsync(Employee loggedInEmployee, Guid tenantId); + Task> GetServiceDetailsListAsync(Guid? serviceId, Employee loggedInEmployee, Guid tenantId); Task> CreateServiceAsync(ServiceMasterDto serviceMasterDto, Employee loggedInEmployee, Guid tenantId); Task> UpdateServiceAsync(Guid id, ServiceMasterDto serviceMasterDto, Employee loggedInEmployee, Guid tenantId); Task> DeleteServiceAsync(Guid id, bool active, Employee loggedInEmployee, Guid tenantId); #endregion + + #region =================================================================== Activity APIs =================================================================== + Task> GetActivitiesMasterAsync(Guid? activityGroupId, Employee loggedInEmployee, Guid tenantId); + Task> CreateActivityAsync(CreateActivityMasterDto createActivity, Employee loggedInEmployee, Guid tenantId); + Task> UpdateActivityAsync(Guid id, CreateActivityMasterDto createActivity, Employee loggedInEmployee, Guid tenantId); + Task> DeleteActivityAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId); + + #endregion + #region =================================================================== Activity Group Master APIs =================================================================== - Task> GetActivityGroupsAsync(Employee loggedInEmployee, Guid tenantId); + Task> GetActivityGroupsAsync(Guid? serviceId, Employee loggedInEmployee, Guid tenantId); Task> CreateActivityGroupAsync(ActivityGroupDto activityGroupDto, Employee loggedInEmployee, Guid tenantId); Task> UpdateActivityGroupAsync(Guid id, ActivityGroupDto activityGroupDto, Employee loggedInEmployee, Guid tenantId); Task> DeleteActivityGroupAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId); diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs index 6d06559..7679d95 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IProjectServices.cs @@ -25,7 +25,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces Task> GetProjectByEmployeeBasicAsync(Guid employeeId, Guid tenantId, Employee loggedInEmployee); Task> GetInfraDetailsAsync(Guid projectId, Guid tenantId, Employee loggedInEmployee); - Task> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee); + Task> GetWorkItemsAsync(Guid workAreaId, Guid? serviceId, Guid tenantId, Employee loggedInEmployee); Task> GetTasksByEmployeeAsync(Guid employeeId, DateTime fromDate, DateTime toDate, Guid tenantId, Employee loggedInEmployee); Task ManageProjectInfraAsync(List infraDtos, Guid tenantId, Employee loggedInEmployee); Task>> CreateProjectTaskAsync(List workItemDtos, Guid tenantId, Employee loggedInEmployee);