Optimized the side menu APIs

This commit is contained in:
ashutosh.nehete 2025-12-05 19:06:21 +05:30
parent 0960702f0c
commit 852ddc7e02
10 changed files with 301 additions and 396 deletions

View File

@ -248,7 +248,6 @@ namespace Marco.Pms.DataAccess.Data
public DbSet<InvoiceAttachmentType> InvoiceAttachmentTypes { get; set; }
#endregion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

View File

@ -9,6 +9,7 @@ namespace Marco.Pms.CacheHelper
public class SidebarMenuHelper
{
private readonly IMongoCollection<MenuSection> _collection;
private readonly IMongoCollection<WebMenuSection> _webCollection;
private readonly ILogger<SidebarMenuHelper> _logger;
public SidebarMenuHelper(IConfiguration configuration, ILogger<SidebarMenuHelper> logger)
@ -19,8 +20,75 @@ namespace Marco.Pms.CacheHelper
var client = new MongoClient(mongoUrl);
var database = client.GetDatabase(mongoUrl.DatabaseName);
_collection = database.GetCollection<MenuSection>("Menus");
_webCollection = database.GetCollection<WebMenuSection>("WebSideMenus");
}
public async Task<List<WebMenuSection>> GetAllWebMenuSectionsAsync(Guid tenantId)
{
try
{
var filter = Builders<WebMenuSection>.Filter.Eq(e => e.TenantId, tenantId);
var result = await _webCollection
.Find(filter)
.ToListAsync();
if (result.Any())
{
return result;
}
tenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26");
filter = Builders<WebMenuSection>.Filter.Eq(e => e.TenantId, tenantId);
result = await _webCollection
.Find(filter)
.ToListAsync();
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while fetching Web Menu Sections.");
return new List<WebMenuSection>();
}
}
public async Task<WebMenuSection?> CreateWebMenuSectionAsync(WebMenuSection section)
{
try
{
await _webCollection.InsertOneAsync(section);
return section;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while adding Web Menu Section.");
return null;
}
}
public async Task<WebMenuSection?> AddWebMenuItemAsync(Guid sectionId, List<WebSideMenuItem> newItems)
{
try
{
var filter = Builders<WebMenuSection>.Filter.Eq(s => s.Id, sectionId);
var update = Builders<WebMenuSection>.Update.PushEach(s => s.Items, newItems);
var result = await _webCollection.UpdateOneAsync(filter, update);
if (result.ModifiedCount > 0)
{
return await _webCollection.Find(s => s.Id == sectionId).FirstOrDefaultAsync();
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error adding menu item.");
return null;
}
}
public async Task<MenuSection?> CreateMenuSectionAsync(MenuSection section)
{
try

View File

@ -0,0 +1,19 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Marco.Pms.Model.AppMenu
{
public class WebMenuSection
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; } = Guid.NewGuid();
public string? Header { get; set; }
public string? Title { get; set; }
public List<WebSideMenuItem> Items { get; set; } = new List<WebSideMenuItem>();
[BsonRepresentation(BsonType.String)]
public Guid TenantId { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Marco.Pms.Model.AppMenu
{
public class WebSideMenuItem
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; } = Guid.NewGuid();
[BsonRepresentation(BsonType.String)]
public Guid? ParentMenuId { get; set; }
public string? Text { get; set; }
public string? Icon { get; set; }
public bool Available { get; set; } = true;
public string Link { get; set; } = string.Empty;
[BsonRepresentation(BsonType.String)]
public List<Guid> PermissionIds { get; set; } = new List<Guid>();
}
}

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.Dtos.AppMenu
{
public class CreateWebMenuSectionDto
{
public required string Header { get; set; }
public required string Title { get; set; }
public List<CreateWebSideMenuItemDto> Items { get; set; } = new List<CreateWebSideMenuItemDto>();
}
}

View File

@ -0,0 +1,15 @@
namespace Marco.Pms.Model.Dtos.AppMenu
{
public class CreateWebSideMenuItemDto
{
public Guid? Id { get; set; }
public Guid? ParentMenuId { get; set; }
public string? Text { get; set; }
public string? Icon { get; set; }
public bool Available { get; set; } = true;
public string Link { get; set; } = string.Empty;
// Changed from string → List<string>
public List<Guid> PermissionIds { get; set; } = new List<Guid>();
}
}

View File

@ -0,0 +1,12 @@
namespace Marco.Pms.Model.ViewModels.AppMenu
{
public class WebMenuSectionVM
{
public Guid Id { get; set; }
public string? Header { get; set; }
public string? Name { get; set; }
public List<WebSideMenuItemVM> Items { get; set; } = new List<WebSideMenuItemVM>();
public Guid TenantId { get; set; }
}
}

View File

@ -0,0 +1,12 @@
namespace Marco.Pms.Model.ViewModels.AppMenu
{
public class WebSideMenuItemVM
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Icon { get; set; }
public bool Available { get; set; }
public string? Link { get; set; }
public List<WebSideMenuItemVM> Submenu { get; set; } = new List<WebSideMenuItemVM>();
}
}

