using AutoMapper; using Marco.Pms.CacheHelper; using Marco.Pms.Model.AppMenu; using Marco.Pms.Model.Dtos.AppMenu; using Marco.Pms.Model.Entitlements; 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 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) { _userHelper = userHelper; _sideBarMenuHelper = sideBarMenuHelper; _mapper = mapper; _logger = logger; _serviceScopeFactory = serviceScopeFactory; tenantId = userHelper.GetTenantId(); } /// /// 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. Create scoped permission service // - Avoid capturing scoped services directly in controller ctor when they depend on per-request state. using var scope = _serviceScopeFactory.CreateScope(); var permissionService = scope.ServiceProvider.GetRequiredService(); // 4. 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); // 5. 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); // 6. 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 AddMenuItemAsync([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 master menu list based on enabled features for the current tenant. /// /// List of master menu items available for the tenant [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.")); } } [HttpGet("get/menu-mobile")] public async Task GetAppSideBarMenuForobile() { // 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(); try { // Step 2: Fetch all menu sections for the tenant var menus = await _sideBarMenuHelper.GetAllMenuSectionsAsync(tenantId); if (!(menus?.Any() ?? false)) { menus = new List { MenuStaticMaster.menu }; } List response = new List(); foreach (var menu in menus) { var allowedItems = new List(); foreach (var item in menu.Items) { // --- Item permission check --- if (!item.PermissionIds.Any()) { MenuSectionApplicationVM menuVM = new MenuSectionApplicationVM { Id = item.Id, Name = item.Text, Available = true, MobileLink = item.MobileLink, }; response.Add(menuVM); if (item.Submenu?.Any() == true) { var allowedSubmenus = new List(); foreach (var subItem in item.Submenu) { if (!subItem.PermissionIds.Any()) { MenuSectionApplicationVM subMenuVM = new MenuSectionApplicationVM { Id = subItem.Id, Name = subItem.Text, Available = true, MobileLink = subItem.MobileLink }; response.Add(subMenuVM); continue; } var subMenuPermissionIds = subItem.PermissionIds .Select(Guid.Parse) .ToList(); bool isSubItemAllowed = await _permissions.HasPermissionAny(subMenuPermissionIds, employeeId); if (isSubItemAllowed) { MenuSectionApplicationVM subMenuVM = new MenuSectionApplicationVM { Id = subItem.Id, Name = subItem.Text, Available = true, MobileLink = subItem.MobileLink }; response.Add(subMenuVM); } } // Replace with filtered submenus item.Submenu = allowedSubmenus; } } 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(); foreach (var subItem in item.Submenu) { if (!subItem.PermissionIds.Any()) { MenuSectionApplicationVM subMenuVM = new MenuSectionApplicationVM { Id = subItem.Id, Name = subItem.Text, Available = true, MobileLink = subItem.MobileLink }; response.Add(subMenuVM); continue; } var subMenuPermissionIds = subItem.PermissionIds .Select(Guid.Parse) .ToList(); bool isSubItemAllowed = await _permissions.HasPermissionAny(subMenuPermissionIds, employeeId); if (isSubItemAllowed) { MenuSectionApplicationVM subMenuVM = new MenuSectionApplicationVM { Id = subItem.Id, Name = subItem.Text, Available = true, MobileLink = subItem.MobileLink, }; response.Add(subMenuVM); } } // Replace with filtered submenus item.Submenu = allowedSubmenus; } MenuSectionApplicationVM menuVM = new MenuSectionApplicationVM { Id = item.Id, Name = item.Text, Available = true, MobileLink = item.MobileLink }; response.Add(menuVM); } } } // Replace with filtered items menu.Items = allowedItems; } var viewDocumentTask = Task.Run(async () => { using var taskScope = _serviceScopeFactory.CreateScope(); var permissions = taskScope.ServiceProvider.GetRequiredService(); return await permissions.HasPermission(PermissionsMaster.ViewDocument, employeeId); }); var uploadDocumentTask = Task.Run(async () => { using var taskScope = _serviceScopeFactory.CreateScope(); var permissions = taskScope.ServiceProvider.GetRequiredService(); return await permissions.HasPermission(PermissionsMaster.UploadDocument, employeeId); }); var verifyDocumentTask = Task.Run(async () => { using var taskScope = _serviceScopeFactory.CreateScope(); var permissions = taskScope.ServiceProvider.GetRequiredService(); return await permissions.HasPermission(PermissionsMaster.VerifyDocument, employeeId); }); var downloadDocumentTask = Task.Run(async () => { using var taskScope = _serviceScopeFactory.CreateScope(); var permissions = taskScope.ServiceProvider.GetRequiredService(); return await permissions.HasPermission(PermissionsMaster.DownloadDocument, employeeId); }); await Task.WhenAll(viewDocumentTask, uploadDocumentTask, verifyDocumentTask, downloadDocumentTask); var viewDocument = viewDocumentTask.Result; var uploadDocument = uploadDocumentTask.Result; var verifyDocument = verifyDocumentTask.Result; var downloadDocument = downloadDocumentTask.Result; if (viewDocument || uploadDocument || verifyDocument || downloadDocument) { response.Add(new MenuSectionApplicationVM { Id = Guid.Parse("443d6444-250b-4164-89fd-bcd7cedd9e43"), Name = "Documents", Available = true, MobileLink = "/dashboard/document-main-page" }); } response.Add(new MenuSectionApplicationVM { Id = Guid.Parse("7faddfe7-994b-4712-91c2-32ba44129d9b"), Name = "Service Projects", Available = true, MobileLink = "/dashboard/service-projects" }); response.Add(new MenuSectionApplicationVM { Id = Guid.Parse("5fab4b88-c9a0-417b-aca2-130980fdb0cf"), Name = "Infra Projects", Available = true, MobileLink = "/dashboard/infra-projects" }); // Step 3: Log success response = response.Where(ms => !string.IsNullOrWhiteSpace(ms.MobileLink)).ToList(); _logger.LogInfo("Fetched sidebar menu successfully. Tenant: {TenantId}, EmployeeId: {EmployeeId}, SectionsReturned: {Count}", tenantId, employeeId, menus.Count); return Ok(ApiResponse.SuccessResponse(response, "Sidebar menu fetched successfully", 200)); } 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.ErrorResponse("Server Error", "An unexpected error occurred while fetching the sidebar menu.", 500)); } } } }