using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TaskTrackingSystem.Database.AppDbContextModels; using TaskTrackingSystem.Shared.Models.Menu; using MenuEntity = TaskTrackingSystem.Database.AppDbContextModels.Menu; namespace TaskTrackingSystem.WebApi.Features.Menu { public class MenuService { private readonly AppDbContext _db; public MenuService(AppDbContext db) { _db = db; } public async Task> GetMenuTreeByRoleIdAsync(long roleId) { var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == roleId && r.IsDeleted != true); if (role == null) { return new List(); } return await GetMenuTreeForRoleAsync(role.Id); } public async Task> GetMenuTreeByRoleNameAsync(string roleName) { if (string.IsNullOrWhiteSpace(roleName)) { return new List(); } var role = await _db.Roles.FirstOrDefaultAsync(r => r.Name == roleName && r.IsDeleted != true); if (role == null) { return new List(); } return await GetMenuTreeForRoleAsync(role.Id); } private async Task> GetMenuTreeForRoleAsync(long roleId) { var visibleMenus = await _db.Menus .Where(m => !m.IsDeleted && m.Visible) .OrderBy(m => m.OrderNo) .ToListAsync(); var assignedMenuIds = await _db.RoleMenus .Where(rm => rm.RoleId == roleId && !rm.IsDeleted) .Select(rm => rm.MenuId) .ToListAsync(); var assignedPermissionMenuIds = await _db.RolePermissions .Where(rp => rp.RoleId == roleId && !rp.IsDeleted) .Select(rp => rp.Permission.MenuId) .ToListAsync(); var allowedMenuIds = assignedMenuIds .Concat(assignedPermissionMenuIds) .Distinct() .ToHashSet(); if (allowedMenuIds.Count == 0) { return new List(); } var visibleMenuLookup = visibleMenus.ToDictionary(m => m.MenuId); var expandedMenuIds = ExpandWithAncestors(allowedMenuIds, visibleMenuLookup); var filteredMenus = visibleMenus .Where(m => expandedMenuIds.Contains(m.MenuId)) .OrderBy(m => m.OrderNo) .ToList(); return BuildHierarchy(filteredMenus); } private static List BuildHierarchy(List menus) { if (menus.Count == 0) { return new List(); } var menuLookup = menus.ToDictionary(m => m.MenuId); var dtoLookup = menus.ToDictionary( m => m.MenuId, m => new MenuDto { MenuCode = m.MenuCode, ParentCode = GetParentCode(m, menuLookup), MenuName = m.MenuName, MenuUrl = m.MenuUrl, OrderNo = m.OrderNo, Icon = m.Icon }); foreach (var dto in dtoLookup.Values) { dto.SubMenus = new List(); } var roots = new List(); foreach (var menu in menus.OrderBy(m => m.OrderNo).ThenBy(m => m.MenuName, StringComparer.OrdinalIgnoreCase)) { var dto = dtoLookup[menu.MenuId]; var parentId = menu.ParentMenuId; if (!parentId.HasValue || !dtoLookup.TryGetValue(parentId.Value, out var parentDto)) { roots.Add(dto); continue; } parentDto.SubMenus.Add(dto); } SortMenuTree(roots); return roots; } private static string GetParentCode(MenuEntity menu, IReadOnlyDictionary menuLookup) { if (!menu.ParentMenuId.HasValue) { return string.Empty; } return menuLookup.TryGetValue(menu.ParentMenuId.Value, out var parent) ? parent.MenuCode : string.Empty; } private static HashSet ExpandWithAncestors(IEnumerable menuIds, IReadOnlyDictionary menuLookup) { var expanded = new HashSet(); foreach (var menuId in menuIds) { var currentId = menuId; while (currentId > 0 && expanded.Add(currentId)) { if (!menuLookup.TryGetValue(currentId, out var currentMenu) || !currentMenu.ParentMenuId.HasValue) { break; } currentId = currentMenu.ParentMenuId.Value; } } return expanded; } private static void SortMenuTree(IList menus) { var ordered = menus .OrderBy(m => m.OrderNo) .ThenBy(m => m.MenuName, StringComparer.OrdinalIgnoreCase) .ToList(); menus.Clear(); foreach (var menu in ordered) { if (menu.SubMenus.Count > 0) { SortMenuTree(menu.SubMenus); } menus.Add(menu); } } public async Task GetMenuVersionAsync(long roleId, string roleName) { if (roleId <= 0 && !string.IsNullOrWhiteSpace(roleName)) { var role = await _db.Roles.FirstOrDefaultAsync(r => r.Name == roleName && r.IsDeleted != true); if (role != null) { roleId = role.Id; } } if (roleId <= 0) { return "0"; } var latestRoleMenuChange = await _db.RoleMenus .Where(rm => rm.RoleId == roleId && !rm.IsDeleted) .Select(rm => (DateTime?)(rm.UpdatedAt ?? rm.CreatedAt)) .MaxAsync(); var latestPermissionChange = await _db.RolePermissions .Where(rp => rp.RoleId == roleId && !rp.IsDeleted) .Select(rp => (DateTime?)(rp.UpdatedAt ?? rp.CreatedAt)) .MaxAsync(); var latestMenuMetadataChange = await _db.Menus .Select(m => (DateTime?)(m.UpdatedAt ?? m.CreatedAt)) .MaxAsync(); var latest = latestRoleMenuChange ?? latestPermissionChange; if (latestPermissionChange.HasValue && (!latest.HasValue || latestPermissionChange.Value > latest.Value)) { latest = latestPermissionChange; } if (latestMenuMetadataChange.HasValue && (!latest.HasValue || latestMenuMetadataChange.Value > latest.Value)) { latest = latestMenuMetadataChange; } return latest.HasValue ? latest.Value.Ticks.ToString() : "0"; } public async Task> GetAllAccessItemsAsync() { var menus = await _db.Menus .Where(m => !m.IsDeleted && m.MenuCode != "ROLE_LAYOUTS" && m.MenuName != "Role Layouts") .OrderBy(m => m.OrderNo) .ToListAsync(); var permissions = await _db.Permissions .Where(p => !p.IsDeleted) .OrderBy(p => p.OrderNo) .ToListAsync(); var menuLookup = menus.ToDictionary(m => m.MenuId); var menuDtoLookup = menus .Select(m => new { m.MenuId, Dto = new AccessMenuDto { MenuCode = m.MenuCode, ParentCode = GetParentCode(m, menuLookup), MenuName = m.MenuName, MenuUrl = m.MenuUrl, OrderNo = m.OrderNo, Icon = m.Icon, Visible = m.Visible } }) .ToList(); var menuDtos = menuDtoLookup.Select(x => x.Dto).ToList(); var dtoLookup = menuDtoLookup.ToDictionary(x => x.MenuId, x => x.Dto); foreach (var permissionGroup in permissions.GroupBy(p => p.MenuId)) { if (!dtoLookup.TryGetValue(permissionGroup.Key, out var dto)) { continue; } foreach (var permission in permissionGroup.OrderBy(p => p.OrderNo).ThenBy(p => p.ActionName, StringComparer.OrdinalIgnoreCase)) { dto.Permissions.Add(new AccessPermissionDto { PermissionId = permission.PermissionId.ToString(), PermissionCode = permission.PermissionCode, ParentMenuCode = menuLookup.TryGetValue(permission.MenuId, out var parentMenu) ? parentMenu.MenuCode : string.Empty, ActionName = permission.ActionName, ApiName = permission.ApiName, Visible = permission.Visible, OrderNo = permission.OrderNo }); } } return menuDtos; } public async Task> GetCurrentAccessCodesAsync(long roleId, string roleName) { if (roleId <= 0 && !string.IsNullOrWhiteSpace(roleName)) { var role = await _db.Roles.FirstOrDefaultAsync(r => r.Name == roleName && r.IsDeleted != true); if (role != null) { roleId = role.Id; } } if (roleId <= 0) { return new List(); } var validMenuCodes = await _db.Menus .Where(m => !m.IsDeleted) .Select(m => m.MenuCode) .ToListAsync(); var validPermissionCodes = await _db.Permissions .Where(p => !p.IsDeleted) .Select(p => p.PermissionCode) .ToListAsync(); var validCodes = validMenuCodes .Concat(validPermissionCodes) .ToHashSet(StringComparer.OrdinalIgnoreCase); var menuCodes = await _db.RoleMenus .Where(rm => rm.RoleId == roleId && !rm.IsDeleted) .Select(rm => rm.Menu.MenuCode) .ToListAsync(); var permissionCodes = await _db.RolePermissions .Where(rp => rp.RoleId == roleId && !rp.IsDeleted) .Select(rp => rp.Permission.PermissionCode) .ToListAsync(); return menuCodes .Concat(permissionCodes) .Where(code => validCodes.Contains(code)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); } } }