diff --git a/Marco.Pms.Helpers/CacheHelper/SidebarMenu.cs b/Marco.Pms.Helpers/CacheHelper/SidebarMenu.cs new file mode 100644 index 0000000..8ee9ed4 --- /dev/null +++ b/Marco.Pms.Helpers/CacheHelper/SidebarMenu.cs @@ -0,0 +1,214 @@ +using Marco.Pms.Model.AppMenu; +using Marco.Pms.Model.Dtos.AppMenu; +using Marco.Pms.Model.ViewModels.AppMenu; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using static System.Collections.Specialized.BitVector32; + +namespace Marco.Pms.CacheHelper +{ + public class SideBarMenu + { + private readonly IMongoCollection _collection; + private readonly ILogger _logger; + + public SideBarMenu(IConfiguration configuration, ILogger logger) + { + _logger = logger; + var connectionString = configuration["MongoDB:ConnectionMenu"]; + var mongoUrl = new MongoUrl(connectionString); + var client = new MongoClient(mongoUrl); + var database = client.GetDatabase(mongoUrl.DatabaseName); + _collection = database.GetCollection("Menus"); + } + + public async Task CreateMenuSectionAsync(MenuSection section) + { + try + { + await _collection.InsertOneAsync(section); + return section; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred while adding MenuSection."); + return null; + } + } + + public async Task UpdateMenuSectionAsync(Guid sectionId, MenuSection updatedSection) + { + try + { + var filter = Builders.Filter.Eq(s => s.Id, sectionId); + + var update = Builders.Update + .Set(s => s.Header, updatedSection.Header) + .Set(s => s.Title, updatedSection.Title) + .Set(s => s.Items, updatedSection.Items); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.ModifiedCount > 0) + { + return await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync(); + } + + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating MenuSection."); + return null; + } + } + + public async Task AddMenuItemAsync(Guid sectionId, MenuItem newItem) + { + try + { + newItem.Id = Guid.NewGuid(); + + var filter = Builders.Filter.Eq(s => s.Id, sectionId); + + var update = Builders.Update.Push(s => s.Items, newItem); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.ModifiedCount > 0) + { + return await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync(); + } + + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding menu item."); + return null; + } + } + + public async Task UpdateMenuItemAsync(Guid sectionId, Guid itemId, MenuItem updatedItem) + { + try + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(s => s.Id, sectionId), + Builders.Filter.ElemMatch(s => s.Items, i => i.Id == itemId) + ); + + var update = Builders.Update + .Set("Items.$.Text", updatedItem.Text) + .Set("Items.$.Icon", updatedItem.Icon) + .Set("Items.$.Available", updatedItem.Available) + .Set("Items.$.Link", updatedItem.Link) + .Set("Items.$.PermissionKeys", updatedItem.PermissionKeys); // <-- updated + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.ModifiedCount > 0) + { + // Re-fetch section and return the updated item + var section = await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync(); + return section?.Items.FirstOrDefault(i => i.Id == itemId); + } + + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating MenuItem."); + return null; + } + } + + public async Task AddSubMenuItemAsync(Guid sectionId, Guid itemId, SubMenuItem newSubItem) + { + try + { + newSubItem.Id = Guid.NewGuid(); + + // Match the MenuSection and the specific MenuItem inside it + var filter = Builders.Filter.And( + Builders.Filter.Eq(s => s.Id, sectionId), + Builders.Filter.ElemMatch(s => s.Items, i => i.Id == itemId) + ); + + // Use positional operator `$` to target matched MenuItem and push into its Submenu + var update = Builders.Update.Push("Items.$.Submenu", newSubItem); + + var result = await _collection.UpdateOneAsync(filter, update); + + if (result.ModifiedCount > 0) + { + return await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync(); + } + + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding submenu item."); + return null; + } + } + + public async Task UpdateSubmenuItemAsync(Guid sectionId, Guid itemId, Guid subItemId, SubMenuItem updatedSub) + { + try + { + var filter = Builders.Filter.Eq(s => s.Id, sectionId); + + var arrayFilters = new List + { + new BsonDocumentArrayFilterDefinition( + new BsonDocument("item._id", itemId.ToString())), + new BsonDocumentArrayFilterDefinition( + new BsonDocument("sub._id", subItemId.ToString())) + }; + + var update = Builders.Update + .Set("Items.$[item].Submenu.$[sub].Text", updatedSub.Text) + .Set("Items.$[item].Submenu.$[sub].Available", updatedSub.Available) + .Set("Items.$[item].Submenu.$[sub].Link", updatedSub.Link) + .Set("Items.$[item].Submenu.$[sub].PermissionKeys", updatedSub.PermissionKeys); // <-- updated + + var options = new UpdateOptions { ArrayFilters = arrayFilters }; + + var result = await _collection.UpdateOneAsync(filter, update, options); + + if (result.ModifiedCount == 0) + return null; + + var updatedSection = await _collection.Find(x => x.Id == sectionId).FirstOrDefaultAsync(); + + var subItem = updatedSection?.Items + .FirstOrDefault(i => i.Id == itemId)? + .Submenu + .FirstOrDefault(s => s.Id == subItemId); + + return subItem; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating SubMenuItem."); + return null; + } + } + + + + public async Task> GetAllMenuSectionsAsync() + { + return await _collection.Find(_ => true).ToListAsync(); + } + + + } +} diff --git a/Marco.Pms.Model/AppMenu/SideBarMenu.cs b/Marco.Pms.Model/AppMenu/SideBarMenu.cs new file mode 100644 index 0000000..cd826f0 --- /dev/null +++ b/Marco.Pms.Model/AppMenu/SideBarMenu.cs @@ -0,0 +1,51 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Marco.Pms.Model.AppMenu +{ + public class MenuSection + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } = Guid.NewGuid(); + + public string? Header { get; set; } + public string? Title { get; set; } + public List Items { get; set; } = new List(); + } + + public class MenuItem + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } = Guid.NewGuid(); + + public string? Text { get; set; } + public string? Icon { get; set; } + public bool Available { get; set; } = true; + + public string? Link { get; set; } + + // Changed from string → List + public List PermissionKeys { get; set; } = new List(); + + public List Submenu { get; set; } = new List(); + } + + public class SubMenuItem + { + [BsonId] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } = Guid.NewGuid(); + + public string? Text { get; set; } + public bool Available { get; set; } = true; + + public string Link { get; set; } = string.Empty; + + // Changed from string → List + public List PermissionKeys { get; set; } = new List(); + } +} + + diff --git a/Marco.Pms.Model/Dtos/AppMenu/SideBarMenuDtco.cs b/Marco.Pms.Model/Dtos/AppMenu/SideBarMenuDtco.cs new file mode 100644 index 0000000..e31759a --- /dev/null +++ b/Marco.Pms.Model/Dtos/AppMenu/SideBarMenuDtco.cs @@ -0,0 +1,42 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Marco.Pms.Model.Dtos.AppMenu +{ + public class MenuSectionDto + { + public string? Header { get; set; } + public string? Title { get; set; } + public List Items { get; set; } = new List(); + } + + public class MenuItemDto + { + public string? Text { get; set; } + public string? Icon { get; set; } + public bool Available { get; set; } = true; + + public string? Link { get; set; } + + // Changed from string → List + public List PermissionKeys { get; set; } = new List(); + + public List Submenu { get; set; } = new List(); + } + + public class SubMenuItemDto + { + public string? Text { get; set; } + public bool Available { get; set; } = true; + + public string Link { get; set; } = string.Empty; + + // Changed from string → List + public List PermissionKeys { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/Marco.Pms.Model/ViewModels/AppMenu/AppMenuVM.cs b/Marco.Pms.Model/ViewModels/AppMenu/AppMenuVM.cs new file mode 100644 index 0000000..43d85c7 --- /dev/null +++ b/Marco.Pms.Model/ViewModels/AppMenu/AppMenuVM.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace Marco.Pms.Model.ViewModels.AppMenu +{ + public class MenuSectionVm + { + public string? Header { get; set; } + public string? Title { get; set; } + public List Items { get; set; } = new(); + } + + public class MenuItemVm + { + public string? Text { get; set; } + public string? Icon { get; set; } + public bool Available { get; set; } = true; + public string? Link { get; set; } + public List Submenu { get; set; } = new(); + } + + public class SubMenuItemVm + { + public string? Text { get; set; } + public bool Available { get; set; } = true; + public string Link { get; set; } = string.Empty; + } +} diff --git a/Marco.Pms.Services/Controllers/AppMenuController.cs b/Marco.Pms.Services/Controllers/AppMenuController.cs new file mode 100644 index 0000000..12dc7ec --- /dev/null +++ b/Marco.Pms.Services/Controllers/AppMenuController.cs @@ -0,0 +1,325 @@ +using AutoMapper; +using Azure; +using Marco.Pms.CacheHelper; +using Marco.Pms.Model.AppMenu; +using Marco.Pms.Model.Dtos.AppMenu; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Entitlements; +using Marco.Pms.Model.Utilities; +using Marco.Pms.Model.ViewModels.AppMenu; +using Marco.Pms.Services.Service; +using Marco.Pms.Services.Service.ServiceInterfaces; +using MarcoBMS.Services.Helpers; +using MarcoBMS.Services.Service; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Driver; +using Org.BouncyCastle.Asn1.Ocsp; +using System.Linq; +using System.Threading.Tasks; +using static System.Collections.Specialized.BitVector32; + +namespace Marco.Pms.Services.Controllers +{ + [Authorize] + [ApiController] + [Route("api/[controller]")] + public class AppMenuController : ControllerBase + { + + private readonly UserHelper _userHelper; + private readonly EmployeeHelper _employeeHelper; + private readonly RolesHelper _rolesHelper; + private readonly SideBarMenu _sideBarMenuHelper; + private readonly IMapper _mapper; + private readonly ILoggingService _logger; + private readonly PermissionServices _permissions; + + public AppMenuController(EmployeeHelper employeeHelper, IProjectServices projectServices, UserHelper userHelper, RolesHelper rolesHelper, SideBarMenu sideBarMenuHelper, IMapper mapper, ILoggingService logger, PermissionServices permissions) + { + + _userHelper = userHelper; + _employeeHelper = employeeHelper; + _rolesHelper = rolesHelper; + _sideBarMenuHelper = sideBarMenuHelper; + _mapper = mapper; + _logger = logger; + _permissions = permissions; + } + + + [HttpPost("sidebar/menu-section")] + public async Task CreateAppSideBarMenu([FromBody] MenuSectionDto MenuSecetion) + { + + + var user = await _userHelper.GetCurrentEmployeeAsync(); + + if (!(user.ApplicationUser?.IsRootUser ?? false)) + { + _logger.LogWarning("Access Denied while creating side menu"); + return StatusCode(403, ApiResponse.ErrorResponse("access denied", "User haven't permission", 403)); + } + + var sideMenuSection = _mapper.Map(MenuSecetion); + try + { + sideMenuSection = await _sideBarMenuHelper.CreateMenuSectionAsync(sideMenuSection); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error Occurred while creating Menu"); + return StatusCode(500, ApiResponse.ErrorResponse("Server Error", ex, 500)); + } + + if (sideMenuSection == null) { + _logger.LogWarning("Error Occurred while creating Menu"); + return BadRequest(ApiResponse.ErrorResponse("Invalid MenuSection", 400)); + } + + _logger.LogInfo("Error Occurred while creating Menu"); + return Ok(ApiResponse.SuccessResponse(sideMenuSection, "Sidebar menu created successfully.", 201)); + + } + + [HttpPut("sidebar/menu-section/{sectionId}")] + public async Task UpdateMenuSection(Guid sectionId, [FromBody] MenuSection updatedSection) + { + if (sectionId == Guid.Empty || updatedSection == null) + { + _logger.LogWarning("Error Occurred while Updating Menu Item"); + return BadRequest(ApiResponse.ErrorResponse("Invalid section ID, item ID, or menu item payload.", 400)); + } + var UpdatedMenuSection = _mapper.Map(updatedSection); + try + { + UpdatedMenuSection = await _sideBarMenuHelper.UpdateMenuSectionAsync(sectionId, UpdatedMenuSection); + + if (UpdatedMenuSection == null) + return NotFound(ApiResponse.ErrorResponse("Menu section not found", 404)); + + return Ok(ApiResponse.SuccessResponse(UpdatedMenuSection, "Menu section updated successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update menu section"); + return StatusCode(500, ApiResponse.ErrorResponse("Server error", ex, 500)); + } + } + + [HttpPost("sidebar/menus/{sectionId}/items")] + public async Task AddMenuItem(Guid sectionId, [FromBody] MenuItemDto newItemDto) + { + if (sectionId == Guid.Empty || newItemDto == null) + return BadRequest(ApiResponse.ErrorResponse("Invalid input", 400)); + + try + { + var menuItem = _mapper.Map(newItemDto); + + var result = await _sideBarMenuHelper.AddMenuItemAsync(sectionId, menuItem); + + if (result == null) + return NotFound(ApiResponse.ErrorResponse("Menu section not found", 404)); + + _logger.LogInfo("Added MenuItem in Section: {SectionId}"); + + return Ok(ApiResponse.SuccessResponse(result, "Menu item added successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred while adding MenuItem inside MenuSection: {SectionId}", sectionId); + return StatusCode(500, ApiResponse.ErrorResponse("Server error", ex, 500)); + } + } + + [HttpPut("sidebar/{sectionId}/items/{itemId}")] + public async Task UpdateMenuItem(Guid sectionId, Guid itemId, [FromBody] MenuItemDto updatedMenuItem) + { + + if (sectionId == Guid.Empty || itemId == Guid.Empty || updatedMenuItem == null) + { + _logger.LogWarning("Error Occurred while Updating Menu Item"); + return BadRequest(ApiResponse.ErrorResponse("Invalid section ID, item ID, or menu item payload.", 400)); + + } + + var sideMenuItem = _mapper.Map(updatedMenuItem); + + try + { + + sideMenuItem = await _sideBarMenuHelper.UpdateMenuItemAsync(sectionId, itemId, sideMenuItem); + + if (sideMenuItem == null) + { + _logger.LogWarning("Error Occurred while Updating SidBar Section:{SectionId} MenuItem:{itemId} "); + return BadRequest(ApiResponse.ErrorResponse("Menu creation failed", 400)); + } + + _logger.LogInfo("SidBar Section{SectionId} MenuItem {itemId} Updated "); + return Ok(ApiResponse.SuccessResponse(sideMenuItem, "Sidebar MenuItem Updated successfully.", 201)); + + } + catch (Exception ex) { + _logger.LogError(ex, "Error Occurred while creating MenuItem"); + return StatusCode(500, ApiResponse.ErrorResponse("Server Error", ex, 500)); + } + + + } + + [HttpPost("sidebar/menus/{sectionId}/items/{itemId}/subitems")] + public async Task AddSubMenuItem(Guid sectionId, Guid itemId, [FromBody] SubMenuItemDto newSubItem) + { + if (sectionId == Guid.Empty || itemId == Guid.Empty || newSubItem == null) + return BadRequest(ApiResponse.ErrorResponse("Invalid input", 400)); + + try + { + var subMenuItem = _mapper.Map(newSubItem); + + var result = await _sideBarMenuHelper.AddSubMenuItemAsync(sectionId, itemId, subMenuItem); + + if (result == null) + { + return NotFound(ApiResponse.ErrorResponse("Menu item not found", 404)); + + } + + _logger.LogInfo("Added SubMenuItem in Section: {SectionId}, MenuItem: {ItemId}"); + return Ok(ApiResponse.SuccessResponse(result, "Submenu item added successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to add submenu item"); + return StatusCode(500, ApiResponse.ErrorResponse("Server error", ex, 500)); + } + } + + + [HttpPut("sidebar/{sectionId}/items/{itemId}/subitems/{subItemId}")] + public async Task UpdateSubmenuItem(Guid sectionId, Guid itemId, Guid subItemId, [FromBody] SubMenuItemDto updatedSubMenuItem) + { + if (sectionId == Guid.Empty || itemId == Guid.Empty || subItemId == Guid.Empty || updatedSubMenuItem == null) + return BadRequest(ApiResponse.ErrorResponse("Invalid input", 400)); + + try + { + var SubMenuItem = _mapper.Map(updatedSubMenuItem); + SubMenuItem = await _sideBarMenuHelper.UpdateSubmenuItemAsync(sectionId, itemId, subItemId, SubMenuItem); + + if (SubMenuItem == null) + return NotFound(ApiResponse.ErrorResponse("Submenu item not found", 404)); + + _logger.LogInfo("SidBar Section{SectionId} MenuItem {itemId} SubMenuItem {subItemId} Updated"); + return Ok(ApiResponse.SuccessResponse(SubMenuItem, "Submenu item updated successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error Occurred while Updating Sub-MenuItem"); + return StatusCode(500, ApiResponse.ErrorResponse("Server Error", ex, 500)); + } + } + + [HttpGet("sidebar/menu-section")] + public async Task GetAppSideBarMenu() + { + var loggedUser = await _userHelper.GetCurrentEmployeeAsync(); + var employeeId = loggedUser.Id; + + try + { + + var menus = await _sideBarMenuHelper.GetAllMenuSectionsAsync(); + + foreach (var menu in menus) + { + var allowedItems = new List(); + + foreach (var item in menu.Items) + { + bool isAllowed = false; + + if (item.PermissionKeys == null || !item.PermissionKeys.Any()) + { + isAllowed = true; + } + else + { + foreach (var pk in item.PermissionKeys) + { + if (Guid.TryParse(pk, out var permissionId)) + { + if (await _permissions.HasPermission(permissionId, employeeId)) + { + isAllowed = true; + break; + } + } + } + } + + if (isAllowed) + { + + if (item.Submenu != null && item.Submenu.Any()) + { + var allowedSubmenus = new List(); + foreach (var sm in item.Submenu) + { + if (sm.PermissionKeys == null || !sm.PermissionKeys.Any()) + { + allowedSubmenus.Add(sm); + } + else + { + foreach (var pk in sm.PermissionKeys) + { + if (Guid.TryParse(pk, out var permissionId)) + { + if (await _permissions.HasPermission(permissionId, employeeId)) + { + allowedSubmenus.Add(sm); + break; + } + } + } + } + } + item.Submenu = allowedSubmenus; + } + + allowedItems.Add(item); + } + } + + menu.Items = allowedItems; + } + + _logger.LogInfo("Fetched Sidebar Menu"); + return Ok(ApiResponse.SuccessResponse(menus, "SideBar Menu Fetched successfully")); + } + catch (Exception ex) { + + _logger.LogError(ex, "Error Occurred while Updating Fetching Menu"); + return StatusCode(500, ApiResponse.ErrorResponse("Server Error", ex, 500)); + } + + + } + + + } + + + + + + + + + +} + diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 00e008a..454ae5e 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -1,6 +1,8 @@ using AutoMapper; using Marco.Pms.Model.Dtos.Expenses; using Marco.Pms.Model.Dtos.Master; +using Marco.Pms.Model.AppMenu; +using Marco.Pms.Model.Dtos.AppMenu; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Dtos.Tenant; using Marco.Pms.Model.Employees; @@ -305,6 +307,12 @@ namespace Marco.Pms.Services.MappingProfiles opt => opt.MapFrom(src => Guid.Parse(src.DocumentId))); #endregion + + #region ======================================================= AppMenu ======================================================= + CreateMap(); + CreateMap(); + CreateMap(); + #endregion } } } diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 702836e..20b2de5 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -197,6 +197,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); #endregion // Singleton services (one instance for the app's lifetime)