User commited on
Commit
4c550c2
·
1 Parent(s): 01be06d

add: new db tables Permission, RoleMenusLegacy, Menu, Fix RolePerms and Perms, adjust Roleservice so it writes to RoleMenus and RolePermissions separately. Menuservice adjust to read from Menus and Permissions instead of the old menu tables.

Browse files
TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs CHANGED
@@ -21,10 +21,14 @@ public partial class AppDbContext : DbContext
21
 
22
  public virtual DbSet<FileAttachment> FileAttachments { get; set; }
23
 
 
 
24
  public virtual DbSet<MenuAdmin> MenuAdmins { get; set; }
25
 
26
  public virtual DbSet<MenuAdminDetail> MenuAdminDetails { get; set; }
27
 
 
 
28
  public virtual DbSet<Project> Projects { get; set; }
29
 
30
  public virtual DbSet<ProjectMember> ProjectMembers { get; set; }
@@ -33,6 +37,10 @@ public partial class AppDbContext : DbContext
33
 
34
  public virtual DbSet<RoleMenu> RoleMenus { get; set; }
35
 
 
 
 
 
36
  public virtual DbSet<Task> Tasks { get; set; }
37
 
38
  public virtual DbSet<TaskHistory> TaskHistories { get; set; }
@@ -42,8 +50,8 @@ public partial class AppDbContext : DbContext
42
  public virtual DbSet<User> Users { get; set; }
43
 
44
  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
45
- {
46
- }
47
 
48
  protected override void OnModelCreating(ModelBuilder modelBuilder)