View File

@ -53,16 +53,123 @@ namespace Marco.Pms.Services.Controllers
tenantId = userHelper.GetTenantId();
}
[HttpGet("get/menu")]
public async Task<IActionResult> GetAppSideBarMenuAsync()
{
// Correlation ID for distributed tracing across services and logs.
var correlationId = HttpContext.TraceIdentifier;
/// <summary>
/// Creates a new sidebar menu section for the tenant.
/// Only accessible by root users or for the super tenant.
/// </summary>
/// <param name="menuSectionDto">The data for the new menu section.</param>
/// <returns>HTTP response with result of the operation.</returns>
_logger.LogInfo("GetAppSideBarMenuAsync started. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId);
try
{
// Step 1: Validate tenant context
if (tenantId == Guid.Empty)
{
_logger.LogWarning("GetAppSideBarMenuAsync rejected: Invalid TenantId. CorrelationId: {CorrelationId}", correlationId);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid TenantId", "The tenant identifier provided is invalid or missing.", 400));
}
// Step 2: Resolve current employee context for permission checks
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var employeeId = loggedInEmployee.Id;
// Step 3: Create scoped permission service (avoid capturing scoped services in controller)
using var scope = _serviceScopeFactory.CreateScope();
var permissions = scope.ServiceProvider.GetRequiredService<PermissionServices>();
_logger.LogDebug("GetAppSideBarMenuAsync resolved employee context. EmployeeId: {EmployeeId}, TenantId: {TenantId}, CorrelationId: {CorrelationId}",
employeeId, tenantId, correlationId);
// Step 4: Fetch all menu sections for tenant (null-safe check)
var menus = await _sideBarMenuHelper.GetAllWebMenuSectionsAsync(tenantId);
if (menus == null || !menus.Any())
{
_logger.LogInfo("GetAppSideBarMenuAsync: No menu sections found. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
tenantId, correlationId);
return Ok(ApiResponse<object>.SuccessResponse(new List<WebMenuSectionVM>(), "No sidebar menu sections configured for this tenant.", 200));
}
_logger.LogDebug("GetAppSideBarMenuAsync loaded {MenuSectionCount} menu sections. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
menus.Count, tenantId, correlationId);
// Step 5: Filter and build menu response with permission checks
var response = new List<WebMenuSectionVM>();
foreach (var menuSection in menus)
{
var sectionVM = _mapper.Map<WebMenuSectionVM>(menuSection);
if (menuSection.Items == null)
{
_logger.LogDebug("Skipping menu section with null items. SectionId: {SectionId}, TenantId: {TenantId}", menuSection.Id, tenantId);
continue;
}
foreach (var menuItem in menuSection.Items)
{
// Skip items without permission check if required
if (menuItem.PermissionIds.Any())
{
var hasPermission = await permissions.HasPermissionAny(menuItem.PermissionIds, employeeId);
if (!hasPermission)
{
_logger.LogDebug("Menu item access denied due to missing permissions. ItemId: {ItemId}, EmployeeId: {EmployeeId}, TenantId: {TenantId}",
menuItem.Id, employeeId, tenantId);
continue;
}
}
var itemVM = _mapper.Map<WebSideMenuItemVM>(menuItem);
if (menuItem.ParentMenuId.HasValue)
{
sectionVM.Items.Where(i => i.Id == menuItem.ParentMenuId.Value).FirstOrDefault()?.Submenu.Add(itemVM);
}
else
{
sectionVM.Items.Add(itemVM);
}
}
// Only include sections with at least one accessible item
if (sectionVM.Items.Any())
{
response.Add(sectionVM);
}
}
_logger.LogInfo("GetAppSideBarMenuAsync completed successfully. TenantId: {TenantId}, EmployeeId: {EmployeeId}, OriginalSections: {OriginalCount}, FilteredSections: {FilteredCount}, CorrelationId: {CorrelationId}",
tenantId, employeeId, menus.Count, response.Count, correlationId);
return Ok(ApiResponse<object>.SuccessResponse(response, $"Sidebar menu fetched successfully. {response.Count} sections returned.", 200));
}
catch (OperationCanceledException)
{
_logger.LogWarning("GetAppSideBarMenuAsync cancelled. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
tenantId, correlationId);
return StatusCode(499, ApiResponse<object>.ErrorResponse("Request Cancelled", "The request was cancelled by the client.", 499));
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "GetAppSideBarMenuAsync authorization failed. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
tenantId, correlationId);
return StatusCode(403, (ApiResponse<object>.ErrorResponse("Access Denied", "Insufficient permissions to access menu sections.", 403)));
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error in GetAppSideBarMenuAsync. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
tenantId, correlationId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred while fetching the sidebar menu.", 500));
}
}
[HttpPost("add/sidebar/menu-section")]
public async Task<IActionResult> CreateAppSideBarMenu([FromBody] CreateMenuSectionDto menuSectionDto)
public async Task<IActionResult> CreateAppSideBarMenuAsync([FromBody] CreateWebMenuSectionDto model)
{
// Step 1: Fetch logged-in user
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
@ -76,17 +183,17 @@ namespace Marco.Pms.Services.Controllers
}
// Step 3: Map DTO to entity
var sideMenuSection = _mapper.Map<MenuSection>(menuSectionDto);
var sideMenuSection = _mapper.Map<WebMenuSection>(model);
sideMenuSection.TenantId = tenantId;
try
{
// Step 4: Save entity using helper
sideMenuSection = await _sideBarMenuHelper.CreateMenuSectionAsync(sideMenuSection);
sideMenuSection = await _sideBarMenuHelper.CreateWebMenuSectionAsync(sideMenuSection);
if (sideMenuSection == null)
{
_logger.LogWarning("Failed to create sidebar menu section. Tenant: {TenantId}, Request: {@MenuSectionDto}", tenantId, menuSectionDto);
_logger.LogWarning("Failed to create sidebar menu section. Tenant: {TenantId}, Request: {@MenuSectionDto}", tenantId, model);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid MenuSection", 400));
}
@ -100,86 +207,14 @@ namespace Marco.Pms.Services.Controllers
{
// Step 6: Handle and log unexpected server errors
_logger.LogError(ex, "Unexpected error occurred while creating sidebar menu. Tenant: {TenantId}, EmployeeId: {EmployeeId}, Request: {@MenuSectionDto}",
tenantId, loggedInEmployee.Id, menuSectionDto);
tenantId, loggedInEmployee.Id, model);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server Error", "An unexpected error occurred.", 500));
}
}
/// <summary>
/// Updates an existing sidebar menu section for the tenant.
/// Only accessible by root users or for the super tenant.
/// </summary>
/// <param name="sectionId">The unique identifier of the section to update.</param>
/// <param name="updatedSection">The updated data for the sidebar menu section.</param>
/// <returns>HTTP response with the result of the operation.</returns>
[HttpPut("edit/sidebar/menu-section/{sectionId}")]
public async Task<IActionResult> UpdateMenuSection(Guid sectionId, [FromBody] UpdateMenuSectionDto updatedSection)
{
// Step 1: Fetch logged-in user
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false;
// Step 2: Authorization check
if (!isRootUser && tenantId != superTenantId)
{
_logger.LogWarning("Access denied: User {UserId} attempted to update sidebar menu section {SectionId} in Tenant {TenantId}",
loggedInEmployee.Id, sectionId, tenantId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "User does not have permission.", 403));
}
// Step 3: Validate request
if (sectionId == Guid.Empty || sectionId != updatedSection.Id)
{
_logger.LogWarning("Invalid update request. Tenant: {TenantId}, SectionId: {SectionId}, PayloadId: {PayloadId}, UserId: {UserId}",
tenantId, sectionId, updatedSection.Id, loggedInEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid section ID or mismatched payload.", 400));
}
// Step 4: Map DTO to entity
var menuSectionEntity = _mapper.Map<MenuSection>(updatedSection);
try
{
// Step 5: Perform update operation
var result = await _sideBarMenuHelper.UpdateMenuSectionAsync(sectionId, menuSectionEntity);
if (result == null)
{
_logger.LogWarning("Menu section not found for update. SectionId: {SectionId}, TenantId: {TenantId}, UserId: {UserId}",
sectionId, tenantId, loggedInEmployee.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Menu section not found", 404));
}
// Step 6: Successful update
_logger.LogInfo("Menu section updated successfully. SectionId: {SectionId}, TenantId: {TenantId}, UserId: {UserId}",
sectionId, tenantId, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(result, "Menu section updated successfully"));
}
catch (Exception ex)
{
// Step 7: Unexpected server error
_logger.LogError(ex, "Failed to update menu section. SectionId: {SectionId}, TenantId: {TenantId}, UserId: {UserId}, Payload: {@UpdatedSection}",
sectionId, tenantId, loggedInEmployee.Id, updatedSection);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server error", "An unexpected error occurred while updating the menu section.", 500));
}
}
/// <summary>
/// Adds a new menu item to an existing sidebar menu section.
/// Only accessible by root users or for the super tenant.
/// </summary>
/// <param name="sectionId">The unique identifier of the section the item will be added to.</param>
/// <param name="newItemDto">The details of the new menu item.</param>
/// <returns>HTTP response with the result of the operation.</returns>
[HttpPost("add/sidebar/menus/{sectionId}/items")]
public async Task<IActionResult> AddMenuItem(Guid sectionId, [FromBody] CreateMenuItemDto newItemDto)
[HttpPost("add/sidebar/section/{sectionId}/items")]
public async Task<IActionResult> AddMenuItemAsync(Guid sectionId, [FromBody] List<CreateWebSideMenuItemDto> model)
{
// Step 1: Fetch logged-in user
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
@ -195,7 +230,7 @@ namespace Marco.Pms.Services.Controllers
}
// Step 3: Input validation
if (sectionId == Guid.Empty || newItemDto == null)
if (sectionId == Guid.Empty || model == null)
{
_logger.LogWarning("Invalid AddMenuItem request. Tenant: {TenantId}, SectionId: {SectionId}, UserId: {UserId}",
tenantId, sectionId, loggedInEmployee.Id);
@ -206,10 +241,10 @@ namespace Marco.Pms.Services.Controllers
try
{
// Step 4: Map DTO to entity
var menuItemEntity = _mapper.Map<MenuItem>(newItemDto);
var menuItemEntity = _mapper.Map<List<WebSideMenuItem>>(model);
// Step 5: Perform Add operation
var result = await _sideBarMenuHelper.AddMenuItemAsync(sectionId, menuItemEntity);
var result = await _sideBarMenuHelper.AddWebMenuItemAsync(sectionId, menuItemEntity);
if (result == null)
{
@ -229,318 +264,12 @@ namespace Marco.Pms.Services.Controllers
{
// Step 7: Handle unexpected errors
_logger.LogError(ex, "Error occurred while adding menu item. SectionId: {SectionId}, TenantId: {TenantId}, UserId: {UserId}, Payload: {@NewItemDto}",
sectionId, tenantId, loggedInEmployee.Id, newItemDto);
sectionId, tenantId, loggedInEmployee.Id, model);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server error", "An unexpected error occurred while adding the menu item.", 500));
}
}
/// <summary>
/// Updates an existing menu item inside a sidebar menu section.
/// Only accessible by root users or within the super tenant.
/// </summary>
/// <param name="sectionId">The ID of the sidebar menu section.</param>
/// <param name="itemId">The ID of the menu item to update.</param>
/// <param name="updatedMenuItem">The updated menu item details.</param>
/// <returns>HTTP response with the result of the update operation.</returns>
[HttpPut("edit/sidebar/{sectionId}/items/{itemId}")]
public async Task<IActionResult> UpdateMenuItem(Guid sectionId, Guid itemId, [FromBody] UpdateMenuItemDto updatedMenuItem)
{
// Step 1: Fetch logged-in user
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false;
// Step 2: Authorization check
if (!isRootUser && tenantId != superTenantId)
{
_logger.LogWarning("Access denied: User {UserId} attempted to update menu item {ItemId} in Section {SectionId}, Tenant {TenantId}",
loggedInEmployee.Id, itemId, sectionId, tenantId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "User does not have permission.", 403));
}
// Step 3: Input validation
if (sectionId == Guid.Empty || itemId == Guid.Empty || updatedMenuItem == null || updatedMenuItem.Id != itemId)
{
_logger.LogWarning("Invalid UpdateMenuItem request. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, UserId: {UserId}",
tenantId, sectionId, itemId, loggedInEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid section ID, item ID, or menu item payload.", 400));
}
// Step 4: Map DTO to entity
var menuItemEntity = _mapper.Map<MenuItem>(updatedMenuItem);
try
{
// Step 5: Perform update operation
var result = await _sideBarMenuHelper.UpdateMenuItemAsync(sectionId, itemId, menuItemEntity);
if (result == null)
{
_logger.LogWarning("Menu item not found or update failed. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, UserId: {UserId}",
tenantId, sectionId, itemId, loggedInEmployee.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Menu item not found or update failed.", 404));
}
// Step 6: Success log
_logger.LogInfo("Menu item updated successfully. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, UserId: {UserId}",
tenantId, sectionId, itemId, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(result, "Sidebar menu item updated successfully."));
}
catch (Exception ex)
{
// ✅ Step 7: Handle server errors
_logger.LogError(ex, "Error occurred while updating menu item. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, UserId: {UserId}, Payload: {@UpdatedMenuItem}",
tenantId, sectionId, itemId, loggedInEmployee.Id, updatedMenuItem);
return StatusCode(
500,
ApiResponse<object>.ErrorResponse("Server Error", "An unexpected error occurred while updating the menu item.", 500)
);
}
}
/// <summary>
/// Adds a new sub-menu item to an existing menu item inside a sidebar menu section.
/// Only accessible by root users or within the super tenant.
/// </summary>
/// <param name="sectionId">The ID of the sidebar menu section.</param>
/// <param name="itemId">The ID of the parent menu item.</param>
/// <param name="newSubItem">The details of the new sub-menu item.</param>
/// <returns>HTTP response with the result of the add operation.</returns>
[HttpPost("add/sidebar/menus/{sectionId}/items/{itemId}/subitems")]
public async Task<IActionResult> AddSubMenuItem(Guid sectionId, Guid itemId, [FromBody] CreateSubMenuItemDto newSubItem)
{
// Step 1: Fetch logged-in user
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false;
// Step 2: Authorization check
if (!isRootUser && tenantId != superTenantId)
{
_logger.LogWarning("Access denied: User {UserId} attempted to add sub-menu item in Section {SectionId}, MenuItem {ItemId}, Tenant {TenantId}",
loggedInEmployee.Id, sectionId, itemId, tenantId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "User does not have permission.", 403));
}
// Step 3: Validate input
if (sectionId == Guid.Empty || itemId == Guid.Empty || newSubItem == null)
{
_logger.LogWarning("Invalid AddSubMenuItem request. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, UserId: {UserId}",
tenantId, sectionId, itemId, loggedInEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid section ID, item ID, or sub-menu item payload.", 400));
}
try
{
// Step 4: Map DTO to entity
var subMenuItemEntity = _mapper.Map<SubMenuItem>(newSubItem);
// Step 5: Perform add operation
var result = await _sideBarMenuHelper.AddSubMenuItemAsync(sectionId, itemId, subMenuItemEntity);
if (result == null)
{
_logger.LogWarning("Parent menu item not found. Failed to add sub-menu item. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, UserId: {UserId}",
tenantId, sectionId, itemId, loggedInEmployee.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Parent menu item not found.", 404));
}
// Step 6: Success logging
_logger.LogInfo("Sub-menu item added successfully. Tenant: {TenantId}, SectionId: {SectionId}, ParentItemId: {ItemId}, SubItemId: {SubItemId}, UserId: {UserId}",
tenantId, sectionId, itemId, result.Id, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(result, "Sub-menu item added successfully."));
}
catch (Exception ex)
{
// Step 7: Handle unexpected errors
_logger.LogError(ex, "Error occurred while adding sub-menu item. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, UserId: {UserId}, Payload: {@NewSubItem}",
tenantId, sectionId, itemId, loggedInEmployee.Id, newSubItem);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server Error", "An unexpected error occurred while adding the sub-menu item.", 500));
}
}
/// <summary>
/// Updates an existing sub-menu item inside a sidebar menu section.
/// Only accessible by root users or within the super tenant.
/// </summary>
/// <param name="sectionId">The ID of the sidebar menu section.</param>
/// <param name="itemId">The ID of the parent menu item.</param>
/// <param name="subItemId">The ID of the sub-menu item to update.</param>
/// <param name="updatedSubMenuItem">The updated sub-menu item details.</param>
/// <returns>HTTP response with the result of the update operation.</returns>
[HttpPut("edit/sidebar/{sectionId}/items/{itemId}/subitems/{subItemId}")]
public async Task<IActionResult> UpdateSubmenuItem(Guid sectionId, Guid itemId, Guid subItemId, [FromBody] UpdateSubMenuItemDto updatedSubMenuItem)
{
// Step 1: Fetch logged-in user
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false;
// Step 2: Authorization check
if (!isRootUser && tenantId != superTenantId)
{
_logger.LogWarning("Access denied: User {UserId} attempted to update sub-menu {SubItemId} under MenuItem {ItemId} in Section {SectionId}, Tenant {TenantId}",
loggedInEmployee.Id, subItemId, itemId, sectionId, tenantId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied", "User does not have permission.", 403));
}
// Step 3: Input validation
if (sectionId == Guid.Empty || itemId == Guid.Empty || subItemId == Guid.Empty || updatedSubMenuItem == null || updatedSubMenuItem.Id != subItemId)
{
_logger.LogWarning("Invalid UpdateSubMenuItem request. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, SubItemId: {SubItemId}, UserId: {UserId}",
tenantId, sectionId, itemId, subItemId, loggedInEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid section ID, menu item ID, sub-item ID, or payload mismatch.", 400));
}
try
{
// Step 4: Map DTO to entity
var subMenuEntity = _mapper.Map<SubMenuItem>(updatedSubMenuItem);
// Step 5: Perform update operation
var result = await _sideBarMenuHelper.UpdateSubmenuItemAsync(sectionId, itemId, subItemId, subMenuEntity);
if (result == null)
{
_logger.LogWarning("Sub-menu item not found or update failed. Tenant: {TenantId}, SectionId: {SectionId}, ItemId: {ItemId}, SubItemId: {SubItemId}, UserId: {UserId}",
tenantId, sectionId, itemId, subItemId, loggedInEmployee.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Sub-menu item not found.", 404));
}
// Step 6: Log success
_logger.LogInfo("Sub-menu item updated successfully. Tenant: {TenantId}, SectionId: {SectionId}, MenuItemId: {ItemId}, SubItemId: {SubItemId}, UserId: {UserId}",
tenantId, sectionId, itemId, subItemId, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(result, "Sub-menu item updated successfully."));
}
catch (Exception ex)
{
// Step 7: Handle unexpected errors
_logger.LogError(ex, "Error occurred while updating sub-menu item. Tenant: {TenantId}, SectionId: {SectionId}, MenuItemId: {ItemId}, SubItemId: {SubItemId}, UserId: {UserId}, Payload: {@UpdatedSubMenuItem}",
tenantId, sectionId, itemId, subItemId, loggedInEmployee.Id, updatedSubMenuItem);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server Error", "An unexpected error occurred while updating the sub-menu item.", 500));
}
}
/// <summary>
/// Fetches the sidebar menu for the current tenant and filters items based on employee permissions.
/// </summary>
/// <returns>The sidebar menu with only the items/sub-items the employee has access to.</returns>
[HttpGet("get/menu")]
public async Task<IActionResult> GetAppSideBarMenu()
{
// Step 1: Get logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var employeeId = loggedInEmployee.Id;
using var scope = _serviceScopeFactory.CreateScope();
var _permissions = scope.ServiceProvider.GetRequiredService<PermissionServices>();
try
{
// Step 2: Fetch all menu sections for the tenant
var menus = await _sideBarMenuHelper.GetAllMenuSectionsAsync(tenantId);
if (!(menus?.Any() ?? false))
{
menus = new List<MenuSection>
{
MenuStaticMaster.menu
};
}
foreach (var menu in menus)
{
var allowedItems = new List<MenuItem>();
foreach (var item in menu.Items)
{
// --- Item permission check ---
if (!item.PermissionIds.Any())
{
allowedItems.Add(item);
}
else
{
// Convert permission string IDs to GUIDs
var menuPermissionIds = item.PermissionIds
.Select(Guid.Parse)
.ToList();
bool isAllowed = await _permissions.HasPermissionAny(menuPermissionIds, employeeId);
// If allowed, filter its submenus as well
if (isAllowed)
{
if (item.Submenu?.Any() == true)
{
var allowedSubmenus = new List<SubMenuItem>();
foreach (var subItem in item.Submenu)
{
if (!subItem.PermissionIds.Any())
{
allowedSubmenus.Add(subItem);
continue;
}
var subMenuPermissionIds = subItem.PermissionIds
.Select(Guid.Parse)
.ToList();
bool isSubItemAllowed = await _permissions.HasPermissionAny(subMenuPermissionIds, employeeId);
if (isSubItemAllowed)
{
allowedSubmenus.Add(subItem);
}
}
// Replace with filtered submenus
item.Submenu = allowedSubmenus;
}
allowedItems.Add(item);
}
}
}
// Replace with filtered items
menu.Items = allowedItems;
}
// Step 3: Log success
_logger.LogInfo("Fetched sidebar menu successfully. Tenant: {TenantId}, EmployeeId: {EmployeeId}, SectionsReturned: {Count}",
tenantId, employeeId, menus.Count);
var response = _mapper.Map<List<MenuSectionVM>>(menus);
return Ok(ApiResponse<object>.SuccessResponse(response, "Sidebar menu fetched successfully"));
}
catch (Exception ex)
{
// Step 4: Handle unexpected errors
_logger.LogError(ex, "Error occurred while fetching sidebar menu. Tenant: {TenantId}, EmployeeId: {EmployeeId}",
tenantId, employeeId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server Error", "An unexpected error occurred while fetching the sidebar menu.", 500));
}
}
/// <summary>
/// Retrieves the master menu list based on enabled features for the current tenant.
/// </summary>

View File

@ -34,6 +34,7 @@ using Marco.Pms.Model.ServiceProject;
using Marco.Pms.Model.TenantModels;
using Marco.Pms.Model.TenantModels.MongoDBModel;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.AppMenu;
using Marco.Pms.Model.ViewModels.Collection;
using Marco.Pms.Model.ViewModels.Directory;
using Marco.Pms.Model.ViewModels.DocumentManager;
@ -564,19 +565,38 @@ namespace Marco.Pms.Services.MappingProfiles
#region ======================================================= AppMenu =======================================================
CreateMap<CreateMenuSectionDto, MenuSection>();
CreateMap<CreateWebMenuSectionDto, WebMenuSection>();
CreateMap<UpdateMenuSectionDto, MenuSection>();
CreateMap<MenuSection, MenuSectionVM>()
.ForMember(
dest => dest.Name,
opt => opt.MapFrom(src => src.Title));
CreateMap<WebMenuSection, WebMenuSectionVM>()
.ForMember(
dest => dest.Name,
opt => opt.MapFrom(src => src.Title))
.ForMember(
dest => dest.Items,
opt => opt.MapFrom(src => new List<WebSideMenuItem>()));
CreateMap<CreateMenuItemDto, MenuItem>();
CreateMap<CreateWebSideMenuItemDto, WebSideMenuItem>()
.ForMember(
dest => dest.Id,
opt => opt.MapFrom(src => src.Id.HasValue ? src.Id.Value : Guid.NewGuid()));
CreateMap<UpdateMenuItemDto, MenuItem>();
CreateMap<MenuItem, MenuItemVM>()
.ForMember(
dest => dest.Name,
opt => opt.MapFrom(src => src.Text));
CreateMap<WebSideMenuItem, WebSideMenuItemVM>()
.ForMember(
dest => dest.Name,
opt => opt.MapFrom(src => src.Text));
CreateMap<CreateSubMenuItemDto, SubMenuItem>();
CreateMap<UpdateSubMenuItemDto, SubMenuItem>();
CreateMap<SubMenuItem, SubMenuItemVM>()