using AutoMapper; using Marco.Pms.CacheHelper; using Marco.Pms.Model.AppMenu; using Marco.Pms.Model.Dtos.AppMenu; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.AppMenu; using Marco.Pms.Model.ViewModels.DocumentManager; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Marco.Pms.Services.Controllers { [Authorize] [ApiController] [Route("api/[controller]")] public class AppMenuController : ControllerBase { private readonly UserHelper _userHelper; private readonly SidebarMenuHelper _sideBarMenuHelper; private readonly IMapper _mapper; private readonly ILoggingService _logger; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly PermissionServices _permissionService; private readonly Guid tenantId; private static readonly Guid superTenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26"); private static readonly Guid ProjectManagement = Guid.Parse("53176ebf-c75d-42e5-839f-4508ffac3def"); private static readonly Guid ExpenseManagement = Guid.Parse("a4e25142-449b-4334-a6e5-22f70e4732d7"); private static readonly Guid TaskManagement = Guid.Parse("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"); private static readonly Guid EmployeeManagement = Guid.Parse("81ab8a87-8ccd-4015-a917-0627cee6a100"); private static readonly Guid AttendanceManagement = Guid.Parse("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"); private static readonly Guid MastersMangent = Guid.Parse("be3b3afc-6ccf-4566-b9b6-aafcb65546be"); private static readonly Guid DirectoryManagement = Guid.Parse("39e66f81-efc6-446c-95bd-46bff6cfb606"); private static readonly Guid TenantManagement = Guid.Parse("2f3509b7-160d-410a-b9b6-daadd96c986d"); public AppMenuController(UserHelper userHelper, SidebarMenuHelper sideBarMenuHelper, IMapper mapper, ILoggingService logger, IServiceScopeFactory serviceScopeFactory, PermissionServices permissionService) { _userHelper = userHelper; _sideBarMenuHelper = sideBarMenuHelper; _mapper = mapper; _logger = logger; _serviceScopeFactory = serviceScopeFactory; tenantId = userHelper.GetTenantId(); _permissionService = permissionService; } /// /// Returns the sidebar menu for the current tenant and logged-in employee, /// filtered by permission and structured for the web application UI. /// [HttpGet("get/menu")] public async Task GetAppSideBarMenuAsync() { // Correlation ID enables tracing this request across services and logs. var correlationId = HttpContext.TraceIdentifier; // Log the high-level intent and core context up front (no PII, no secrets). _logger.LogInfo("GetAppSideBarMenuAsync started. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); try { // 1. Validate tenant context if (tenantId == Guid.Empty) { _logger.LogWarning("GetAppSideBarMenuAsync rejected due to invalid tenant. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); var error = ApiResponse.ErrorResponse("Invalid Tenant Identifier", "The tenant identifier is missing or invalid. Please verify the tenant context and try again.", 400); return BadRequest(error); } // 2. Resolve current employee context // - This call should throw or return a known result if the user is not authenticated. var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee is null) { _logger.LogWarning("GetAppSideBarMenuAsync failed: current employee not resolved. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); var error = ApiResponse.ErrorResponse("User Context Not Found", "The current user context could not be resolved. Please re-authenticate and try again.", 403); return StatusCode(403, error); } var employeeId = loggedInEmployee.Id; _logger.LogDebug("GetAppSideBarMenuAsync resolved employee context. EmployeeId: {EmployeeId}, TenantId: {TenantId}, CorrelationId: {CorrelationId}", employeeId, tenantId, correlationId); // 3. Preload all permission ids for the employee for efficient in-memory checks var permissionIds = await _permissionService.GetPermissionIdsByEmployeeId(employeeId); _logger.LogDebug("GetAppSideBarMenuAsync loaded {PermissionCount} permissions for EmployeeId: {EmployeeId}, CorrelationId: {CorrelationId}", permissionIds.Count, employeeId, correlationId); // 4. Fetch all menu entries configured for this tenant var menus = await _sideBarMenuHelper.GetAllWebMenuSectionsAsync(tenantId); if (menus == null || !menus.Any()) { _logger.LogInfo("GetAppSideBarMenuAsync: No menu sections found for tenant. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); var emptyResponse = new List(); return Ok(ApiResponse.SuccessResponse(emptyResponse, "No sidebar menu sections are configured for the current tenant.", 200)); } _logger.LogDebug("GetAppSideBarMenuAsync loaded {MenuSectionCount} raw menu records. TenantId: {TenantId}, CorrelationId: {CorrelationId}", menus.Count, tenantId, correlationId); // 5. Build logical menu sections (root + children) and apply permission filtering var responseSections = new List(); // Root container section for the web UI. var rootSection = new WebMenuSectionVM { Id = Guid.Parse("4885d9f4-89b8-447d-9a95-7434b343dfda"), Header = "Main Navigation", Name = "Main Menu" }; // To avoid multiple enumerations and improve readability, materialize once. var menusList = menus.ToList(); foreach (var menu in menusList) { // Skip any non-root menu entry; these will be attached as children. if (menu.ParentMenuId.HasValue) { continue; } var itemVm = _mapper.Map(menu); // If the menu requires any permission, check now. if (menu.PermissionIds.Any()) { var hasMenuPermission = _permissionService.HasPermissionAny(permissionIds, menu.PermissionIds, employeeId); if (!hasMenuPermission) { _logger.LogDebug("Menu item skipped due to insufficient permissions. MenuId: {MenuId}, EmployeeId: {EmployeeId}, CorrelationId: {CorrelationId}", menu.Id, employeeId, correlationId); continue; } } // Resolve submenus only for eligible root menu entries. var subMenus = menusList .Where(m => m.ParentMenuId.HasValue && m.ParentMenuId == menu.Id) .ToList(); foreach (var subMenu in subMenus) { var subMenuVm = _mapper.Map(subMenu); // If the submenu requires permissions, validate before adding. if (subMenu.PermissionIds.Any()) { var hasSubPermission = _permissionService.HasPermissionAny(permissionIds, subMenu.PermissionIds, employeeId); if (!hasSubPermission) { _logger.LogDebug( "Submenu item skipped due to insufficient permissions. MenuId: {MenuId}, ParentMenuId: {ParentMenuId}, EmployeeId: {EmployeeId}, CorrelationId: {CorrelationId}", subMenu.Id, subMenu.ParentMenuId!, employeeId, correlationId); continue; } } // Add the submenu to the root section itemVm.Submenu.Add(subMenuVm); } // Add the root menu item rootSection.Items.Add(itemVm); } if (rootSection.Items.Any()) { responseSections.Add(rootSection); } _logger.LogInfo( "GetAppSideBarMenuAsync completed successfully. TenantId: {TenantId}, EmployeeId: {EmployeeId}, OriginalMenuCount: {OriginalCount}, ReturnedSectionCount: {SectionCount}, CorrelationId: {CorrelationId}", tenantId, employeeId, menusList.Count, responseSections.Count, correlationId); return Ok(ApiResponse.SuccessResponse(responseSections, responseSections.Any() ? $"Sidebar menu fetched successfully. {responseSections.Count} section(s) returned." : "No accessible sidebar menu items were found for the current user.", 200)); } catch (OperationCanceledException) { // This typically indicates client disconnected or explicit cancellation. _logger.LogWarning("GetAppSideBarMenuAsync was cancelled. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); var error = ApiResponse.ErrorResponse("Request Cancelled", "The operation was cancelled, likely due to a client disconnection or explicit cancellation request.", 499); // Non-standard but commonly used in APIs for client closed request. return StatusCode(499, error); } catch (UnauthorizedAccessException ex) { // Handle authorization-related errors explicitly to avoid leaking details. _logger.LogError(ex, "GetAppSideBarMenuAsync authorization failure. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); var error = ApiResponse.ErrorResponse("Access Denied", "You do not have sufficient permissions to access the sidebar menu.", 403); return StatusCode(403, error); } catch (Exception ex) { // Fallback handler for unexpected failures. Keep log detailed, response generic. _logger.LogError(ex, "Unhandled exception in GetAppSideBarMenuAsync. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); var error = ApiResponse.ErrorResponse("Internal Server Error", "An unexpected error occurred while processing the sidebar menu request. Please try again later.", 500); return StatusCode(500, error); } } [HttpPost("add/side-menu")] public async Task AddWebMenuItemAsync([FromBody] List model) { // Step 1: Validate tenant context if (tenantId == Guid.Empty) { _logger.LogWarning("GetAppSideBarMenuAsync rejected: Invalid TenantId."); return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "The tenant identifier provided is invalid or missing.", 400)); } // Step 2: Fetch logged-in user var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; // Step 3: Authorization check if (!isRootUser && tenantId != superTenantId) { _logger.LogWarning("Access denied: User {UserId} attempted to add menu item in Tenant {TenantId}", loggedInEmployee.Id, tenantId); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied", "User does not have permission.", 403)); } // Step 4: Input validation if (model == null) { _logger.LogWarning("Invalid AddMenuItem request. Tenant: {TenantId}, UserId: {UserId}", tenantId, loggedInEmployee.Id); return BadRequest(ApiResponse.ErrorResponse("Invalid section ID or menu item payload.", 400)); } try { // Step 5: Map DTO to entity var menuItemEntity = _mapper.Map>(model); menuItemEntity.ForEach(m => m.TenantId = tenantId); // Step 6: Perform Add operation var result = await _sideBarMenuHelper.AddWebMenuItemAsync(menuItemEntity); if (result == null) { _logger.LogWarning("Menu section not found. Unable to add menu item. TenantId: {TenantId}, UserId: {UserId}", tenantId, loggedInEmployee.Id); return NotFound(ApiResponse.ErrorResponse("Menu section not found", 404)); } // Step 7: Successful addition _logger.LogInfo("Menu items added successfully TenantId: {TenantId}, UserId: {UserId}", tenantId, loggedInEmployee.Id); return Ok(ApiResponse.SuccessResponse(result, "Menu item added successfully")); } catch (Exception ex) { // Step 8: Handle unexpected errors _logger.LogError(ex, "Error occurred while adding menu item. TenantId: {TenantId}, UserId: {UserId}, Payload: {@NewItemDto}", tenantId, loggedInEmployee.Id, model); return StatusCode(500, ApiResponse.ErrorResponse("Server error", "An unexpected error occurred while adding the menu item.", 500)); } } /// /// Retrieves the mobile sidebar menu sections for the authenticated employee within the current tenant, /// filtered by employee permissions and structured for mobile application consumption. /// Supports permission-based access control and tenant isolation. /// /// A filtered list of accessible mobile menu sections or appropriate error response. /// Returns filtered mobile menu sections successfully. /// Invalid tenant identifier provided. /// Employee context not resolved or insufficient permissions. /// Internal server error during menu retrieval or processing. [HttpGet("get/menu-mobile")] public async Task GetAppSideBarMenuForMobileAsync() { // Correlation ID enables distributed tracing across services, middleware, and structured logs. var correlationId = HttpContext.TraceIdentifier; _logger.LogInfo("GetAppSideBarMenuForMobileAsync started. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); try { // 1. Validate tenant isolation - critical for multi-tenant security if (tenantId == Guid.Empty) { _logger.LogWarning("GetAppSideBarMenuForMobileAsync rejected: invalid tenant context. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); var error = ApiResponse.ErrorResponse( "InvalidTenantContext", "Tenant identifier is missing or invalid. Verify tenant context and retry.", 400); return BadRequest(error); } // 2. Resolve authenticated employee context with tenant isolation var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee is null) { _logger.LogWarning("GetAppSideBarMenuForMobileAsync failed: employee context not resolved. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); var error = ApiResponse.ErrorResponse("EmployeeContextNotFound", "Current employee context could not be resolved. Please authenticate and retry.", 403); return StatusCode(403, error); } var employeeId = loggedInEmployee.Id; _logger.LogDebug("GetAppSideBarMenuForMobileAsync resolved employee: EmployeeId: {EmployeeId}, TenantId: {TenantId}, CorrelationId: {CorrelationId}", employeeId, tenantId, correlationId); // 3. Bulk-load employee permissions for efficient in-memory permission checks (avoids N+1 queries) var permissionIds = await _permissionService.GetPermissionIdsByEmployeeId(employeeId); _logger.LogDebug("GetAppSideBarMenuForMobileAsync loaded {PermissionCount} permissions for EmployeeId: {EmployeeId}, TenantId: {TenantId}, CorrelationId: {CorrelationId}", permissionIds?.Count ?? 0, employeeId, tenantId, correlationId); // 4. Fetch tenant-specific mobile menu configuration var allMenus = await _sideBarMenuHelper.GetAllMobileMenuSectionsAsync(tenantId); if (allMenus == null || !allMenus.Any()) { _logger.LogInfo("GetAppSideBarMenuForMobileAsync: no mobile menu sections configured. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); var emptyResponse = new List(); return Ok(ApiResponse.SuccessResponse(emptyResponse, "No mobile sidebar menu sections configured for this tenant.", 200)); } _logger.LogDebug("GetAppSideBarMenuForMobileAsync loaded {MenuCount} raw sections. TenantId: {TenantId}, CorrelationId: {CorrelationId}", allMenus.Count, tenantId, correlationId); // 5. Filter menus by employee permissions (in-memory for performance) var accessibleMenus = new List(); foreach (var menuSection in allMenus) { // Skip permission check for public menu items if (!menuSection.PermissionIds.Any()) { accessibleMenus.Add(menuSection); continue; } // Perform permission intersection check var hasAccess = _permissionService.HasPermissionAny(permissionIds ?? new List(), menuSection.PermissionIds, employeeId); if (hasAccess) { accessibleMenus.Add(menuSection); _logger.LogDebug("GetAppSideBarMenuForMobileAsync granted menu access. MenuId: {MenuId}, EmployeeId: {EmployeeId}, CorrelationId: {CorrelationId}", menuSection.Id, employeeId, correlationId); } else { _logger.LogDebug("GetAppSideBarMenuForMobileAsync denied menu access. MenuId: {MenuId}, EmployeeId: {EmployeeId}, CorrelationId: {CorrelationId}", menuSection.Id, employeeId, correlationId); } } // 6. Defensive mapping with null-safety var response = _mapper.Map>(accessibleMenus); _logger.LogInfo("GetAppSideBarMenuForMobileAsync completed successfully. AccessibleMenus: {AccessibleCount}/{TotalCount}, EmployeeId: {EmployeeId}, TenantId: {TenantId}, CorrelationId: {CorrelationId}", accessibleMenus.Count, allMenus.Count, employeeId, tenantId, correlationId); return Ok(ApiResponse.SuccessResponse(response, $"Mobile sidebar menu fetched successfully ({response?.Count ?? 0} sections accessible).", 200)); } catch (OperationCanceledException) { _logger.LogWarning("GetAppSideBarMenuForMobileAsync cancelled. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); return StatusCode(499, ApiResponse.ErrorResponse("RequestCancelled", "Request was cancelled by client or timeout.", 499)); } catch (UnauthorizedAccessException ex) { _logger.LogError(ex, "GetAppSideBarMenuForMobileAsync authorization failed. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); return StatusCode(403, ApiResponse.ErrorResponse("AuthorizationFailed", "Insufficient permissions to access mobile menu sections.", 403)); } catch (Exception ex) { _logger.LogError(ex, "GetAppSideBarMenuForMobileAsync failed unexpectedly. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId); return StatusCode(500, ApiResponse.ErrorResponse("InternalServerError", "An unexpected error occurred while fetching the mobile sidebar menu. Please contact support if issue persists.", 500)); } } [HttpPost("add/mobile/side-menu")] public async Task AddMobileMenuItemAsync([FromBody] List model) { // Step 1: Validate tenant context if (tenantId == Guid.Empty) { _logger.LogWarning("GetAppSideBarMenuAsync rejected: Invalid TenantId."); return BadRequest(ApiResponse.ErrorResponse("Invalid TenantId", "The tenant identifier provided is invalid or missing.", 400)); } // Step 2: Fetch logged-in user var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; // Step 3: Authorization check if (!isRootUser && tenantId != superTenantId) { _logger.LogWarning("Access denied: User {UserId} attempted to add menu item in Tenant {TenantId}", loggedInEmployee.Id, tenantId); return StatusCode(403, ApiResponse.ErrorResponse("Access Denied", "User does not have permission.", 403)); } // Step 4: Input validation if (model == null) { _logger.LogWarning("Invalid AddMenuItem request. Tenant: {TenantId}, UserId: {UserId}", tenantId, loggedInEmployee.Id); return BadRequest(ApiResponse.ErrorResponse("Invalid section ID or menu item payload.", 400)); } // Step 5: Map DTO to entity var menuItemEntity = _mapper.Map>(model); menuItemEntity.ForEach(m => m.TenantId = tenantId); // Step 6: Perform Add operation var result = await _sideBarMenuHelper.AddMobileMenuItemAsync(menuItemEntity); if (result == null) { _logger.LogWarning("Menu section not found. Unable to add menu item. TenantId: {TenantId}, UserId: {UserId}", tenantId, loggedInEmployee.Id); return NotFound(ApiResponse.ErrorResponse("Menu section not found", 404)); } // Step 7: Successful addition _logger.LogInfo("Menu items added successfully TenantId: {TenantId}, UserId: {UserId}", tenantId, loggedInEmployee.Id); return Ok(ApiResponse.SuccessResponse(result, "Menu item added successfully")); } [HttpGet("get/master-list")] public async Task GetMasterList() { // Start logging scope for observability try { // Get currently logged-in employee var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); _logger.LogInfo("Fetching master list for EmployeeId: {EmployeeId}", loggedInEmployee.Id); using var scope = _serviceScopeFactory.CreateScope(); var generalHelper = scope.ServiceProvider.GetRequiredService(); // Define static master menus for each feature section var featureMenus = new Dictionary> { { EmployeeManagement, new List { new MasterMenuVM { Id = 1, Name = "Application Role" }, new MasterMenuVM { Id = 2, Name = "Job Role" }, new MasterMenuVM { Id = 9, Name = "Document Category" }, new MasterMenuVM { Id = 10, Name = "Document Type" } } }, { ProjectManagement, new List { new MasterMenuVM { Id = 3, Name = "Work Category" }, new MasterMenuVM { Id = 8, Name = "Services" } } }, { DirectoryManagement, new List { new MasterMenuVM { Id = 4, Name = "Contact Category" }, new MasterMenuVM { Id = 5, Name = "Contact Tag" } } }, { ExpenseManagement, new List { new MasterMenuVM { Id = 6, Name = "Expense Category" }, new MasterMenuVM { Id = 7, Name = "Payment Mode" }, new MasterMenuVM { Id = 10, Name = "Payment Adjustment Head" } } } }; if (tenantId == superTenantId) { var superResponse = featureMenus.Values.SelectMany(list => list).OrderBy(r => r.Name).ToList(); _logger.LogInfo("MasterMenu count for TenantId {TenantId}: {Count}", tenantId, superResponse.Count); return Ok(ApiResponse.SuccessResponse(superResponse, "Successfully fetched the master table list", 200)); } // Fetch features enabled for tenant var featureIds = await generalHelper.GetFeatureIdsByTenentIdAsync(tenantId); _logger.LogInfo("Enabled features for TenantId: {TenantId} -> {FeatureIds}", tenantId, string.Join(",", featureIds)); // Aggregate menus based on enabled features var response = featureIds .Where(id => featureMenus.ContainsKey(id)) .SelectMany(id => featureMenus[id]) .OrderBy(r => r.Name) .ToList(); _logger.LogInfo("MasterMenu count for TenantId {TenantId}: {Count}", tenantId, response.Count); return Ok(ApiResponse.SuccessResponse(response, "Successfully fetched the master table list", 200)); } catch (Exception ex) { // Critical error tracking _logger.LogError(ex, "Error occurred while fetching master menu list for TenantId: {TenantId}", tenantId); return StatusCode(500, ApiResponse.ErrorResponse("An unexpected error occurred while fetching master menu list.")); } } } }