49
  {
@@ -103,6 +111,27 @@ public partial class AppDbContext : DbContext
103
  .HasConstraintName("FK_FileAttachments_Tasks");
104
  });
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  modelBuilder.Entity<MenuAdmin>(entity =>
107
  {
108
  entity.HasKey(e => e.AdminMenuId);
@@ -128,6 +157,28 @@ public partial class AppDbContext : DbContext
128
  entity.Property(e => e.ParentMenuCode).HasMaxLength(50);
129
  });
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  modelBuilder.Entity<Project>(entity =>
132
  {
133
  entity.HasIndex(e => e.CreatedById, "IX_Projects_CreatedById");
@@ -181,12 +232,63 @@ public partial class AppDbContext : DbContext
181
 
182
  modelBuilder.Entity<RoleMenu>(entity =>
183
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  entity.Property(e => e.RoleMenuId).HasMaxLength(50);
185
  entity.Property(e => e.CreatedUserId).HasMaxLength(50);
186
  entity.Property(e => e.MenuCode).HasMaxLength(50);
187
- entity.Property(e => e.RoleCode).HasMaxLength(50);
188
  entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
189
- entity.HasIndex(e => e.RoleId, "IX_RoleMenus_RoleId");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  });
191
 
192
  modelBuilder.Entity<Task>(entity =>
 
21
 
22
  public virtual DbSet<FileAttachment> FileAttachments { get; set; }
23
 
24
+ public virtual DbSet<Menu> Menus { get; set; }
25
+
26
  public virtual DbSet<MenuAdmin> MenuAdmins { get; set; }
27
 
28
  public virtual DbSet<MenuAdminDetail> MenuAdminDetails { get; set; }
29
 
30
+ public virtual DbSet<Permission> Permissions { get; set; }
31
+
32
  public virtual DbSet<Project> Projects { get; set; }
33
 
34
  public virtual DbSet<ProjectMember> ProjectMembers { get; set; }
 
37
 
38
  public virtual DbSet<RoleMenu> RoleMenus { get; set; }
39
 
40
+ public virtual DbSet<RoleMenusLegacy> RoleMenusLegacies { get; set; }
41
+
42
+ public virtual DbSet<RolePermission> RolePermissions { get; set; }
43
+
44
  public virtual DbSet<Task> Tasks { get; set; }
45
 
46
  public virtual DbSet<TaskHistory> TaskHistories { get; set; }
 
50
  public virtual DbSet<User> Users { get; set; }
51
 
52
  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
53
+ #warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see https://go.microsoft.com/fwlink/?LinkId=723263.
54
+ => optionsBuilder.UseSqlServer("Server=.;Database=TTS;User Id=sa;Password=sasa@123;TrustServerCertificate=True;");
55
 
56
  protected override void OnModelCreating(ModelBuilder modelBuilder)
57
  {
 
111
  .HasConstraintName("FK_FileAttachments_Tasks");
112
  });
113
 
114
+ modelBuilder.Entity<Menu>(entity =>
115
+ {
116
+ entity.HasIndex(e => e.ParentMenuId, "IX_Menus_ParentMenuId");
117
+
118
+ entity.HasIndex(e => e.MenuCode, "UQ_Menus_MenuCode").IsUnique();
119
+
120
+ entity.Property(e => e.CreatedAt)
121
+ .HasPrecision(0)
122
+ .HasDefaultValueSql("(sysutcdatetime())");
123
+ entity.Property(e => e.Icon).HasMaxLength(50);
124
+ entity.Property(e => e.MenuCode).HasMaxLength(50);
125
+ entity.Property(e => e.MenuName).HasMaxLength(100);
126
+ entity.Property(e => e.MenuUrl).HasMaxLength(200);
127
+ entity.Property(e => e.UpdatedAt).HasPrecision(0);
128
+ entity.Property(e => e.Visible).HasDefaultValue(true);
129
+
130
+ entity.HasOne(d => d.ParentMenu).WithMany(p => p.InverseParentMenu)
131
+ .HasForeignKey(d => d.ParentMenuId)
132
+ .HasConstraintName("FK_Menus_ParentMenu");
133
+ });
134
+
135
  modelBuilder.Entity<MenuAdmin>(entity =>
136
  {
137
  entity.HasKey(e => e.AdminMenuId);
 
157
  entity.Property(e => e.ParentMenuCode).HasMaxLength(50);
158
  });
159
 
160
+ modelBuilder.Entity<Permission>(entity =>
161
+ {
162
+ entity.HasIndex(e => e.MenuId, "IX_Permissions_MenuId");
163
+
164
+ entity.HasIndex(e => e.PermissionCode, "UQ_Permissions_PermissionCode").IsUnique();
165
+
166
+ entity.Property(e => e.ActionName).HasMaxLength(100);
167
+ entity.Property(e => e.ApiName).HasMaxLength(100);
168
+ entity.Property(e => e.CreatedAt)
169
+ .HasPrecision(0)
170
+ .HasDefaultValueSql("(sysutcdatetime())");
171
+ entity.Property(e => e.HttpMethod).HasMaxLength(20);
172
+ entity.Property(e => e.PermissionCode).HasMaxLength(50);
173
+ entity.Property(e => e.UpdatedAt).HasPrecision(0);
174
+ entity.Property(e => e.Visible).HasDefaultValue(true);
175
+
176
+ entity.HasOne(d => d.Menu).WithMany(p => p.Permissions)
177
+ .HasForeignKey(d => d.MenuId)
178
+ .OnDelete(DeleteBehavior.ClientSetNull)
179
+ .HasConstraintName("FK_Permissions_Menu");
180
+ });
181
+
182
  modelBuilder.Entity<Project>(entity =>
183
  {
184
  entity.HasIndex(e => e.CreatedById, "IX_Projects_CreatedById");
 
232
 
233
  modelBuilder.Entity<RoleMenu>(entity =>
234
  {
235
+ entity.HasKey(e => e.RoleMenuId).HasName("PK_RoleMenus_New");
236
+
237
+ entity.HasIndex(e => new { e.RoleId, e.MenuId }, "UQ_RoleMenus_Role_Menu").IsUnique();
238
+
239
+ entity.Property(e => e.CreatedAt)
240
+ .HasPrecision(0)
241
+ .HasDefaultValueSql("(sysutcdatetime())");
242
+ entity.Property(e => e.UpdatedAt).HasPrecision(0);
243
+
244
+ entity.HasOne(d => d.Menu).WithMany(p => p.RoleMenus)
245
+ .HasForeignKey(d => d.MenuId)
246
+ .OnDelete(DeleteBehavior.ClientSetNull)
247
+ .HasConstraintName("FK_RoleMenus_Menus");
248
+
249
+ entity.HasOne(d => d.Role).WithMany(p => p.RoleMenus)
250
+ .HasForeignKey(d => d.RoleId)
251
+ .OnDelete(DeleteBehavior.ClientSetNull)
252
+ .HasConstraintName("FK_RoleMenus_Roles");
253
+ });
254
+
255
+ modelBuilder.Entity<RoleMenusLegacy>(entity =>
256
+ {
257
+ entity.HasKey(e => e.RoleMenuId).HasName("PK_RoleMenus");
258
+
259
+ entity.ToTable("RoleMenus_Legacy");
260
+
261
+ entity.HasIndex(e => e.RoleId, "IX_RoleMenus_RoleId");
262
+
263
  entity.Property(e => e.RoleMenuId).HasMaxLength(50);
264
  entity.Property(e => e.CreatedUserId).HasMaxLength(50);
265
  entity.Property(e => e.MenuCode).HasMaxLength(50);
 
266
  entity.Property(e => e.ModifiedUserId).HasMaxLength(50);
267
+ entity.Property(e => e.RoleCode).HasMaxLength(50);
268
+ });
269
+
270
+ modelBuilder.Entity<RolePermission>(entity =>
271
+ {
272
+ entity.HasIndex(e => e.PermissionId, "IX_RolePermissions_PermissionId");
273
+
274
+ entity.HasIndex(e => e.RoleId, "IX_RolePermissions_RoleId");
275
+
276
+ entity.HasIndex(e => new { e.RoleId, e.PermissionId }, "UQ_RolePermissions_Role_Permission").IsUnique();
277
+
278
+ entity.Property(e => e.CreatedAt)
279
+ .HasPrecision(0)
280
+ .HasDefaultValueSql("(sysutcdatetime())");
281
+ entity.Property(e => e.UpdatedAt).HasPrecision(0);
282
+
283
+ entity.HasOne(d => d.Permission).WithMany(p => p.RolePermissions)
284
+ .HasForeignKey(d => d.PermissionId)
285
+ .OnDelete(DeleteBehavior.ClientSetNull)
286
+ .HasConstraintName("FK_RolePermissions_Permissions");
287
+
288
+ entity.HasOne(d => d.Role).WithMany(p => p.RolePermissions)
289
+ .HasForeignKey(d => d.RoleId)
290
+ .OnDelete(DeleteBehavior.ClientSetNull)
291
+ .HasConstraintName("FK_RolePermissions_Roles");
292
  });
293
 
294
  modelBuilder.Entity<Task>(entity =>
TaskTrackingSystem.Database/AppDbContextModels/{AdminMenu.cs → Menu.cs} RENAMED
@@ -3,23 +3,39 @@ using System.Collections.Generic;
3
 
4
  namespace TaskTrackingSystem.Database.AppDbContextModels;
5
 
6
- public partial class AdminMenu
7
  {
8
- public string AdminMenuId { get; set; } = null!;
9
 
10
  public string MenuCode { get; set; } = null!;
11
 
12
- public string ParentCode { get; set; } = null!;
13
 
14
  public string MenuName { get; set; } = null!;
15
 
16
  public string? MenuUrl { get; set; }
17
 
 
 
18
  public bool Visible { get; set; }
19
 
20
  public int OrderNo { get; set; }
21
 
22
- public string? Icon { get; set; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- public int DelFlag { get; set; }
25
  }
 
3
 
4
  namespace TaskTrackingSystem.Database.AppDbContextModels;
5
 
6
+ public partial class Menu
7
  {
8
+ public long MenuId { get; set; }
9
 
10
  public string MenuCode { get; set; } = null!;
11
 
12
+ public long? ParentMenuId { get; set; }
13
 
14
  public string MenuName { get; set; } = null!;
15
 
16
  public string? MenuUrl { get; set; }
17
 
18
+ public string? Icon { get; set; }
19
+
20
  public bool Visible { get; set; }
21
 
22
  public int OrderNo { get; set; }
23
 
24
+ public bool IsDeleted { get; set; }
25
+
26
+ public long? CreatedById { get; set; }
27
+
28
+ public DateTime CreatedAt { get; set; }
29
+
30
+ public long? UpdatedById { get; set; }
31
+
32
+ public DateTime? UpdatedAt { get; set; }
33
+
34
+ public virtual ICollection<Menu> InverseParentMenu { get; set; } = new List<Menu>();
35
+
36
+ public virtual Menu? ParentMenu { get; set; }
37
+
38
+ public virtual ICollection<Permission> Permissions { get; set; } = new List<Permission>();
39
 
40
+ public virtual ICollection<RoleMenu> RoleMenus { get; set; } = new List<RoleMenu>();
41
  }
TaskTrackingSystem.Database/AppDbContextModels/Permission.cs ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System;
2
+ using System.Collections.Generic;
3
+
4
+ namespace TaskTrackingSystem.Database.AppDbContextModels;
5
+
6
+ public partial class Permission
7
+ {
8
+ public long PermissionId { get; set; }
9
+
10
+ public string PermissionCode { get; set; } = null!;
11
+
12
+ public long MenuId { get; set; }
13
+
14
+ public string ActionName { get; set; } = null!;
15
+
16
+ public string ApiName { get; set; } = null!;
17
+
18
+ public string? HttpMethod { get; set; }
19
+
20
+ public bool Visible { get; set; }
21
+
22
+ public int OrderNo { get; set; }
23
+
24
+ public bool IsDeleted { get; set; }
25
+
26
+ public long? CreatedById { get; set; }
27
+
28
+ public DateTime CreatedAt { get; set; }
29
+
30
+ public long? UpdatedById { get; set; }
31
+
32
+ public DateTime? UpdatedAt { get; set; }
33
+
34
+ public virtual Menu Menu { get; set; } = null!;
35
+
36
+ public virtual ICollection<RolePermission> RolePermissions { get; set; } = new List<RolePermission>();
37
+ }
TaskTrackingSystem.Database/AppDbContextModels/Role.cs CHANGED
@@ -21,5 +21,9 @@ public partial class Role
21
 
22
  public bool IsDeleted { get; set; }
23
 
 
 
 
 
24
  public virtual ICollection<User> Users { get; set; } = new List<User>();
25
  }
 
21
 
22
  public bool IsDeleted { get; set; }
23
 
24
+ public virtual ICollection<RoleMenu> RoleMenus { get; set; } = new List<RoleMenu>();
25
+
26
+ public virtual ICollection<RolePermission> RolePermissions { get; set; } = new List<RolePermission>();
27
+
28
  public virtual ICollection<User> Users { get; set; } = new List<User>();
29
  }
TaskTrackingSystem.Database/AppDbContextModels/RoleMenu.cs CHANGED
@@ -5,21 +5,23 @@ namespace TaskTrackingSystem.Database.AppDbContextModels;
5
 
6
  public partial class RoleMenu
7
  {
8
- public string RoleMenuId { get; set; } = null!;
9
 
10
  public long RoleId { get; set; }
11
 
12
- public string? RoleCode { get; set; }
13
 
14
- public string MenuCode { get; set; } = null!;
15
 
16
- public int DelFlag { get; set; }
17
 
18
- public string? CreatedUserId { get; set; }
19
 
20
- public DateTime CreatedDateTime { get; set; }
21
 
22
- public string? ModifiedUserId { get; set; }
23
 
24
- public DateTime? ModifiedDateTime { get; set; }
 
 
25
  }
 
5
 
6
  public partial class RoleMenu
7
  {
8
+ public long RoleMenuId { get; set; }
9
 
10
  public long RoleId { get; set; }
11
 
12
+ public long MenuId { get; set; }
13
 
14
+ public bool IsDeleted { get; set; }
15
 
16
+ public long? CreatedById { get; set; }
17
 
18
+ public DateTime CreatedAt { get; set; }
19
 
20
+ public long? UpdatedById { get; set; }
21
 
22
+ public DateTime? UpdatedAt { get; set; }
23
 
24
+ public virtual Menu Menu { get; set; } = null!;
25
+
26
+ public virtual Role Role { get; set; } = null!;
27
  }
TaskTrackingSystem.Database/AppDbContextModels/RoleMenusLegacy.cs ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System;
2
+ using System.Collections.Generic;
3
+
4
+ namespace TaskTrackingSystem.Database.AppDbContextModels;
5
+
6
+ public partial class RoleMenusLegacy
7
+ {
8
+ public string RoleMenuId { get; set; } = null!;
9
+
10
+ public string RoleCode { get; set; } = null!;
11
+
12
+ public string MenuCode { get; set; } = null!;
13
+
14
+ public int DelFlag { get; set; }
15
+
16
+ public string? CreatedUserId { get; set; }
17
+
18
+ public DateTime CreatedDateTime { get; set; }
19
+
20
+ public string? ModifiedUserId { get; set; }
21
+
22
+ public DateTime? ModifiedDateTime { get; set; }
23
+
24
+ public long RoleId { get; set; }
25
+ }
TaskTrackingSystem.Database/AppDbContextModels/RolePermission.cs ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ using System;
2
+ using System.Collections.Generic;
3
+
4
+ namespace TaskTrackingSystem.Database.AppDbContextModels;
5
+
6
+ public partial class RolePermission
7
+ {
8
+ public long RolePermissionId { get; set; }
9
+
10
+ public long RoleId { get; set; }
11
+
12
+ public long PermissionId { get; set; }
13
+
14
+ public bool IsDeleted { get; set; }
15
+
16
+ public long? CreatedById { get; set; }
17
+
18
+ public DateTime CreatedAt { get; set; }
19
+
20
+ public long? UpdatedById { get; set; }
21
+
22
+ public DateTime? UpdatedAt { get; set; }
23
+
24
+ public virtual Permission Permission { get; set; } = null!;
25
+
26
+ public virtual Role Role { get; set; } = null!;
27
+ }
TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs CHANGED
@@ -47,5 +47,18 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
47
  var menus = await _menuService.GetAllMenusAsync();
48
  return Ok(menus);
49
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
51
  }
 
47
  var menus = await _menuService.GetAllMenusAsync();
48
  return Ok(menus);
49
  }
50
+
51
+ /// <summary>
52
+ /// Returns a lightweight version string for the current user's role permissions.
53
+ /// Clients poll this to detect when an admin has changed permissions without fetching the full menu list.
54
+ /// </summary>
55
+ [HttpGet("version")]
56
+ public async Task<ActionResult<string>> GetMenuVersion()
57
+ {
58
+ var roleId = User.GetRoleId();
59
+ var roleName = User.GetRoleName() ?? string.Empty;
60
+ var version = await _menuService.GetMenuVersionAsync(roleId, roleName);
61
+ return Ok(version);
62
+ }
63
  }
64
  }
TaskTrackingSystem.WebApi/Features/Menu/MenuService.cs CHANGED
@@ -25,7 +25,7 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
25
  return new List<MenuDto>();
26
  }
27
 
28
- return await GetMenusForRoleAsync(role.Id, role.Name);
29
  }
30
 
31
  public async Task<List<MenuDto>> GetMenusByRoleNameAsync(string roleName)
@@ -41,131 +41,120 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
41
  return new List<MenuDto>();
42
  }
43
 
44
- return await GetMenusForRoleAsync(role.Id, role.Name);
45
  }
46
 
47
- private async Task<List<MenuDto>> GetMenusForRoleAsync(long roleId, string roleName)
48
  {
49
- var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == roleId && r.IsDeleted != true);
50
- if (role == null)
51
- {
52
- return new List<MenuDto>();
53
- }
54
-
55
- var allVisibleMenus = await _db.MenuAdmins
56
- .Where(m => m.Visible && m.DelFlag == 0)
57
  .OrderBy(m => m.OrderNo)
58
- .Select(m => new MenuDto
59
- {
60
- MenuCode = m.MenuCode,
61
- ParentCode = m.ParentCode,
62
- MenuName = m.MenuName,
63
- MenuUrl = m.MenuUrl,
64
- OrderNo = m.OrderNo,
65
- Icon = m.Icon
66
- })
67
  .ToListAsync();
68
 
69
- // Keep compatibility with roles stored by either RoleId or the older RoleCode link.
70
- var allowedMenuCodes = await GetAllowedMenuCodesAsync(role.Id, role.Name);
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
- if (allowedMenuCodes.Count == 0)
73
  {
74
  return new List<MenuDto>();
75
  }
76
 
77
- var visibleMenuLookup = allVisibleMenus
78
- .GroupBy(m => m.MenuCode, StringComparer.OrdinalIgnoreCase)
79
- .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
80
 
81
- var expandedMenuCodes = ExpandWithAncestors(allowedMenuCodes, visibleMenuLookup);
82
- var menus = allVisibleMenus
83
- .Where(m => expandedMenuCodes.Contains(m.MenuCode))
84
  .OrderBy(m => m.OrderNo)
85
  .ToList();
86
 
87
- return BuildHierarchy(menus);
88
  }
89
 
90
- private static List<MenuDto> BuildHierarchy(List<MenuDto> menus)
91
  {
92
  if (menus.Count == 0)
93
  {
94
  return new List<MenuDto>();
95
  }
96
 
97
- var menuLookup = menus
98
- .GroupBy(m => m.MenuCode, StringComparer.OrdinalIgnoreCase)
99
- .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
 
 
 
 
 
 
 
 
 
100
 
101
- foreach (var menu in menuLookup.Values)
102
  {
103
- menu.SubMenus = new List<MenuDto>();
104
  }
105
 
106
  var roots = new List<MenuDto>();
107
 
108
- foreach (var menu in menuLookup.Values.OrderBy(m => m.OrderNo).ThenBy(m => m.MenuName))
109
  {
110
- var parentCode = NormalizeMenuCode(menu.ParentCode);
111
- if (string.IsNullOrWhiteSpace(parentCode) ||
112
- parentCode == "0" ||
113
- !menuLookup.TryGetValue(parentCode, out var parent))
114
  {
115
- roots.Add(menu);
116
  continue;
117
  }
118
 
119
- parent.SubMenus.Add(menu);
120
  }
121
 
122
  SortMenuTree(roots);
123
  return roots;
124
  }
125
 
126
- private async Task<HashSet<string>> GetAllowedMenuCodesAsync(long roleId, string roleName)
127
  {
128
- var allowedMenuCodes = await _db.RoleMenus
129
- .Where(rm => rm.RoleId == roleId && rm.DelFlag == 0)
130
- .Select(rm => rm.MenuCode)
131
- .ToListAsync();
132
-
133
- if (allowedMenuCodes.Count > 0)
134
  {
135
- return allowedMenuCodes.ToHashSet(StringComparer.OrdinalIgnoreCase);
136
  }
137
 
138
- if (!string.IsNullOrWhiteSpace(roleName))
139
- {
140
- allowedMenuCodes = await _db.RoleMenus
141
- .Where(rm => rm.RoleCode == roleName && rm.DelFlag == 0)
142
- .Select(rm => rm.MenuCode)
143
- .ToListAsync();
144
- }
145
-
146
- return allowedMenuCodes.ToHashSet(StringComparer.OrdinalIgnoreCase);
147
  }
148
 
149
- private static HashSet<string> ExpandWithAncestors(IEnumerable<string> menuCodes, IReadOnlyDictionary<string, MenuDto> menuLookup)
150
  {
151
- var expanded = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
152
 
153
- foreach (var menuCode in menuCodes.Where(code => !string.IsNullOrWhiteSpace(code)))
154
  {
155
- var currentCode = menuCode.Trim();
156
 
157
- while (!string.IsNullOrWhiteSpace(currentCode) && expanded.Add(currentCode))
158
  {
159
- if (!menuLookup.TryGetValue(currentCode, out var currentMenu))
160
  {
161
  break;
162
  }
163
 
164
- currentCode = NormalizeMenuCode(currentMenu.ParentCode);
165
- if (string.IsNullOrWhiteSpace(currentCode) || currentCode == "0")
166
- {
167
- break;
168
- }
169
  }
170
  }
171
 
@@ -174,7 +163,11 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
174
 
175
  private static void SortMenuTree(IList<MenuDto> menus)
176
  {
177
- var ordered = menus.OrderBy(m => m.OrderNo).ThenBy(m => m.MenuName, StringComparer.OrdinalIgnoreCase).ToList();
 
 
 
 
178
  menus.Clear();
179
 
180
  foreach (var menu in ordered)
@@ -188,50 +181,78 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
188
  }
189
  }
190
 
191
- private static string NormalizeMenuCode(string? code)
192
  {
193
- return string.IsNullOrWhiteSpace(code) ? string.Empty : code.Trim();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  }
 
195
  public async Task<List<MenuAdminDto>> GetAllMenusAsync()
196
  {
197
- var menus = await _db.MenuAdmins
198
- .Where(m => m.DelFlag == 0)
199
  .OrderBy(m => m.OrderNo)
200
- .Select(m => new MenuAdminDto
201
- {
202
- MenuCode = m.MenuCode,
203
- ParentCode = m.ParentCode,
204
- MenuName = m.MenuName,
205
- MenuUrl = m.MenuUrl,
206
- OrderNo = m.OrderNo,
207
- Icon = m.Icon,
208
- Visible = m.Visible
209
- })
210
  .ToListAsync();
211
 
212
- var details = await _db.MenuAdminDetails
213
- .Where(d => d.DelFlag == 0)
214
- .OrderBy(d => d.OrderNo)
215
- .Select(d => new MenuAdminDetailDto
 
 
 
 
216
  {
217
- MenuAdminDetailId = d.MenuAdminDetailId,
218
- MenuDetailCode = d.MenuDetailCode,
219
- ParentMenuCode = d.ParentMenuCode,
220
- ActionName = d.ActionName,
221
- ApiName = d.ApiName,
222
- Visible = d.Visible,
223
- OrderNo = d.OrderNo
 
 
 
 
224
  })
225
- .ToListAsync();
 
 
 
226
 
227
- foreach (var menu in menus)
228
  {
229
- menu.Actions = details
230
- .Where(d => d.ParentMenuCode.Equals(menu.MenuCode, StringComparison.OrdinalIgnoreCase))
231
- .ToList();
 
 
 
 
 
 
 
 
 
 
232
  }
233
 
234
- return menus;
235
  }
236
  }
237
  }
 
25
  return new List<MenuDto>();
26
  }
27
 
28
+ return await GetMenusForRoleAsync(role.Id);
29
  }
30
 
31
  public async Task<List<MenuDto>> GetMenusByRoleNameAsync(string roleName)
 
41
  return new List<MenuDto>();
42
  }
43
 
44
+ return await GetMenusForRoleAsync(role.Id);
45
  }
46
 
47
+ private async Task<List<MenuDto>> GetMenusForRoleAsync(long roleId)
48
  {
49
+ var visibleMenus = await _db.Menus
50
+ .Where(m => !m.IsDeleted && m.Visible)
 
 
 
 
 
 
51
  .OrderBy(m => m.OrderNo)
 
 
 
 
 
 
 
 
 
52
  .ToListAsync();
53
 
54
+ var assignedMenuIds = await _db.RoleMenus
55
+ .Where(rm => rm.RoleId == roleId && !rm.IsDeleted)
56
+ .Select(rm => rm.MenuId)
57
+ .ToListAsync();
58
+
59
+ var assignedPermissionMenuIds = await _db.RolePermissions
60
+ .Where(rp => rp.RoleId == roleId && !rp.IsDeleted)
61
+ .Select(rp => rp.Permission.MenuId)
62
+ .ToListAsync();
63
+
64
+ var allowedMenuIds = assignedMenuIds
65
+ .Concat(assignedPermissionMenuIds)
66
+ .Distinct()
67
+ .ToHashSet();
68
 
69
+ if (allowedMenuIds.Count == 0)
70
  {
71
  return new List<MenuDto>();
72
  }
73
 
74
+ var visibleMenuLookup = visibleMenus.ToDictionary(m => m.MenuId);
75
+ var expandedMenuIds = ExpandWithAncestors(allowedMenuIds, visibleMenuLookup);
 
76
 
77
+ var filteredMenus = visibleMenus
78
+ .Where(m => expandedMenuIds.Contains(m.MenuId))
 
79
  .OrderBy(m => m.OrderNo)
80
  .ToList();
81
 
82
+ return BuildHierarchy(filteredMenus);
83
  }
84
 
85
+ private static List<MenuDto> BuildHierarchy(List<Menu> menus)
86
  {
87
  if (menus.Count == 0)
88
  {
89
  return new List<MenuDto>();
90
  }
91
 
92
+ var menuLookup = menus.ToDictionary(m => m.MenuId);
93
+ var dtoLookup = menus.ToDictionary(
94
+ m => m.MenuId,
95
+ m => new MenuDto
96
+ {
97
+ MenuCode = m.MenuCode,
98
+ ParentCode = GetParentCode(m, menuLookup),
99
+ MenuName = m.MenuName,
100
+ MenuUrl = m.MenuUrl,
101
+ OrderNo = m.OrderNo,
102
+ Icon = m.Icon
103
+ });
104
 
105
+ foreach (var dto in dtoLookup.Values)
106
  {
107
+ dto.SubMenus = new List<MenuDto>();
108
  }
109
 
110
  var roots = new List<MenuDto>();
111
 
112
+ foreach (var menu in menus.OrderBy(m => m.OrderNo).ThenBy(m => m.MenuName, StringComparer.OrdinalIgnoreCase))
113
  {
114
+ var dto = dtoLookup[menu.MenuId];
115
+ var parentId = menu.ParentMenuId;
116
+
117
+ if (!parentId.HasValue || !dtoLookup.TryGetValue(parentId.Value, out var parentDto))
118
  {
119
+ roots.Add(dto);
120
  continue;
121
  }
122
 
123
+ parentDto.SubMenus.Add(dto);
124
  }
125
 
126
  SortMenuTree(roots);
127
  return roots;
128
  }
129
 
130
+ private static string GetParentCode(Menu menu, IReadOnlyDictionary<long, Menu> menuLookup)
131
  {
132
+ if (!menu.ParentMenuId.HasValue)
 
 
 
 
 
133
  {
134
+ return string.Empty;
135
  }
136
 
137
+ return menuLookup.TryGetValue(menu.ParentMenuId.Value, out var parent)
138
+ ? parent.MenuCode
139
+ : string.Empty;
 
 
 
 
 
 
140
  }
141
 
142
+ private static HashSet<long> ExpandWithAncestors(IEnumerable<long> menuIds, IReadOnlyDictionary<long, Menu> menuLookup)
143
  {
144
+ var expanded = new HashSet<long>();
145
 
146
+ foreach (var menuId in menuIds)
147
  {
148
+ var currentId = menuId;
149
 
150
+ while (currentId > 0 && expanded.Add(currentId))
151
  {
152
+ if (!menuLookup.TryGetValue(currentId, out var currentMenu) || !currentMenu.ParentMenuId.HasValue)
153
  {
154
  break;
155
  }
156
 
157
+ currentId = currentMenu.ParentMenuId.Value;
 
 
 
 
158
  }
159
  }
160
 
 
163
 
164
  private static void SortMenuTree(IList<MenuDto> menus)
165
  {
166
+ var ordered = menus
167
+ .OrderBy(m => m.OrderNo)
168
+ .ThenBy(m => m.MenuName, StringComparer.OrdinalIgnoreCase)
169
+ .ToList();
170
+
171
  menus.Clear();
172
 
173
  foreach (var menu in ordered)
 
181
  }
182
  }
183
 
184
+ public async Task<string> GetMenuVersionAsync(long roleId, string roleName)
185
  {
186
+ var latestMenuChange = await _db.RoleMenus
187
+ .Where(rm => rm.RoleId == roleId && !rm.IsDeleted)
188
+ .Select(rm => (DateTime?)(rm.UpdatedAt ?? rm.CreatedAt))
189
+ .MaxAsync();
190
+
191
+ var latestPermissionChange = await _db.RolePermissions
192
+ .Where(rp => rp.RoleId == roleId && !rp.IsDeleted)
193
+ .Select(rp => (DateTime?)(rp.UpdatedAt ?? rp.CreatedAt))
194
+ .MaxAsync();
195
+
196
+ var latest = latestMenuChange ?? latestPermissionChange;
197
+ if (latestPermissionChange.HasValue && (!latest.HasValue || latestPermissionChange.Value > latest.Value))
198
+ {
199
+ latest = latestPermissionChange;
200
+ }
201
+
202
+ return latest.HasValue ? latest.Value.Ticks.ToString() : "0";
203
  }
204
+
205
  public async Task<List<MenuAdminDto>> GetAllMenusAsync()
206
  {
207
+ var menus = await _db.Menus
208
+ .Where(m => !m.IsDeleted)
209
  .OrderBy(m => m.OrderNo)
 
 
 
 
 
 
 
 
 
 
210
  .ToListAsync();
211
 
212
+ var permissions = await _db.Permissions
213
+ .Where(p => !p.IsDeleted)
214
+ .OrderBy(p => p.OrderNo)
215
+ .ToListAsync();
216
+
217
+ var menuLookup = menus.ToDictionary(m => m.MenuId);
218
+ var menuDtoLookup = menus
219
+ .Select(m => new
220
  {
221
+ m.MenuId,
222
+ Dto = new MenuAdminDto
223
+ {
224
+ MenuCode = m.MenuCode,
225
+ ParentCode = GetParentCode(m, menuLookup),
226
+ MenuName = m.MenuName,
227
+ MenuUrl = m.MenuUrl,
228
+ OrderNo = m.OrderNo,
229
+ Icon = m.Icon,
230
+ Visible = m.Visible
231
+ }
232
  })
233
+ .ToList();
234
+
235
+ var menuDtos = menuDtoLookup.Select(x => x.Dto).ToList();
236
+ var dtoLookup = menuDtoLookup.ToDictionary(x => x.MenuId, x => x.Dto);
237
 
238
+ foreach (var permission in permissions)
239
  {
240
+ if (dtoLookup.TryGetValue(permission.MenuId, out var dto))
241
+ {
242
+ dto.Actions.Add(new MenuAdminDetailDto
243
+ {
244
+ MenuAdminDetailId = permission.PermissionId.ToString(),
245
+ MenuDetailCode = permission.PermissionCode,
246
+ ParentMenuCode = menuLookup.TryGetValue(permission.MenuId, out var parentMenu) ? parentMenu.MenuCode : string.Empty,
247
+ ActionName = permission.ActionName,
248
+ ApiName = permission.ApiName,
249
+ Visible = permission.Visible,
250
+ OrderNo = permission.OrderNo
251
+ });
252
+ }
253
  }
254
 
255
+ return menuDtos;
256
  }
257
  }
258
  }
TaskTrackingSystem.WebApi/Features/Role/RoleService.cs CHANGED
@@ -28,15 +28,17 @@ namespace TaskTrackingSystem.WebApi.Features.Role
28
  Name = r.Name,
29
  Description = r.Description,
30
  CreatedAt = r.CreatedAt ?? DateTime.UtcNow
31
- }).ToListAsync();
 
32
  }
33
 
34
  public async Task<RoleDto?> GetRoleByIdAsync(long id)
35
  {
36
- var role = await _db.Roles
37
- .FirstOrDefaultAsync(r => r.Id == id && r.IsDeleted != true);
38
-
39
- if (role == null) return null;
 
40
 
41
  return new RoleDto
42
  {
@@ -69,7 +71,7 @@ namespace TaskTrackingSystem.WebApi.Features.Role
69
  }
70
  }
71
 
72
- var role = new TaskTrackingSystem.Database.AppDbContextModels.Role
73
  {
74
  Name = dto.Name,
75
  Description = dto.Description,
@@ -86,15 +88,13 @@ namespace TaskTrackingSystem.WebApi.Features.Role
86
  return Result<RoleDto>.Failure(assignResult.ErrorMessage ?? ResultMessages.FailedToCreateRole, assignResult.StatusCode);
87
  }
88
 
89
- var resultDto = new RoleDto
90
  {
91
  Id = role.Id,
92
  Name = role.Name,
93
  Description = role.Description,
94
  CreatedAt = role.CreatedAt ?? DateTime.UtcNow
95
- };
96
-
97
- return Result<RoleDto>.Success(resultDto, 201);
98
  }
99
 
100
  public async Task<Result> UpdateRoleAsync(long id, UpdateRoleDto dto, long? currentUserId = null)
@@ -105,7 +105,10 @@ namespace TaskTrackingSystem.WebApi.Features.Role
105
  }
106
 
107
  var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == id && r.IsDeleted != true);
108
- if (role == null) return Result.Failure(ResultMessages.RoleNotFound(id), 404);
 
 
 
109
 
110
  var nameExists = await _db.Roles.AnyAsync(r => r.Name == dto.Name && r.Id != id && r.IsDeleted != true);
111
  if (nameExists)
@@ -113,21 +116,11 @@ namespace TaskTrackingSystem.WebApi.Features.Role
113
  return Result.Failure("Role name is already taken by another role.", 400);
114
  }
115
 
116
- var oldName = role.Name;
117
  role.Name = dto.Name;
118
  role.Description = dto.Description;
119
  role.UpdatedAt = DateTime.UtcNow;
120
  role.UpdatedBy = currentUserId;
121
 
122
- var relatedRoleMenus = await _db.RoleMenus
123
- .Where(rm => rm.RoleId == role.Id || rm.RoleCode == oldName)
124
- .ToListAsync();
125
-
126
- foreach (var roleMenu in relatedRoleMenus)
127
- {
128
- roleMenu.RoleCode = dto.Name;
129
- }
130
-
131
  _db.Roles.Update(role);
132
  await _db.SaveChangesAsync();
133
  return Result.Success(200);
@@ -136,7 +129,10 @@ namespace TaskTrackingSystem.WebApi.Features.Role
136
  public async Task<Result> SoftDeleteRoleAsync(long id)
137
  {
138
  var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == id && r.IsDeleted != true);
139
- if (role == null) return Result.Failure(ResultMessages.RoleNotFound(id), 404);
 
 
 
140
 
141
  role.IsDeleted = true;
142
  _db.Roles.Update(role);
@@ -144,7 +140,6 @@ namespace TaskTrackingSystem.WebApi.Features.Role
144
  return Result.Success(200);
145
  }
146
 
147
-
148
  public async Task<Result<List<string>>> GetAssignedMenusByRoleIdAsync(long roleId)
149
  {
150
  var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == roleId && r.IsDeleted != true);
@@ -154,19 +149,21 @@ namespace TaskTrackingSystem.WebApi.Features.Role
154
  }
155
 
156
  var menuCodes = await _db.RoleMenus
157
- .Where(rm => rm.RoleId == role.Id && rm.DelFlag == 0)
158
- .Select(rm => rm.MenuCode)
159
  .ToListAsync();
160
 
161
- if (!menuCodes.Any())
162
- {
163
- menuCodes = await _db.RoleMenus
164
- .Where(rm => rm.RoleCode == role.Name && rm.DelFlag == 0)
165
- .Select(rm => rm.MenuCode)
166
- .ToListAsync();
167
- }
 
 
168
 
169
- return Result<List<string>>.Success(menuCodes);
170
  }
171
 
172
  public async Task<Result> AssignMenusToRoleAsync(long roleId, AssignMenusDto dto)
@@ -182,34 +179,83 @@ namespace TaskTrackingSystem.WebApi.Features.Role
182
  return Result.Failure("MenuCodes cannot be null", 400);
183
  }
184
 
185
- var validCodesResult = await ValidatePermissionCodesAsync(dto.MenuCodes);
 
 
 
 
 
 
186
  if (!validCodesResult.IsSuccess)
187
  {
188
  return Result.Failure(validCodesResult.ErrorMessage ?? ResultMessages.FailedToUpdateRole, validCodesResult.StatusCode);
189
  }
190
 
191
- // Remove existing role menus matching either the new RoleId link or the old RoleCode link.
192
- var existingRoleMenus = await _db.RoleMenus
193
- .Where(rm => rm.RoleId == role.Id || rm.RoleCode == role.Name)
 
 
 
 
 
 
 
 
 
 
194
  .ToListAsync();
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  if (existingRoleMenus.Any())
197
  {
198
  _db.RoleMenus.RemoveRange(existingRoleMenus);
199
  }
200
 
201
- // Bulk insert new RoleMenu links
202
- int i = 1;
203
- foreach (var menuCode in dto.MenuCodes)
 
 
 
204
  {
205
  _db.RoleMenus.Add(new RoleMenu
206
  {
207
- RoleMenuId = $"RM_{role.Id}_{i++}_{DateTime.UtcNow.Ticks}",
208
  RoleId = role.Id,
209
- RoleCode = role.Name,
210
- MenuCode = menuCode,
211
- DelFlag = 0,
212
- CreatedDateTime = DateTime.UtcNow
 
 
 
 
 
 
 
 
 
 
213
  });
214
  }
215
 
@@ -217,24 +263,43 @@ namespace TaskTrackingSystem.WebApi.Features.Role
217
  return Result.Success(200);
218
  }
219
 
220
- private async Task<Result> ValidatePermissionCodesAsync(IEnumerable<string> menuCodes)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  {
222
- var validMenuCodes = await _db.MenuAdmins
223
- .Where(m => m.DelFlag == 0)
 
 
 
 
 
224
  .Select(m => m.MenuCode)
225
  .ToListAsync();
226
 
227
- var validActionCodes = await _db.MenuAdminDetails
228
- .Where(d => d.DelFlag == 0)
229
- .Select(d => d.MenuDetailCode)
230
  .ToListAsync();
231
 
232
  var validCodes = validMenuCodes
233
- .Concat(validActionCodes)
234
  .ToHashSet(StringComparer.OrdinalIgnoreCase);
235
 
236
- var invalidCodes = menuCodes
237
- .Where(code => !string.IsNullOrWhiteSpace(code))
238
  .Where(code => !validCodes.Contains(code))
239
  .Distinct(StringComparer.OrdinalIgnoreCase)
240
  .ToList();
 
28
  Name = r.Name,
29
  Description = r.Description,
30
  CreatedAt = r.CreatedAt ?? DateTime.UtcNow
31
+ })
32
+ .ToListAsync();
33
  }
34
 
35
  public async Task<RoleDto?> GetRoleByIdAsync(long id)
36
  {
37
+ var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == id && r.IsDeleted != true);
38
+ if (role == null)
39
+ {
40
+ return null;
41
+ }
42
 
43
  return new RoleDto
44
  {
 
71
  }
72
  }
73
 
74
+ var role = new Role
75
  {
76
  Name = dto.Name,
77
  Description = dto.Description,
 
88
  return Result<RoleDto>.Failure(assignResult.ErrorMessage ?? ResultMessages.FailedToCreateRole, assignResult.StatusCode);
89
  }
90
 
91
+ return Result<RoleDto>.Success(new RoleDto
92
  {
93
  Id = role.Id,
94
  Name = role.Name,
95
  Description = role.Description,
96
  CreatedAt = role.CreatedAt ?? DateTime.UtcNow
97
+ }, 201);
 
 
98
  }
99
 
100
  public async Task<Result> UpdateRoleAsync(long id, UpdateRoleDto dto, long? currentUserId = null)
 
105
  }
106
 
107
  var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == id && r.IsDeleted != true);
108
+ if (role == null)
109
+ {
110
+ return Result.Failure(ResultMessages.RoleNotFound(id), 404);
111
+ }
112
 
113
  var nameExists = await _db.Roles.AnyAsync(r => r.Name == dto.Name && r.Id != id && r.IsDeleted != true);
114
  if (nameExists)
 
116
  return Result.Failure("Role name is already taken by another role.", 400);
117
  }
118
 
 
119
  role.Name = dto.Name;
120
  role.Description = dto.Description;
121
  role.UpdatedAt = DateTime.UtcNow;
122
  role.UpdatedBy = currentUserId;
123
 
 
 
 
 
 
 
 
 
 
124
  _db.Roles.Update(role);
125
  await _db.SaveChangesAsync();
126
  return Result.Success(200);
 
129
  public async Task<Result> SoftDeleteRoleAsync(long id)
130
  {
131
  var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == id && r.IsDeleted != true);
132
+ if (role == null)
133
+ {
134
+ return Result.Failure(ResultMessages.RoleNotFound(id), 404);
135
+ }
136
 
137
  role.IsDeleted = true;
138
  _db.Roles.Update(role);
 
140
  return Result.Success(200);
141
  }
142
 
 
143
  public async Task<Result<List<string>>> GetAssignedMenusByRoleIdAsync(long roleId)
144
  {
145
  var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == roleId && r.IsDeleted != true);
 
149
  }
150
 
151
  var menuCodes = await _db.RoleMenus
152
+ .Where(rm => rm.RoleId == role.Id && !rm.IsDeleted)
153
+ .Select(rm => rm.Menu.MenuCode)
154
  .ToListAsync();
155
 
156
+ var permissionCodes = await _db.RolePermissions
157
+ .Where(rp => rp.RoleId == role.Id && !rp.IsDeleted)
158
+ .Select(rp => rp.Permission.PermissionCode)
159
+ .ToListAsync();
160
+
161
+ var assignedCodes = menuCodes
162
+ .Concat(permissionCodes)
163
+ .Distinct(StringComparer.OrdinalIgnoreCase)
164
+ .ToList();
165
 
166
+ return Result<List<string>>.Success(assignedCodes);
167
  }
168
 
169
  public async Task<Result> AssignMenusToRoleAsync(long roleId, AssignMenusDto dto)
 
179
  return Result.Failure("MenuCodes cannot be null", 400);
180
  }
181
 
182
+ var selectedCodes = dto.MenuCodes
183
+ .Where(code => !string.IsNullOrWhiteSpace(code))
184
+ .Select(code => code.Trim())
185
+ .Distinct(StringComparer.OrdinalIgnoreCase)
186
+ .ToList();
187
+
188
+ var validCodesResult = await ValidatePermissionCodesAsync(selectedCodes);
189
  if (!validCodesResult.IsSuccess)
190
  {
191
  return Result.Failure(validCodesResult.ErrorMessage ?? ResultMessages.FailedToUpdateRole, validCodesResult.StatusCode);
192
  }
193
 
194
+ var selectedMenus = await _db.Menus
195
+ .Where(m => !m.IsDeleted && selectedCodes.Contains(m.MenuCode))
196
+ .Select(m => new { m.MenuId, m.MenuCode, m.ParentMenuId })
197
+ .ToListAsync();
198
+
199
+ var selectedPermissions = await _db.Permissions
200
+ .Where(p => !p.IsDeleted && selectedCodes.Contains(p.PermissionCode))
201
+ .Select(p => new { p.PermissionId, p.PermissionCode, p.MenuId })
202
+ .ToListAsync();
203
+
204
+ var menuLookup = await _db.Menus
205
+ .Where(m => !m.IsDeleted)
206
+ .Select(m => new { m.MenuId, m.ParentMenuId })
207
  .ToListAsync();
208
 
209
+ var menuIdLookup = menuLookup.ToDictionary(x => x.MenuId, x => x.ParentMenuId);
210
+ var menuIdsToPersist = new HashSet<long>();
211
+
212
+ foreach (var menu in selectedMenus)
213
+ {
214
+ AddWithAncestors(menu.MenuId, menuIdLookup, menuIdsToPersist);
215
+ }
216
+
217
+ foreach (var permission in selectedPermissions)
218
+ {
219
+ AddWithAncestors(permission.MenuId, menuIdLookup, menuIdsToPersist);
220
+ }
221
+
222
+ var permissionIdsToPersist = selectedPermissions
223
+ .Select(p => p.PermissionId)
224
+ .Distinct()
225
+ .ToList();
226
+
227
+ var existingRoleMenus = await _db.RoleMenus.Where(rm => rm.RoleId == role.Id).ToListAsync();
228
+ var existingRolePermissions = await _db.RolePermissions.Where(rp => rp.RoleId == role.Id).ToListAsync();
229
+
230
  if (existingRoleMenus.Any())
231
  {
232
  _db.RoleMenus.RemoveRange(existingRoleMenus);
233
  }
234
 
235
+ if (existingRolePermissions.Any())
236
+ {
237
+ _db.RolePermissions.RemoveRange(existingRolePermissions);
238
+ }
239
+
240
+ foreach (var menuId in menuIdsToPersist)
241
  {
242
  _db.RoleMenus.Add(new RoleMenu
243
  {
 
244
  RoleId = role.Id,
245
+ MenuId = menuId,
246
+ IsDeleted = false,
247
+ CreatedAt = DateTime.UtcNow
248
+ });
249
+ }
250
+
251
+ foreach (var permissionId in permissionIdsToPersist)
252
+ {
253
+ _db.RolePermissions.Add(new RolePermission
254
+ {
255
+ RoleId = role.Id,
256
+ PermissionId = permissionId,
257
+ IsDeleted = false,
258
+ CreatedAt = DateTime.UtcNow
259
  });
260
  }
261
 
 
263
  return Result.Success(200);
264
  }
265
 
266
+ private static void AddWithAncestors(long menuId, IReadOnlyDictionary<long, long?> parentLookup, ISet<long> target)
267
+ {
268
+ var current = menuId;
269
+
270
+ while (current > 0 && target.Add(current))
271
+ {
272
+ if (!parentLookup.TryGetValue(current, out var parentId) || !parentId.HasValue)
273
+ {
274
+ break;
275
+ }
276
+
277
+ current = parentId.Value;
278
+ }
279
+ }
280
+
281
+ private async Task<Result> ValidatePermissionCodesAsync(IEnumerable<string> codes)
282
  {
283
+ var normalizedCodes = codes
284
+ .Where(code => !string.IsNullOrWhiteSpace(code))
285
+ .Select(code => code.Trim())
286
+ .ToList();
287
+
288
+ var validMenuCodes = await _db.Menus
289
+ .Where(m => !m.IsDeleted)
290
  .Select(m => m.MenuCode)
291
  .ToListAsync();
292
 
293
+ var validPermissionCodes = await _db.Permissions
294
+ .Where(p => !p.IsDeleted)
295
+ .Select(p => p.PermissionCode)
296
  .ToListAsync();
297
 
298
  var validCodes = validMenuCodes
299
+ .Concat(validPermissionCodes)
300
  .ToHashSet(StringComparer.OrdinalIgnoreCase);
301
 
302
+ var invalidCodes = normalizedCodes
 
303
  .Where(code => !validCodes.Contains(code))
304
  .Distinct(StringComparer.OrdinalIgnoreCase)
305
  .ToList();
TaskTrackingSystem.WebApp/Components/Layout/NavMenu.razor CHANGED
@@ -94,16 +94,55 @@
94
  private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
95
  {
96
  currentPath = Navigation.ToBaseRelativePath(e.Location).Trim('/');
 
 
97
 
98
- foreach (var menu in menus)
 
 
99
  {
100
- if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
 
 
 
101
  {
102
- expandedMenuGroups.Add(menu.MenuCode);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  }
104
  }
 
 
 
 
105
 
106
- InvokeAsync(StateHasChanged);
107
  }
108
 
109
  private void ToggleMenuGroup(string menuCode)
 
94
  private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
95
  {
96
  currentPath = Navigation.ToBaseRelativePath(e.Location).Trim('/');
97
+ InvokeAsync(RefreshMenusAsync);
98
+ }
99
 
100
+ private async Task RefreshMenusAsync()
101
+ {
102
+ try
103
  {
104
+ var authState = await AuthStateProvider.GetAuthenticationStateAsync();
105
+ var user = authState.User;
106
+
107
+ if (user.Identity?.IsAuthenticated == true)
108
  {
109
+ // GetUserMenusAsync will automatically bust the cache if an admin changed permissions.
110
+ var freshMenus = await MenuAuthorization.GetUserMenusAsync(user);
111
+
112
+ // Only update and re-render if the menu list actually changed.
113
+ if (freshMenus.Count != menus.Count ||
114
+ freshMenus.Select(m => m.MenuCode).SequenceEqual(menus.Select(m => m.MenuCode)) == false)
115
+ {
116
+ menus = freshMenus;
117
+ BuildUrlHierarchy(menus);
118
+ expandedMenuGroups.Clear();
119
+ foreach (var menu in menus)
120
+ {
121
+ if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
122
+ {
123
+ expandedMenuGroups.Add(menu.MenuCode);
124
+ }
125
+ }
126
+ }
127
+ else
128
+ {
129
+ // Menus are the same — just refresh expanded state for the new route.
130
+ foreach (var menu in menus)
131
+ {
132
+ if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
133
+ {
134
+ expandedMenuGroups.Add(menu.MenuCode);
135
+ }
136
+ }
137
+ }
138
  }
139
  }
140
+ catch (Exception ex)
141
+ {
142
+ Console.WriteLine($"Error refreshing menus on navigation: {ex.Message}");
143
+ }
144
 
145
+ StateHasChanged();
146
  }
147
 
148
  private void ToggleMenuGroup(string menuCode)
TaskTrackingSystem.WebApp/MenuAuthorizationService.cs CHANGED
@@ -102,7 +102,8 @@ public class MenuAuthorizationService
102
 
103
  if (_sessionState.CachedMenuRoleId == roleKey && _sessionState.CachedMenus != null)
104
  {
105
- return Task.FromResult(_sessionState.CachedMenus);
 
106
  }
107
 
108
  return LoadMenusAsync(user, roleKey);
@@ -124,6 +125,43 @@ public class MenuAuthorizationService
124
  _ = LoadMenusAsync(user, roleKey);
125
  }
126
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  private Task<List<MenuDto>> LoadMenusAsync(ClaimsPrincipal user, string roleKey)
128
  {
129
  if (_loadingMenus is { IsCompleted: false })
@@ -141,14 +179,21 @@ public class MenuAuthorizationService
141
  {
142
  using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
143
  var client = _apiClient.CreateClient(user);
144
- var response = await client.GetAsync("Menu", cts.Token);
145
- if (!response.IsSuccessStatusCode)
 
 
 
 
 
 
 
146
  {
147
- Console.WriteLine($"Menu API failed: {(int)response.StatusCode} {response.ReasonPhrase}");
148
  return new List<MenuDto>();
149
  }
150
 
151
- var menus = await response.Content.ReadFromJsonAsync<List<MenuDto>>(cancellationToken: cts.Token)
152
  ?? new List<MenuDto>();
153
 
154
  if (menus.Count == 0)
@@ -156,6 +201,14 @@ public class MenuAuthorizationService
156
  menus = BuildFallbackMenus(user);
157
  }
158
 
 
 
 
 
 
 
 
 
159
  _sessionState.CachedMenus = menus;
160
  _sessionState.CachedMenuRoleId = roleKey;
161
  return menus;
 
102
 
103
  if (_sessionState.CachedMenuRoleId == roleKey && _sessionState.CachedMenus != null)
104
  {
105
+ // Cache hit — but verify the permissions haven't been changed by an admin.
106
+ return LoadMenusWithVersionCheckAsync(user, roleKey);
107
  }
108
 
109
  return LoadMenusAsync(user, roleKey);
 
125
  _ = LoadMenusAsync(user, roleKey);
126
  }
127
 
128
+ /// <summary>
129
+ /// When menus are cached, quickly checks the server-side version to see if an admin changed permissions.
130
+ /// If the version changed, busts the cache and re-fetches the full menu list.
131
+ /// Falls back to the cached menus if the version check itself fails (e.g. network error).
132
+ /// </summary>
133
+ private async Task<List<MenuDto>> LoadMenusWithVersionCheckAsync(ClaimsPrincipal user, string roleKey)
134
+ {
135
+ try
136
+ {
137
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
138
+ var client = _apiClient.CreateClient(user);
139
+ var response = await client.GetAsync("Menu/version", cts.Token);
140
+
141
+ if (response.IsSuccessStatusCode)
142
+ {
143
+ var serverVersion = await response.Content.ReadAsStringAsync(cts.Token);
144
+ // Strip surrounding quotes that JSON serialisation may add
145
+ serverVersion = serverVersion.Trim('"');
146
+
147
+ if (serverVersion != _sessionState.CachedMenuVersion)
148
+ {
149
+ // Permissions have changed — bust cache and reload full menus.
150
+ Console.WriteLine($"[MenuAuth] Permission version changed ({_sessionState.CachedMenuVersion} -> {serverVersion}). Reloading menus.");
151
+ _sessionState.ClearMenuCache();
152
+ return await LoadMenusAsync(user, roleKey);
153
+ }
154
+ }
155
+ }
156
+ catch (Exception ex)
157
+ {
158
+ // If the version check fails, just use the cached menus to avoid breaking the UI.
159
+ Console.WriteLine($"[MenuAuth] Version check failed (using cache): {ex.Message}");
160
+ }
161
+
162
+ return _sessionState.CachedMenus!;
163
+ }
164
+
165
  private Task<List<MenuDto>> LoadMenusAsync(ClaimsPrincipal user, string roleKey)
166
  {
167
  if (_loadingMenus is { IsCompleted: false })
 
179
  {
180
  using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
181
  var client = _apiClient.CreateClient(user);
182
+
183
+ // Fetch menus and version in parallel for efficiency.
184
+ var menuTask = client.GetAsync("Menu", cts.Token);
185
+ var versionTask = client.GetAsync("Menu/version", cts.Token);
186
+
187
+ await Task.WhenAll(menuTask, versionTask);
188
+
189
+ var menuResponse = await menuTask;
190
+ if (!menuResponse.IsSuccessStatusCode)
191
  {
192
+ Console.WriteLine($"Menu API failed: {(int)menuResponse.StatusCode} {menuResponse.ReasonPhrase}");
193
  return new List<MenuDto>();
194
  }
195
 
196
+ var menus = await menuResponse.Content.ReadFromJsonAsync<List<MenuDto>>(cancellationToken: cts.Token)
197
  ?? new List<MenuDto>();
198
 
199
  if (menus.Count == 0)
 
201
  menus = BuildFallbackMenus(user);
202
  }
203
 
204
+ // Store the version so we can detect future permission changes.
205
+ var versionResponse = await versionTask;
206
+ if (versionResponse.IsSuccessStatusCode)
207
+ {
208
+ var version = await versionResponse.Content.ReadAsStringAsync(cts.Token);
209
+ _sessionState.CachedMenuVersion = version.Trim('"');
210
+ }
211
+
212
  _sessionState.CachedMenus = menus;
213
  _sessionState.CachedMenuRoleId = roleKey;
214
  return menus;
TaskTrackingSystem.WebApp/UserSessionState.cs CHANGED
@@ -11,10 +11,17 @@ public class UserSessionState
11
  public string? CachedMenuRoleId { get; set; }
12
  public List<MenuDto>? CachedMenus { get; set; }
13
 
 
 
 
 
 
 
14
  public void ClearMenuCache()
15
  {
16
  CachedMenuRoleId = null;
17
  CachedMenus = null;
 
18
  }
19
 
20
  public void ClearSession()
 
11
  public string? CachedMenuRoleId { get; set; }
12
  public List<MenuDto>? CachedMenus { get; set; }
13
 
14
+ /// <summary>
15
+ /// The server-side permissions version (latest RoleMenu timestamp) when the menus were last fetched.
16
+ /// Used to detect when an admin has changed a role's permissions so the cache can be busted.
17
+ /// </summary>
18
+ public string? CachedMenuVersion { get; set; }
19
+
20
  public void ClearMenuCache()
21
  {
22
  CachedMenuRoleId = null;
23
  CachedMenus = null;
24
+ CachedMenuVersion = null;
25
  }
26
 
27
  public void ClearSession()
permission-schema-upgrade.sql ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Safe upgrade path for permission tables.
3
+
4
+ What this script does:
5
+ 1. Renames the old dbo.RoleMenus table to dbo.RoleMenus_Legacy if it still uses MenuCode.
6
+ 2. Creates the new normalized tables:
7
+ - dbo.Menus
8
+ - dbo.Permissions
9
+ - dbo.RoleMenus
10
+ - dbo.RolePermissions
11
+ 3. Copies data from the old schema into the new schema.
12
+
13
+ Run this once in SQL Server Management Studio after backing up the database.
14
+ */
15
+
16
+ SET XACT_ABORT ON;
17
+ BEGIN TRY
18
+ BEGIN TRANSACTION;
19
+
20
+ -------------------------------------------------------------------------
21
+ -- Step 1: rename the legacy RoleMenus table if needed
22
+ -------------------------------------------------------------------------
23
+ IF OBJECT_ID('dbo.RoleMenus', 'U') IS NOT NULL
24
+ AND COL_LENGTH('dbo.RoleMenus', 'MenuId') IS NULL
25
+ AND COL_LENGTH('dbo.RoleMenus', 'MenuCode') IS NOT NULL
26
+ AND OBJECT_ID('dbo.RoleMenus_Legacy', 'U') IS NULL
27
+ BEGIN
28
+ EXEC sp_rename 'dbo.RoleMenus', 'RoleMenus_Legacy';
29
+ END
30
+
31
+ -------------------------------------------------------------------------
32
+ -- Step 2: create the new tables if they do not already exist
33
+ -------------------------------------------------------------------------
34
+ IF OBJECT_ID('dbo.Menus', 'U') IS NULL
35
+ BEGIN
36
+ CREATE TABLE dbo.Menus (
37
+ MenuId bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_Menus PRIMARY KEY,
38
+ MenuCode nvarchar(50) NOT NULL,
39
+ ParentMenuId bigint NULL,
40
+ MenuName nvarchar(100) NOT NULL,
41
+ MenuUrl nvarchar(200) NULL,
42
+ Icon nvarchar(50) NULL,
43
+ Visible bit NOT NULL CONSTRAINT DF_Menus_Visible DEFAULT (1),
44
+ OrderNo int NOT NULL CONSTRAINT DF_Menus_OrderNo DEFAULT (0),
45
+ IsDeleted bit NOT NULL CONSTRAINT DF_Menus_IsDeleted DEFAULT (0),
46
+ CreatedById bigint NULL,
47
+ CreatedAt datetime2(0) NOT NULL CONSTRAINT DF_Menus_CreatedAt DEFAULT (SYSUTCDATETIME()),
48
+ UpdatedById bigint NULL,
49
+ UpdatedAt datetime2(0) NULL,
50
+ CONSTRAINT UQ_Menus_MenuCode UNIQUE (MenuCode)
51
+ );
52
+
53
+ ALTER TABLE dbo.Menus
54
+ ADD CONSTRAINT FK_Menus_ParentMenu
55
+ FOREIGN KEY (ParentMenuId) REFERENCES dbo.Menus(MenuId);
56
+
57
+ CREATE INDEX IX_Menus_ParentMenuId ON dbo.Menus(ParentMenuId);
58
+ END
59
+
60
+ IF OBJECT_ID('dbo.Permissions', 'U') IS NULL
61
+ BEGIN
62
+ CREATE TABLE dbo.Permissions (
63
+ PermissionId bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_Permissions PRIMARY KEY,
64
+ PermissionCode nvarchar(50) NOT NULL,
65
+ MenuId bigint NOT NULL,
66
+ ActionName nvarchar(100) NOT NULL,
67
+ ApiName nvarchar(100) NOT NULL,
68
+ HttpMethod nvarchar(20) NULL,
69
+ Visible bit NOT NULL CONSTRAINT DF_Permissions_Visible DEFAULT (1),
70
+ OrderNo int NOT NULL CONSTRAINT DF_Permissions_OrderNo DEFAULT (0),
71
+ IsDeleted bit NOT NULL CONSTRAINT DF_Permissions_IsDeleted DEFAULT (0),
72
+ CreatedById bigint NULL,
73
+ CreatedAt datetime2(0) NOT NULL CONSTRAINT DF_Permissions_CreatedAt DEFAULT (SYSUTCDATETIME()),
74
+ UpdatedById bigint NULL,
75
+ UpdatedAt datetime2(0) NULL,
76
+ CONSTRAINT UQ_Permissions_PermissionCode UNIQUE (PermissionCode)
77
+ );
78
+
79
+ ALTER TABLE dbo.Permissions
80
+ ADD CONSTRAINT FK_Permissions_Menu
81
+ FOREIGN KEY (MenuId) REFERENCES dbo.Menus(MenuId);
82
+
83
+ CREATE INDEX IX_Permissions_MenuId ON dbo.Permissions(MenuId);
84
+ END
85
+
86
+ IF OBJECT_ID('dbo.RoleMenus', 'U') IS NULL
87
+ BEGIN
88
+ CREATE TABLE dbo.RoleMenus (
89
+ RoleMenuId bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_RoleMenus PRIMARY KEY,
90
+ RoleId bigint NOT NULL,
91
+ MenuId bigint NOT NULL,
92
+ IsDeleted bit NOT NULL CONSTRAINT DF_RoleMenus_IsDeleted DEFAULT (0),
93
+ CreatedById bigint NULL,
94
+ CreatedAt datetime2(0) NOT NULL CONSTRAINT DF_RoleMenus_CreatedAt DEFAULT (SYSUTCDATETIME()),
95
+ UpdatedById bigint NULL,
96
+ UpdatedAt datetime2(0) NULL,
97
+ CONSTRAINT UQ_RoleMenus_Role_Menu UNIQUE (RoleId, MenuId)
98
+ );
99
+
100
+ ALTER TABLE dbo.RoleMenus
101
+ ADD CONSTRAINT FK_RoleMenus_Roles
102
+ FOREIGN KEY (RoleId) REFERENCES dbo.Roles(Id);
103
+
104
+ ALTER TABLE dbo.RoleMenus
105
+ ADD CONSTRAINT FK_RoleMenus_Menus
106
+ FOREIGN KEY (MenuId) REFERENCES dbo.Menus(MenuId);
107
+
108
+ CREATE INDEX IX_RoleMenus_RoleId ON dbo.RoleMenus(RoleId);
109
+ CREATE INDEX IX_RoleMenus_MenuId ON dbo.RoleMenus(MenuId);
110
+ END
111
+
112
+ IF OBJECT_ID('dbo.RolePermissions', 'U') IS NULL
113
+ BEGIN
114
+ CREATE TABLE dbo.RolePermissions (
115
+ RolePermissionId bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_RolePermissions PRIMARY KEY,
116
+ RoleId bigint NOT NULL,
117
+ PermissionId bigint NOT NULL,
118
+ IsDeleted bit NOT NULL CONSTRAINT DF_RolePermissions_IsDeleted DEFAULT (0),
119
+ CreatedById bigint NULL,
120
+ CreatedAt datetime2(0) NOT NULL CONSTRAINT DF_RolePermissions_CreatedAt DEFAULT (SYSUTCDATETIME()),
121
+ UpdatedById bigint NULL,
122
+ UpdatedAt datetime2(0) NULL,
123
+ CONSTRAINT UQ_RolePermissions_Role_Permission UNIQUE (RoleId, PermissionId)
124
+ );
125
+
126
+ ALTER TABLE dbo.RolePermissions
127
+ ADD CONSTRAINT FK_RolePermissions_Roles
128
+ FOREIGN KEY (RoleId) REFERENCES dbo.Roles(Id);
129
+
130
+ ALTER TABLE dbo.RolePermissions
131
+ ADD CONSTRAINT FK_RolePermissions_Permissions
132
+ FOREIGN KEY (PermissionId) REFERENCES dbo.Permissions(PermissionId);
133
+
134
+ CREATE INDEX IX_RolePermissions_RoleId ON dbo.RolePermissions(RoleId);
135
+ CREATE INDEX IX_RolePermissions_PermissionId ON dbo.RolePermissions(PermissionId);
136
+ END
137
+
138
+ -------------------------------------------------------------------------
139
+ -- Step 3: build mapping tables from the old source tables
140
+ -------------------------------------------------------------------------
141
+ IF OBJECT_ID('tempdb..#MenuStage') IS NOT NULL DROP TABLE #MenuStage;
142
+ IF OBJECT_ID('tempdb..#PermissionStage') IS NOT NULL DROP TABLE #PermissionStage;
143
+
144
+ CREATE TABLE #MenuStage (
145
+ OldMenuCode nvarchar(50) NOT NULL PRIMARY KEY,
146
+ OldParentCode nvarchar(50) NULL,
147
+ NewMenuId bigint NOT NULL
148
+ );
149
+
150
+ CREATE TABLE #PermissionStage (
151
+ OldPermissionCode nvarchar(50) NOT NULL PRIMARY KEY,
152
+ OldParentCode nvarchar(50) NOT NULL,
153
+ NewPermissionId bigint NOT NULL
154
+ );
155
+
156
+ -------------------------------------------------------------------------
157
+ -- Step 4: copy menus
158
+ -------------------------------------------------------------------------
159
+ IF OBJECT_ID('dbo.Menus', 'U') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM dbo.Menus)
160
+ BEGIN
161
+ INSERT INTO dbo.Menus (
162
+ MenuCode, ParentMenuId, MenuName, MenuUrl, Icon,
163
+ Visible, OrderNo, IsDeleted,
164
+ CreatedById, CreatedAt, UpdatedById, UpdatedAt
165
+ )
166
+ OUTPUT
167
+ inserted.MenuCode,
168
+ inserted.MenuId
169
+ INTO #MenuStage (OldMenuCode, NewMenuId)
170
+ SELECT
171
+ src.MenuCode,
172
+ NULL,
173
+ src.MenuName,
174
+ src.MenuUrl,
175
+ src.Icon,
176
+ src.Visible,
177
+ src.OrderNo,
178
+ CASE WHEN src.DelFlag = 0 THEN 0 ELSE 1 END,
179
+ TRY_CONVERT(bigint, src.CreatedUserId),
180
+ src.CreatedDateTime,
181
+ TRY_CONVERT(bigint, src.ModifiedUserId),
182
+ src.ModifiedDateTime
183
+ FROM dbo.MenuAdmins src
184
+ WHERE src.DelFlag = 0;
185
+
186
+ UPDATE stage
187
+ SET stage.OldParentCode = src.ParentCode
188
+ FROM #MenuStage stage
189
+ INNER JOIN dbo.MenuAdmins src
190
+ ON src.MenuCode = stage.OldMenuCode;
191
+
192
+ UPDATE m
193
+ SET ParentMenuId = parent.NewMenuId
194
+ FROM dbo.Menus m
195
+ INNER JOIN #MenuStage child
196
+ ON child.NewMenuId = m.MenuId
197
+ LEFT JOIN #MenuStage parent
198
+ ON parent.OldMenuCode = NULLIF(LTRIM(RTRIM(child.OldParentCode)), '')
199
+ WHERE NULLIF(LTRIM(RTRIM(child.OldParentCode)), '') IS NOT NULL
200
+ AND LTRIM(RTRIM(child.OldParentCode)) <> '0';
201
+ END
202
+
203
+ -------------------------------------------------------------------------
204
+ -- Step 5: copy permissions
205
+ -------------------------------------------------------------------------
206
+ IF OBJECT_ID('dbo.Permissions', 'U') IS NOT NULL AND NOT EXISTS (SELECT 1 FROM dbo.Permissions)
207
+ BEGIN
208
+ INSERT INTO dbo.Permissions (
209
+ PermissionCode, MenuId, ActionName, ApiName, HttpMethod,
210
+ Visible, OrderNo, IsDeleted,
211
+ CreatedById, CreatedAt, UpdatedById, UpdatedAt
212
+ )
213
+ OUTPUT
214
+ inserted.PermissionCode,
215
+ inserted.PermissionId
216
+ INTO #PermissionStage (OldPermissionCode, NewPermissionId)
217
+ SELECT
218
+ src.MenuDetailCode,
219
+ m.NewMenuId,
220
+ src.ActionName,
221
+ src.ApiName,
222
+ NULL,
223
+ src.Visible,
224
+ src.OrderNo,
225
+ CASE WHEN src.DelFlag = 0 THEN 0 ELSE 1 END,
226
+ TRY_CONVERT(bigint, src.CreatedUserId),
227
+ src.CreatedDateTime,
228
+ TRY_CONVERT(bigint, src.ModifiedUserId),
229
+ src.ModifiedDateTime
230
+ FROM dbo.MenuAdminDetails src
231
+ INNER JOIN #MenuStage m
232
+ ON m.OldMenuCode = src.ParentMenuCode
233
+ WHERE src.DelFlag = 0;
234
+
235
+ UPDATE stage
236
+ SET stage.OldParentCode = src.ParentMenuCode
237
+ FROM #PermissionStage stage
238
+ INNER JOIN dbo.MenuAdminDetails src
239
+ ON src.MenuDetailCode = stage.OldPermissionCode;
240
+ END
241
+
242
+ -------------------------------------------------------------------------
243
+ -- Step 6: copy role assignments from the legacy table if it exists
244
+ -------------------------------------------------------------------------
245
+ IF OBJECT_ID('dbo.RoleMenus_Legacy', 'U') IS NOT NULL
246
+ BEGIN
247
+ INSERT INTO dbo.RoleMenus (
248
+ RoleId, MenuId, IsDeleted,
249
+ CreatedById, CreatedAt, UpdatedById, UpdatedAt
250
+ )
251
+ SELECT DISTINCT
252
+ COALESCE(roleById.Id, roleByCode.Id) AS RoleId,
253
+ menuStage.NewMenuId,
254
+ 0,
255
+ TRY_CONVERT(bigint, src.CreatedUserId),
256
+ src.CreatedDateTime,
257
+ TRY_CONVERT(bigint, src.ModifiedUserId),
258
+ src.ModifiedDateTime
259
+ FROM dbo.RoleMenus_Legacy src
260
+ LEFT JOIN dbo.Roles roleById
261
+ ON roleById.Id = src.RoleId
262
+ AND roleById.IsDeleted <> 1
263
+ LEFT JOIN dbo.Roles roleByCode
264
+ ON roleByCode.Name = src.RoleCode
265
+ AND roleByCode.IsDeleted <> 1
266
+ INNER JOIN #MenuStage menuStage
267
+ ON menuStage.OldMenuCode = src.MenuCode
268
+ WHERE src.DelFlag = 0
269
+ AND COALESCE(roleById.Id, roleByCode.Id) IS NOT NULL;
270
+
271
+ INSERT INTO dbo.RolePermissions (
272
+ RoleId, PermissionId, IsDeleted,
273
+ CreatedById, CreatedAt, UpdatedById, UpdatedAt
274
+ )
275
+ SELECT DISTINCT
276
+ COALESCE(roleById.Id, roleByCode.Id) AS RoleId,
277
+ permStage.NewPermissionId,
278
+ 0,
279
+ TRY_CONVERT(bigint, src.CreatedUserId),
280
+ src.CreatedDateTime,
281
+ TRY_CONVERT(bigint, src.ModifiedUserId),
282
+ src.ModifiedDateTime
283
+ FROM dbo.RoleMenus_Legacy src
284
+ LEFT JOIN dbo.Roles roleById
285
+ ON roleById.Id = src.RoleId
286
+ AND roleById.IsDeleted <> 1
287
+ LEFT JOIN dbo.Roles roleByCode
288
+ ON roleByCode.Name = src.RoleCode
289
+ AND roleByCode.IsDeleted <> 1
290
+ INNER JOIN #PermissionStage permStage
291
+ ON permStage.OldPermissionCode = src.MenuCode
292
+ WHERE src.DelFlag = 0
293
+ AND COALESCE(roleById.Id, roleByCode.Id) IS NOT NULL;
294
+
295
+ INSERT INTO dbo.RoleMenus (
296
+ RoleId, MenuId, IsDeleted,
297
+ CreatedById, CreatedAt, UpdatedById, UpdatedAt
298
+ )
299
+ SELECT DISTINCT
300
+ COALESCE(roleById.Id, roleByCode.Id) AS RoleId,
301
+ parentMenu.NewMenuId,
302
+ 0,
303
+ TRY_CONVERT(bigint, src.CreatedUserId),
304
+ src.CreatedDateTime,
305
+ TRY_CONVERT(bigint, src.ModifiedUserId),
306
+ src.ModifiedDateTime
307
+ FROM dbo.RoleMenus_Legacy src
308
+ LEFT JOIN dbo.Roles roleById
309
+ ON roleById.Id = src.RoleId
310
+ AND roleById.IsDeleted <> 1
311
+ LEFT JOIN dbo.Roles roleByCode
312
+ ON roleByCode.Name = src.RoleCode
313
+ AND roleByCode.IsDeleted <> 1
314
+ INNER JOIN #PermissionStage permStage
315
+ ON permStage.OldPermissionCode = src.MenuCode
316
+ INNER JOIN #MenuStage parentMenu
317
+ ON parentMenu.OldMenuCode = permStage.OldParentCode
318
+ WHERE src.DelFlag = 0
319
+ AND COALESCE(roleById.Id, roleByCode.Id) IS NOT NULL;
320
+ END
321
+
322
+ COMMIT TRANSACTION;
323
+ END TRY
324
+ BEGIN CATCH
325
+ IF @@TRANCOUNT > 0
326
+ ROLLBACK TRANSACTION;
327
+
328
+ THROW;
329
+ END CATCH;