User commited on
Commit
c42fd03
·
1 Parent(s): 18cf640

fix: rename menu access and permissions on dtos controllers and services

Browse files
TaskTrackingSystem.Shared/Models/Menu/{MenuAdminDto.cs → AccessMenuDto.cs} RENAMED
@@ -1,6 +1,6 @@
1
  namespace TaskTrackingSystem.Shared.Models.Menu
2
  {
3
- public class MenuAdminDto
4
  {
5
  public string MenuCode { get; set; } = string.Empty;
6
  public string ParentCode { get; set; } = string.Empty;
@@ -9,6 +9,7 @@ namespace TaskTrackingSystem.Shared.Models.Menu
9
  public int OrderNo { get; set; }
10
  public string? Icon { get; set; }
11
  public bool Visible { get; set; }
12
- public System.Collections.Generic.List<MenuAdminDetailDto> Actions { get; set; } = new();
13
  }
14
  }
 
 
1
  namespace TaskTrackingSystem.Shared.Models.Menu
2
  {
3
+ public class AccessMenuDto
4
  {
5
  public string MenuCode { get; set; } = string.Empty;
6
  public string ParentCode { get; set; } = string.Empty;
 
9
  public int OrderNo { get; set; }
10
  public string? Icon { get; set; }
11
  public bool Visible { get; set; }
12
+ public System.Collections.Generic.List<AccessPermissionDto> Permissions { get; set; } = new();
13
  }
14
  }
15
+
TaskTrackingSystem.Shared/Models/Menu/{MenuAdminDetailDto.cs → AccessPermissionDto.cs} RENAMED
@@ -2,7 +2,7 @@ using System;
2
 
3
  namespace TaskTrackingSystem.Shared.Models.Menu
4
  {
5
- public class MenuAdminDetailDto
6
  {
7
  public string MenuAdminDetailId { get; set; } = string.Empty;
8
  public string MenuDetailCode { get; set; } = string.Empty;
@@ -13,3 +13,4 @@ namespace TaskTrackingSystem.Shared.Models.Menu
13
  public int OrderNo { get; set; }
14
  }
15
  }
 
 
2
 
3
  namespace TaskTrackingSystem.Shared.Models.Menu
4
  {
5
+ public class AccessPermissionDto
6
  {
7
  public string MenuAdminDetailId { get; set; } = string.Empty;
8
  public string MenuDetailCode { get; set; } = string.Empty;
 
13
  public int OrderNo { get; set; }
14
  }
15
  }
16
+
TaskTrackingSystem.Shared/Models/Role/{AssignMenusDto.cs → AssignAccessDto.cs} RENAMED
@@ -3,12 +3,13 @@ using System.ComponentModel.DataAnnotations;
3
 
4
  namespace TaskTrackingSystem.Shared.Models.Role
5
  {
6
- public class AssignMenusDto
7
  {
8
  [Required]
9
  /// <summary>
10
  /// Menu codes and permission codes selected for the role.
11
  /// </summary>
12
- public List<string> MenuCodes { get; set; } = new List<string>();
13
  }
14
  }
 
 
3
 
4
  namespace TaskTrackingSystem.Shared.Models.Role
5
  {
6
+ public class AssignAccessDto
7
  {
8
  [Required]
9
  /// <summary>
10
  /// Menu codes and permission codes selected for the role.
11
  /// </summary>
12
+ public List<string> AccessCodes { get; set; } = new List<string>();
13
  }
14
  }
15
+
TaskTrackingSystem.Shared/Models/Role/CreateRoleDto.cs CHANGED
@@ -14,6 +14,7 @@ namespace TaskTrackingSystem.Shared.Models.Role
14
  /// <summary>
15
  /// Optional: Menu and permission codes to assign to this role on creation.
16
  /// </summary>
17
- public List<string> MenuCodes { get; set; } = new();
18
  }
19
  }
 
 
14
  /// <summary>
15
  /// Optional: Menu and permission codes to assign to this role on creation.
16
  /// </summary>
17
+ public List<string> AccessCodes { get; set; } = new();
18
  }
19
  }
20
+
TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs CHANGED
@@ -42,9 +42,9 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
42
 
43
  [Authorize(Roles = "Admin")]
44
  [HttpGet("all")]
45
- public async Task<ActionResult<IEnumerable<MenuAdminDto>>> GetAllMenus()
46
  {
47
- var menus = await _menuService.GetAllMenusAsync();
48
  return Ok(menus);
49
  }
50
 
@@ -62,3 +62,5 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
62
  }
63
  }
64
  }
 
 
 
42
 
43
  [Authorize(Roles = "Admin")]
44
  [HttpGet("all")]
45
+ public async Task<ActionResult<IEnumerable<AccessMenuDto>>> GetAllAccessItems()
46
  {
47
+ var menus = await _menuService.GetAllAccessItemsAsync();
48
  return Ok(menus);
49
  }
50
 
 
62
  }
63
  }
64
  }
65
+
66
+
TaskTrackingSystem.WebApi/Features/Menu/MenuService.cs CHANGED
@@ -184,6 +184,20 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
184
 
185
  public async Task<string> GetMenuVersionAsync(long roleId, string roleName)
186
  {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  var latestMenuChange = await _db.RoleMenus
188
  .Where(rm => rm.RoleId == roleId && !rm.IsDeleted)
189
  .Select(rm => (DateTime?)(rm.UpdatedAt ?? rm.CreatedAt))
@@ -203,7 +217,7 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
203
  return latest.HasValue ? latest.Value.Ticks.ToString() : "0";
204
  }
205
 
206
- public async Task<List<MenuAdminDto>> GetAllMenusAsync()
207
  {
208
  var menus = await _db.Menus
209
  .Where(m => !m.IsDeleted)
@@ -220,7 +234,7 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
220
  .Select(m => new
221
  {
222
  m.MenuId,
223
- Dto = new MenuAdminDto
224
  {
225
  MenuCode = m.MenuCode,
226
  ParentCode = GetParentCode(m, menuLookup),
@@ -245,7 +259,7 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
245
 
246
  foreach (var permission in permissionGroup.OrderBy(p => p.OrderNo).ThenBy(p => p.ActionName, StringComparer.OrdinalIgnoreCase))
247
  {
248
- dto.Actions.Add(new MenuAdminDetailDto
249
  {
250
  MenuAdminDetailId = permission.PermissionId.ToString(),
251
  MenuDetailCode = permission.PermissionCode,
@@ -262,3 +276,5 @@ namespace TaskTrackingSystem.WebApi.Features.Menu
262
  }
263
  }
264
  }
 
 
 
184
 
185
  public async Task<string> GetMenuVersionAsync(long roleId, string roleName)
186
  {
187
+ if (roleId <= 0 && !string.IsNullOrWhiteSpace(roleName))
188
+ {
189
+ var role = await _db.Roles.FirstOrDefaultAsync(r => r.Name == roleName && r.IsDeleted != true);
190
+ if (role != null)
191
+ {
192
+ roleId = role.Id;
193
+ }
194
+ }
195
+
196
+ if (roleId <= 0)
197
+ {
198
+ return "0";
199
+ }
200
+
201
  var latestMenuChange = await _db.RoleMenus
202
  .Where(rm => rm.RoleId == roleId && !rm.IsDeleted)
203
  .Select(rm => (DateTime?)(rm.UpdatedAt ?? rm.CreatedAt))
 
217
  return latest.HasValue ? latest.Value.Ticks.ToString() : "0";
218
  }
219
 
220
+ public async Task<List<AccessMenuDto>> GetAllAccessItemsAsync()
221
  {
222
  var menus = await _db.Menus
223
  .Where(m => !m.IsDeleted)
 
234
  .Select(m => new
235
  {
236
  m.MenuId,
237
+ Dto = new AccessMenuDto
238
  {
239
  MenuCode = m.MenuCode,
240
  ParentCode = GetParentCode(m, menuLookup),
 
259
 
260
  foreach (var permission in permissionGroup.OrderBy(p => p.OrderNo).ThenBy(p => p.ActionName, StringComparer.OrdinalIgnoreCase))
261
  {
262
+ dto.Permissions.Add(new AccessPermissionDto
263
  {
264
  MenuAdminDetailId = permission.PermissionId.ToString(),
265
  MenuDetailCode = permission.PermissionCode,
 
276
  }
277
  }
278
  }
279
+
280
+
TaskTrackingSystem.WebApi/Features/Role/RoleController.cs CHANGED
@@ -71,9 +71,9 @@ namespace TaskTrackingSystem.WebApi.Features.Role
71
  }
72
 
73
  [HttpPost("{id}/menus")]
74
- public async Task<IActionResult> AssignMenus(long id, [FromBody] AssignMenusDto dto)
75
  {
76
- var result = await _roleService.AssignMenusToRoleAsync(id, dto);
77
  if (!result.IsSuccess)
78
  {
79
  return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
@@ -82,3 +82,5 @@ namespace TaskTrackingSystem.WebApi.Features.Role
82
  }
83
  }
84
  }
 
 
 
71
  }
72
 
73
  [HttpPost("{id}/menus")]
74
+ public async Task<IActionResult> AssignAccess(long id, [FromBody] AssignAccessDto dto)
75
  {
76
+ var result = await _roleService.AssignAccessToRoleAsync(id, dto);
77
  if (!result.IsSuccess)
78
  {
79
  return StatusCode(result.StatusCode, new { message = result.ErrorMessage });
 
82
  }
83
  }
84
  }
85
+
86
+
TaskTrackingSystem.WebApi/Features/Role/RoleService.cs CHANGED
@@ -63,9 +63,9 @@ namespace TaskTrackingSystem.WebApi.Features.Role
63
  return Result<RoleDto>.Failure("Role name is already taken.", 400);
64
  }
65
 
66
- if (dto.MenuCodes != null && dto.MenuCodes.Any())
67
  {
68
- var validCodesResult = await ValidatePermissionCodesAsync(dto.MenuCodes);
69
  if (!validCodesResult.IsSuccess)
70
  {
71
  return Result<RoleDto>.Failure(validCodesResult.ErrorMessage ?? ResultMessages.FailedToCreateRole, validCodesResult.StatusCode);
@@ -83,7 +83,7 @@ namespace TaskTrackingSystem.WebApi.Features.Role
83
  _db.Roles.Add(role);
84
  await _db.SaveChangesAsync();
85
 
86
- var assignResult = await AssignMenusToRoleAsync(role.Id, new AssignMenusDto { MenuCodes = dto.MenuCodes ?? new List<string>() });
87
  if (!assignResult.IsSuccess)
88
  {
89
  return Result<RoleDto>.Failure(assignResult.ErrorMessage ?? ResultMessages.FailedToCreateRole, assignResult.StatusCode);
@@ -167,7 +167,7 @@ namespace TaskTrackingSystem.WebApi.Features.Role
167
  return Result<List<string>>.Success(assignedCodes);
168
  }
169
 
170
- public async Task<Result> AssignMenusToRoleAsync(long roleId, AssignMenusDto dto)
171
  {
172
  var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == roleId && r.IsDeleted != true);
173
  if (role == null)
@@ -175,12 +175,12 @@ namespace TaskTrackingSystem.WebApi.Features.Role
175
  return Result.Failure(ResultMessages.RoleNotFound(roleId), 404);
176
  }
177
 
178
- if (dto.MenuCodes == null)
179
  {
180
- return Result.Failure("MenuCodes cannot be null", 400);
181
  }
182
 
183
- var selectedCodes = dto.MenuCodes
184
  .Where(code => !string.IsNullOrWhiteSpace(code))
185
  .Select(code => code.Trim())
186
  .Distinct(StringComparer.OrdinalIgnoreCase)
@@ -286,7 +286,7 @@ namespace TaskTrackingSystem.WebApi.Features.Role
286
  .Select(code => code.Trim())
287
  .ToList();
288
 
289
- var validMenuCodes = await _db.Menus
290
  .Where(m => !m.IsDeleted)
291
  .Select(m => m.MenuCode)
292
  .ToListAsync();
@@ -296,7 +296,7 @@ namespace TaskTrackingSystem.WebApi.Features.Role
296
  .Select(p => p.PermissionCode)
297
  .ToListAsync();
298
 
299
- var validCodes = validMenuCodes
300
  .Concat(validPermissionCodes)
301
  .ToHashSet(StringComparer.OrdinalIgnoreCase);
302
 
@@ -314,3 +314,5 @@ namespace TaskTrackingSystem.WebApi.Features.Role
314
  }
315
  }
316
  }
 
 
 
63
  return Result<RoleDto>.Failure("Role name is already taken.", 400);
64
  }
65
 
66
+ if (dto.AccessCodes != null && dto.AccessCodes.Any())
67
  {
68
+ var validCodesResult = await ValidatePermissionCodesAsync(dto.AccessCodes);
69
  if (!validCodesResult.IsSuccess)
70
  {
71
  return Result<RoleDto>.Failure(validCodesResult.ErrorMessage ?? ResultMessages.FailedToCreateRole, validCodesResult.StatusCode);
 
83
  _db.Roles.Add(role);
84
  await _db.SaveChangesAsync();
85
 
86
+ var assignResult = await AssignAccessToRoleAsync(role.Id, new AssignAccessDto { AccessCodes = dto.AccessCodes ?? new List<string>() });
87
  if (!assignResult.IsSuccess)
88
  {
89
  return Result<RoleDto>.Failure(assignResult.ErrorMessage ?? ResultMessages.FailedToCreateRole, assignResult.StatusCode);
 
167
  return Result<List<string>>.Success(assignedCodes);
168
  }
169
 
170
+ public async Task<Result> AssignAccessToRoleAsync(long roleId, AssignAccessDto dto)
171
  {
172
  var role = await _db.Roles.FirstOrDefaultAsync(r => r.Id == roleId && r.IsDeleted != true);
173
  if (role == null)
 
175
  return Result.Failure(ResultMessages.RoleNotFound(roleId), 404);
176
  }
177
 
178
+ if (dto.AccessCodes == null)
179
  {
180
+ return Result.Failure("AccessCodes cannot be null", 400);
181
  }
182
 
183
+ var selectedCodes = dto.AccessCodes
184
  .Where(code => !string.IsNullOrWhiteSpace(code))
185
  .Select(code => code.Trim())
186
  .Distinct(StringComparer.OrdinalIgnoreCase)
 
286
  .Select(code => code.Trim())
287
  .ToList();
288
 
289
+ var validAccessCodes = await _db.Menus
290
  .Where(m => !m.IsDeleted)
291
  .Select(m => m.MenuCode)
292
  .ToListAsync();
 
296
  .Select(p => p.PermissionCode)
297
  .ToListAsync();
298
 
299
+ var validCodes = validAccessCodes
300
  .Concat(validPermissionCodes)
301
  .ToHashSet(StringComparer.OrdinalIgnoreCase);
302
 
 
314
  }
315
  }
316
  }
317
+
318
+
TaskTrackingSystem.WebApp/Components/Layout/MainLayout.razor CHANGED
@@ -94,7 +94,7 @@
94
  "projects" => "Projects",
95
  "tasks" => "Tasks",
96
  "reports" => "Reports",
97
- "roles" => "Roles & Permissions",
98
  "users" => "Users",
99
  _ => "Workspace"
100
  };
 
94
  "projects" => "Projects",
95
  "tasks" => "Tasks",
96
  "reports" => "Reports",
97
+ "roles" => "Roles & Access",
98
  "users" => "Users",
99
  _ => "Workspace"
100
  };
TaskTrackingSystem.WebApp/Components/Pages/Roles.razor CHANGED
@@ -8,7 +8,7 @@
8
 
9
 
10
 
11
- <PageTitle>Roles & Menu Permissions</PageTitle>
12
 
13
  <SuccessAlert Message="@successMessage" />
14
 
@@ -16,8 +16,8 @@
16
  <!-- Page Header -->
17
  <div class="flex items-center justify-between">
18
  <div>
19
- <h2 class="text-3xl font-bold tracking-tight text-slate-900">Roles & Permissions</h2>
20
- <p class="text-slate-500 mt-1">Manage roles, assign permissions, and control menu access.</p>
21
  </div>
22
  <button @onclick="OpenCreateModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
23
  <i data-lucide="plus" class="h-4 w-4 mr-2"></i>
@@ -77,11 +77,11 @@
77
  <td class="px-6 py-4 text-sm text-slate-400">@role.CreatedAt.ToString("dd-MMM-yyyy")</td>
78
  <td class="px-6 py-4 text-right">
79
  <div class="flex items-center justify-end space-x-2">
80
- <button @onclick="() => OpenMenuPermissionsModal(role)"
81
  class="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-indigo-700 bg-indigo-50 hover:bg-indigo-100 transition-all"
82
- title="Manage Permissions">
83
  <i data-lucide="shield-check" class="h-3.5 w-3.5"></i>
84
- Permissions
85
  </button>
86
  <button @onclick="() => OpenEditModal(role)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
87
  <i data-lucide="pencil" class="h-4 w-4"></i>
@@ -172,12 +172,12 @@
172
  {
173
  <p class="text-xs text-slate-400 flex items-center gap-1">
174
  <i data-lucide="info" class="h-3.5 w-3.5 shrink-0"></i>
175
- Pick permissions on the right — they will be assigned when you create.
176
  </p>
177
  }
178
  </div>
179
 
180
- <!-- RIGHT: Permissions (Create only) -->
181
  @if (!isEditing)
182
  {
183
  <div class="w-7/12 flex flex-col min-h-0 border-l border-slate-100">
@@ -185,14 +185,14 @@
185
  <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/60 shrink-0 flex items-center justify-between">
186
  <div class="flex items-center gap-2">
187
  <i data-lucide="shield-check" class="h-4 w-4 text-violet-500"></i>
188
- <span class="text-sm font-semibold text-slate-700">Assign Permissions</span>
189
  </div>
190
- @if (formAllMenus.Any())
191
  {
192
  <label class="flex items-center gap-1.5 cursor-pointer group">
193
  <input type="checkbox"
194
- checked="@IsAllFormMenusSelected"
195
- @onchange="ToggleSelectAllFormMenus"
196
  class="h-3.5 w-3.5 rounded accent-violet-600" />
197
  <span class="text-xs text-slate-500 group-hover:text-violet-600 transition-colors">Select All</span>
198
  </label>
@@ -201,7 +201,7 @@
201
 
202
  <!-- Permission items -->
203
  <div class="overflow-y-auto flex-1 px-5 py-4 space-y-4">
204
- @if (isLoadingFormMenus)
205
  {
206
  <div class="flex items-center justify-center py-10">
207
  <svg class="animate-spin h-5 w-5 text-violet-500" viewBox="0 0 24 24">
@@ -210,22 +210,22 @@
210
  </svg>
211
  </div>
212
  }
213
- else if (!formAllMenus.Any())
214
  {
215
- <p class="text-sm text-slate-400 text-center py-8">No menus found in database.</p>
216
  }
217
  else
218
  {
219
- @foreach (var parent in formAllMenus.Where(m => string.IsNullOrEmpty(m.ParentCode) || m.ParentCode == "0").OrderBy(m => m.OrderNo))
220
  {
221
- var children = formAllMenus.Where(c => c.ParentCode == parent.MenuCode).OrderBy(c => c.OrderNo).ToList();
222
- var parentChecked = formSelectedMenuCodes.Contains(parent.MenuCode);
223
 
224
  <div class="mb-3 rounded-xl border border-slate-100 overflow-hidden shadow-sm">
225
  <!-- Parent Row -->
226
  <label class="flex items-center gap-3 px-4 py-2.5 bg-slate-50/80 cursor-pointer hover:bg-slate-100 transition-colors">
227
  <input type="checkbox" checked="@parentChecked"
228
- @onchange="(e) => ToggleMenuSelection(parent, (bool)(e.Value ?? false), isForm: true)"
229
  class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
230
  <div class="flex items-center gap-2 flex-1">
231
  @if (!string.IsNullOrEmpty(parent.Icon))
@@ -242,27 +242,27 @@
242
  <div class="divide-y divide-slate-100 bg-white">
243
  @foreach (var child in children)
244
  {
245
- var childChecked = formSelectedMenuCodes.Contains(child.MenuCode);
246
  <div class="pl-8 pr-4 py-2.5">
247
  <label class="flex items-center gap-3 cursor-pointer hover:text-violet-600 transition-colors mb-1.5">
248
  <input type="checkbox" checked="@childChecked"
249
- @onchange="(e) => ToggleMenuSelection(child, (bool)(e.Value ?? false), isForm: true)"
250
  class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
251
  <span class="text-sm font-medium text-slate-700">@child.MenuName</span>
252
  </label>
253
 
254
- @if (child.Actions.Any())
255
  {
256
  <div class="flex flex-wrap gap-2 pl-7 mt-1">
257
- @foreach (var action in child.Actions)
258
  {
259
- var isActionChecked = formSelectedMenuCodes.Contains(action.MenuDetailCode);
260
  <label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border cursor-pointer select-none transition-all
261
  @(isActionChecked
262
  ? "bg-violet-50 border-violet-200 text-violet-700"
263
  : "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
264
  <input type="checkbox" checked="@isActionChecked"
265
- @onchange="(e) => ToggleActionSelection(action.MenuDetailCode, (bool)(e.Value ?? false), isForm: true)"
266
  class="h-3 w-3 rounded accent-violet-600" />
267
  <span class="text-xs">@action.ActionName</span>
268
  </label>
@@ -273,19 +273,19 @@
273
  }
274
  </div>
275
  }
276
- else if (parent.Actions.Any())
277
  {
278
- <!-- Direct Actions under Parent -->
279
  <div class="px-5 py-2.5 bg-white flex flex-wrap gap-2">
280
- @foreach (var action in parent.Actions)
281
  {
282
- var isActionChecked = formSelectedMenuCodes.Contains(action.MenuDetailCode);
283
  <label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg border cursor-pointer select-none transition-all
284
  @(isActionChecked
285
  ? "bg-violet-50 border-violet-200 text-violet-700 shadow-sm"
286
  : "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
287
  <input type="checkbox" checked="@isActionChecked"
288
- @onchange="(e) => ToggleActionSelection(action.MenuDetailCode, (bool)(e.Value ?? false), isForm: true)"
289
  class="h-3.5 w-3.5 rounded accent-violet-600" />
290
  <span class="text-xs font-medium">@action.ActionName</span>
291
  </label>
@@ -303,9 +303,9 @@
303
  <!-- Footer -->
304
  <div class="flex items-center justify-between px-6 py-4 border-t border-slate-100 bg-slate-50/50 shrink-0">
305
  <span class="text-xs text-slate-400">
306
- @if (!isEditing && formSelectedMenuCodes.Count > 0)
307
  {
308
- <span class="text-violet-600 font-medium">@formSelectedMenuCodes.Count access item(s)</span>
309
  <span> will be assigned</span>
310
  }
311
  </span>
@@ -327,9 +327,9 @@
327
  </div>
328
  }
329
 
330
- @* ===== Assign Permissions Modal ===== *@
331
- @* ===== Role Permissions Modal ===== *@
332
- @if (showMenuModal)
333
  {
334
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
335
  <div class="bg-white rounded-2xl shadow-2xl w-full max-w-2xl mx-4 overflow-hidden flex flex-col" style="max-height: 85vh;">
@@ -341,11 +341,11 @@
341
  <i data-lucide="shield-check" class="h-4 w-4"></i>
342
  </div>
343
  <div>
344
- <h3 class="text-base font-semibold text-slate-900">Role Permissions — <span class="text-violet-700">@selectedRoleName</span></h3>
345
- <p class="text-xs text-slate-500">Configure menu access and action permissions</p>
346
  </div>
347
  </div>
348
- <button @onclick="CloseMenuModal" class="rounded-lg p-1.5 text-slate-400 hover:bg-white hover:text-slate-600 transition-all">
349
  <i data-lucide="x" class="h-5 w-5"></i>
350
  </button>
351
  </div>
@@ -353,16 +353,16 @@
353
  <!-- Select All Bar -->
354
  <div class="px-6 py-3 border-b border-slate-100 bg-slate-50/60 flex items-center justify-between">
355
  <label class="flex items-center gap-2 cursor-pointer">
356
- <input type="checkbox" checked="@IsAllMenusSelected" @onchange="ToggleSelectAllMenus"
357
  class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
358
- <span class="text-sm font-medium text-slate-700">Select All Permissions</span>
359
  </label>
360
- <span class="text-xs text-slate-400">@selectedMenuCodes.Count access item(s) selected</span>
361
  </div>
362
 
363
  <!-- Menu Items Body -->
364
  <div class="overflow-y-auto flex-1 px-6 py-4">
365
- @if (isLoadingMenus)
366
  {
367
  <div class="flex items-center justify-center py-12">
368
  <svg class="animate-spin h-6 w-6 text-violet-500" viewBox="0 0 24 24">
@@ -371,25 +371,25 @@
371
  </svg>
372
  </div>
373
  }
374
- else if (!allMenus.Any())
375
  {
376
  <div class="text-center py-12">
377
  <i data-lucide="shield-check" class="h-10 w-10 mx-auto text-slate-300 mb-3"></i>
378
- <p class="text-sm text-slate-500">No menus found in database.</p>
379
  </div>
380
  }
381
  else
382
  {
383
- @foreach (var parent in allMenus.Where(m => string.IsNullOrEmpty(m.ParentCode) || m.ParentCode == "0").OrderBy(m => m.OrderNo))
384
  {
385
- var children = allMenus.Where(c => c.ParentCode == parent.MenuCode).OrderBy(c => c.OrderNo).ToList();
386
- var parentChecked = selectedMenuCodes.Contains(parent.MenuCode);
387
 
388
  <div class="mb-4 rounded-xl border border-slate-100 overflow-hidden shadow-sm">
389
  <!-- Parent Row -->
390
  <label class="flex items-center gap-3 px-4 py-3 bg-slate-50/80 cursor-pointer hover:bg-slate-100 transition-colors">
391
  <input type="checkbox" checked="@parentChecked"
392
- @onchange="(e) => ToggleMenuSelection(parent, (bool)(e.Value ?? false), isForm: false)"
393
  class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
394
  <div class="flex items-center gap-2 flex-1">
395
  @if (!string.IsNullOrEmpty(parent.Icon))
@@ -406,27 +406,27 @@
406
  <div class="divide-y divide-slate-100 bg-white">
407
  @foreach (var child in children)
408
  {
409
- var childChecked = selectedMenuCodes.Contains(child.MenuCode);
410
  <div class="pl-8 pr-4 py-3">
411
  <label class="flex items-center gap-3 cursor-pointer hover:text-violet-600 transition-colors mb-2">
412
  <input type="checkbox" checked="@childChecked"
413
- @onchange="(e) => ToggleMenuSelection(child, (bool)(e.Value ?? false), isForm: false)"
414
  class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
415
  <span class="text-sm font-medium text-slate-700">@child.MenuName</span>
416
  </label>
417
 
418
- @if (child.Actions.Any())
419
  {
420
  <div class="flex flex-wrap gap-2 pl-7 mt-1.5">
421
- @foreach (var action in child.Actions)
422
  {
423
- var isActionChecked = selectedMenuCodes.Contains(action.MenuDetailCode);
424
  <label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border cursor-pointer select-none transition-all
425
  @(isActionChecked
426
  ? "bg-violet-50 border-violet-200 text-violet-700"
427
  : "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
428
  <input type="checkbox" checked="@isActionChecked"
429
- @onchange="(e) => ToggleActionSelection(action.MenuDetailCode, (bool)(e.Value ?? false), isForm: false)"
430
  class="h-3 w-3 rounded accent-violet-600" />
431
  <span class="text-xs">@action.ActionName</span>
432
  </label>
@@ -437,19 +437,19 @@
437
  }
438
  </div>
439
  }
440
- else if (parent.Actions.Any())
441
  {
442
- <!-- Direct Actions under Parent -->
443
  <div class="px-6 py-3.5 bg-white flex flex-wrap gap-2.5">
444
- @foreach (var action in parent.Actions)
445
  {
446
- var isActionChecked = selectedMenuCodes.Contains(action.MenuDetailCode);
447
  <label class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border cursor-pointer select-none transition-all
448
  @(isActionChecked
449
  ? "bg-violet-50 border-violet-300 text-violet-700 shadow-sm"
450
  : "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
451
  <input type="checkbox" checked="@isActionChecked"
452
- @onchange="(e) => ToggleActionSelection(action.MenuDetailCode, (bool)(e.Value ?? false), isForm: false)"
453
  class="h-3.5 w-3.5 rounded accent-violet-600" />
454
  <span class="text-xs font-medium">@action.ActionName</span>
455
  </label>
@@ -462,19 +462,19 @@
462
  </div>
463
 
464
  <!-- Footer -->
465
- @if (!string.IsNullOrEmpty(menuSaveError))
466
  {
467
  <div class="px-6 pb-2">
468
  <div class="p-3 rounded-lg bg-red-50 border border-red-100">
469
- <p class="text-sm text-red-600">@menuSaveError</p>
470
  </div>
471
  </div>
472
  }
473
- @if (!string.IsNullOrEmpty(menuSaveSuccess))
474
  {
475
  <div class="px-6 pb-2">
476
  <div class="p-3 rounded-lg bg-emerald-50 border border-emerald-100">
477
- <p class="text-sm text-emerald-700">@menuSaveSuccess</p>
478
  </div>
479
  </div>
480
  }
@@ -485,16 +485,16 @@
485
  Changes apply immediately after saving.
486
  </span>
487
  <div class="flex items-center gap-3">
488
- <button @onclick="CloseMenuModal" class="rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">
489
  Cancel
490
  </button>
491
- <button @onclick="SaveMenuPermissions" disabled="@isSavingMenus"
492
  class="inline-flex items-center gap-2 rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 disabled:opacity-50 transition-colors shadow-sm">
493
- <svg class="animate-spin h-4 w-4 @(isSavingMenus ? "" : "hidden")" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
494
- <span class="@(isSavingMenus ? "" : "hidden")">Saving...</span>
495
 
496
- <i data-lucide="save" class="h-4 w-4 @(isSavingMenus ? "hidden" : "")"></i>
497
- <span class="@(isSavingMenus ? "hidden" : "")">Save Permissions</span>
498
  </button>
499
  </div>
500
  </div>
@@ -517,7 +517,7 @@
517
  @code {
518
  private bool isLoading = true;
519
  private bool showRoleModal = false;
520
- private bool showMenuModal = false;
521
  private bool isEditing = false;
522
  private bool isSaving = false;
523
  private string? roleError;
@@ -574,26 +574,26 @@
574
  private string formName = "";
575
  private string? formDescription;
576
 
577
- // Inline permissions in Create form
578
- private List<MenuAdminDto> formAllMenus = new();
579
- private HashSet<string> formSelectedMenuCodes = new();
580
- private bool isLoadingFormMenus = false;
581
 
582
- private bool IsAllFormMenusSelected
583
- => formAllMenus.Count > 0 && formSelectedMenuCodes.Count == (formAllMenus.Count + formAllMenus.Sum(m => m.Actions.Count));
584
 
585
- // Menu permissions modal (for editing existing role)
586
  private long selectedRoleId;
587
  private string selectedRoleName = "";
588
- private List<MenuAdminDto> allMenus = new();
589
- private HashSet<string> selectedMenuCodes = new();
590
- private bool isLoadingMenus = false;
591
- private bool isSavingMenus = false;
592
- private string? menuSaveError;
593
- private string? menuSaveSuccess;
594
 
595
- private bool IsAllMenusSelected
596
- => allMenus.Count > 0 && selectedMenuCodes.Count == (allMenus.Count + allMenus.Sum(m => m.Actions.Count));
597
 
598
  protected override async Task OnInitializedAsync()
599
  {
@@ -645,16 +645,16 @@
645
  roleError = null;
646
  isRoleNameError = false;
647
  roleNameErrorMsg = "";
648
- formSelectedMenuCodes = new(StringComparer.OrdinalIgnoreCase);
649
- formAllMenus = new();
650
- isLoadingFormMenus = true;
651
  showRoleModal = true;
652
  StateHasChanged();
653
 
654
  try
655
  {
656
  var client = ApiClient.CreateClient();
657
- formAllMenus = await client.GetFromJsonAsync<List<MenuAdminDto>>("Menu/all", Serialization.CaseInsensitive) ?? new();
658
  }
659
  catch (Exception ex)
660
  {
@@ -662,7 +662,7 @@
662
  }
663
  finally
664
  {
665
- isLoadingFormMenus = false;
666
  StateHasChanged();
667
  }
668
  }
@@ -685,20 +685,20 @@
685
  roleError = null;
686
  isRoleNameError = false;
687
  roleNameErrorMsg = "";
688
- formAllMenus = new();
689
- formSelectedMenuCodes = new(StringComparer.OrdinalIgnoreCase);
690
  }
691
 
692
- private void ToggleMenuSelection(MenuAdminDto menu, bool isChecked, bool isForm)
693
  {
694
- var targetSet = isForm ? formSelectedMenuCodes : selectedMenuCodes;
695
- var allMenuSource = isForm ? formAllMenus : allMenus;
696
 
697
  if (isChecked)
698
  {
699
  targetSet.Add(menu.MenuCode);
700
  // Select direct actions
701
- foreach (var action in menu.Actions)
702
  {
703
  targetSet.Add(action.MenuDetailCode);
704
  }
@@ -707,7 +707,7 @@
707
  foreach (var child in children)
708
  {
709
  targetSet.Add(child.MenuCode);
710
- foreach (var action in child.Actions)
711
  {
712
  targetSet.Add(action.MenuDetailCode);
713
  }
@@ -717,7 +717,7 @@
717
  {
718
  targetSet.Remove(menu.MenuCode);
719
  // Unselect direct actions
720
- foreach (var action in menu.Actions)
721
  {
722
  targetSet.Remove(action.MenuDetailCode);
723
  }
@@ -726,7 +726,7 @@
726
  foreach (var child in children)
727
  {
728
  targetSet.Remove(child.MenuCode);
729
- foreach (var action in child.Actions)
730
  {
731
  targetSet.Remove(action.MenuDetailCode);
732
  }
@@ -735,9 +735,9 @@
735
  StateHasChanged();
736
  }
737
 
738
- private void ToggleActionSelection(string actionCode, bool isChecked, bool isForm)
739
  {
740
- var targetSet = isForm ? formSelectedMenuCodes : selectedMenuCodes;
741
  if (isChecked)
742
  {
743
  targetSet.Add(actionCode);
@@ -749,24 +749,24 @@
749
  StateHasChanged();
750
  }
751
 
752
- private void ToggleSelectAllFormMenus(ChangeEventArgs e)
753
  {
754
  var selectAll = (bool)(e.Value ?? false);
755
  if (selectAll)
756
  {
757
- formSelectedMenuCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
758
- foreach (var menu in formAllMenus)
759
  {
760
- formSelectedMenuCodes.Add(menu.MenuCode);
761
- foreach (var action in menu.Actions)
762
  {
763
- formSelectedMenuCodes.Add(action.MenuDetailCode);
764
  }
765
  }
766
  }
767
  else
768
  {
769
- formSelectedMenuCodes.Clear();
770
  }
771
  }
772
 
@@ -861,7 +861,7 @@
861
  {
862
  Name = formName,
863
  Description = formDescription,
864
- MenuCodes = formSelectedMenuCodes.ToList()
865
  };
866
  var response = await client.PostAsJsonAsync("Role", dto);
867
  var res = await response.Content.ReadFromJsonAsync<Result<RoleDto>>(Serialization.CaseInsensitive);
@@ -899,95 +899,97 @@
899
  }
900
 
901
  // ===== Menu & Permission Access =====
902
- private async Task OpenMenuPermissionsModal(RoleDto role)
903
  {
904
  selectedRoleId = role.Id;
905
  selectedRoleName = role.Name;
906
- menuSaveError = null;
907
- menuSaveSuccess = null;
908
- isLoadingMenus = true;
909
- showMenuModal = true;
910
  StateHasChanged();
911
 
912
  var client = ApiClient.CreateClient();
913
  try
914
  {
915
- // Load all available menus with their nested actions
916
- allMenus = await client.GetFromJsonAsync<List<MenuAdminDto>>("Menu/all", Serialization.CaseInsensitive) ?? new();
917
 
918
  // Load currently assigned access items for this role
919
  var assignedResult = await client.GetFromJsonAsync<Result<List<string>>>($"Role/{role.Id}/menus", Serialization.CaseInsensitive);
920
- selectedMenuCodes = new HashSet<string>(assignedResult?.Value ?? new(), StringComparer.OrdinalIgnoreCase);
921
  }
922
  catch (Exception ex)
923
  {
924
- Console.WriteLine($"Load menus error: {ex.Message}");
925
- menuSaveError = "Failed to load menus. Please try again.";
926
  }
927
  finally
928
  {
929
- isLoadingMenus = false;
930
  StateHasChanged();
931
  }
932
  }
933
 
934
- private void CloseMenuModal()
935
  {
936
- showMenuModal = false;
937
- allMenus = new();
938
- selectedMenuCodes = new(StringComparer.OrdinalIgnoreCase);
939
- menuSaveError = null;
940
- menuSaveSuccess = null;
941
  }
942
 
943
- private void ToggleSelectAllMenus(ChangeEventArgs e)
944
  {
945
  var selectAll = (bool)(e.Value ?? false);
946
  if (selectAll)
947
  {
948
- selectedMenuCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
949
- foreach (var menu in allMenus)
950
  {
951
- selectedMenuCodes.Add(menu.MenuCode);
952
- foreach (var action in menu.Actions)
953
  {
954
- selectedMenuCodes.Add(action.MenuDetailCode);
955
  }
956
  }
957
  }
958
  else
959
  {
960
- selectedMenuCodes.Clear();
961
  }
962
  }
963
 
964
- private async Task SaveMenuPermissions()
965
  {
966
- isSavingMenus = true;
967
- menuSaveError = null;
968
- menuSaveSuccess = null;
969
  var client = ApiClient.CreateClient();
970
  try
971
  {
972
- var dto = new AssignMenusDto { MenuCodes = selectedMenuCodes.ToList() };
973
  var response = await client.PostAsJsonAsync($"Role/{selectedRoleId}/menus", dto);
974
  if (response.IsSuccessStatusCode)
975
  {
976
- menuSaveSuccess = $"Access for '{selectedRoleName}' saved successfully!";
977
  }
978
  else
979
  {
980
- menuSaveError = "Failed to save access. Please try again.";
981
  }
982
  }
983
  catch (Exception ex)
984
  {
985
- menuSaveError = ex.Message;
986
  }
987
  finally
988
  {
989
- isSavingMenus = false;
990
  StateHasChanged();
991
  }
992
  }
993
  }
 
 
 
8
 
9
 
10
 
11
+ <PageTitle>Roles & Access</PageTitle>
12
 
13
  <SuccessAlert Message="@successMessage" />
14
 
 
16
  <!-- Page Header -->
17
  <div class="flex items-center justify-between">
18
  <div>
19
+ <h2 class="text-3xl font-bold tracking-tight text-slate-900">Roles & Access</h2>
20
+ <p class="text-slate-500 mt-1">Manage roles, assign menu access and permissions, and control visibility.</p>
21
  </div>
22
  <button @onclick="OpenCreateModal" class="inline-flex items-center rounded-lg bg-violet-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-violet-700 transition-colors shadow-sm">
23
  <i data-lucide="plus" class="h-4 w-4 mr-2"></i>
 
77
  <td class="px-6 py-4 text-sm text-slate-400">@role.CreatedAt.ToString("dd-MMM-yyyy")</td>
78
  <td class="px-6 py-4 text-right">
79
  <div class="flex items-center justify-end space-x-2">
80
+ <button @onclick="() => OpenAccessModal(role)"
81
  class="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs font-medium text-indigo-700 bg-indigo-50 hover:bg-indigo-100 transition-all"
82
+ title="Manage Access">
83
  <i data-lucide="shield-check" class="h-3.5 w-3.5"></i>
84
+ Access
85
  </button>
86
  <button @onclick="() => OpenEditModal(role)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="Edit">
87
  <i data-lucide="pencil" class="h-4 w-4"></i>
 
172
  {
173
  <p class="text-xs text-slate-400 flex items-center gap-1">
174
  <i data-lucide="info" class="h-3.5 w-3.5 shrink-0"></i>
175
+ Pick access on the right — it will be assigned when you create.
176
  </p>
177
  }
178
  </div>
179
 
180
+ <!-- RIGHT: Access (Create only) -->
181
  @if (!isEditing)
182
  {
183
  <div class="w-7/12 flex flex-col min-h-0 border-l border-slate-100">
 
185
  <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/60 shrink-0 flex items-center justify-between">
186
  <div class="flex items-center gap-2">
187
  <i data-lucide="shield-check" class="h-4 w-4 text-violet-500"></i>
188
+ <span class="text-sm font-semibold text-slate-700">Assign Access</span>
189
  </div>
190
+ @if (formAccessItems.Any())
191
  {
192
  <label class="flex items-center gap-1.5 cursor-pointer group">
193
  <input type="checkbox"
194
+ checked="@IsAllFormAccessSelected"
195
+ @onchange="ToggleSelectAllFormAccess"
196
  class="h-3.5 w-3.5 rounded accent-violet-600" />
197
  <span class="text-xs text-slate-500 group-hover:text-violet-600 transition-colors">Select All</span>
198
  </label>
 
201
 
202
  <!-- Permission items -->
203
  <div class="overflow-y-auto flex-1 px-5 py-4 space-y-4">
204
+ @if (isLoadingFormAccessItems)
205
  {
206
  <div class="flex items-center justify-center py-10">
207
  <svg class="animate-spin h-5 w-5 text-violet-500" viewBox="0 0 24 24">
 
210
  </svg>
211
  </div>
212
  }
213
+ else if (!formAccessItems.Any())
214
  {
215
+ <p class="text-sm text-slate-400 text-center py-8">No access items found in database.</p>
216
  }
217
  else
218
  {
219
+ @foreach (var parent in formAccessItems.Where(m => string.IsNullOrEmpty(m.ParentCode) || m.ParentCode == "0").OrderBy(m => m.OrderNo))
220
  {
221
+ var children = formAccessItems.Where(c => c.ParentCode == parent.MenuCode).OrderBy(c => c.OrderNo).ToList();
222
+ var parentChecked = formSelectedAccessCodes.Contains(parent.MenuCode);
223
 
224
  <div class="mb-3 rounded-xl border border-slate-100 overflow-hidden shadow-sm">
225
  <!-- Parent Row -->
226
  <label class="flex items-center gap-3 px-4 py-2.5 bg-slate-50/80 cursor-pointer hover:bg-slate-100 transition-colors">
227
  <input type="checkbox" checked="@parentChecked"
228
+ @onchange="(e) => ToggleAccessSelection(parent, (bool)(e.Value ?? false), isForm: true)"
229
  class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
230
  <div class="flex items-center gap-2 flex-1">
231
  @if (!string.IsNullOrEmpty(parent.Icon))
 
242
  <div class="divide-y divide-slate-100 bg-white">
243
  @foreach (var child in children)
244
  {
245
+ var childChecked = formSelectedAccessCodes.Contains(child.MenuCode);
246
  <div class="pl-8 pr-4 py-2.5">
247
  <label class="flex items-center gap-3 cursor-pointer hover:text-violet-600 transition-colors mb-1.5">
248
  <input type="checkbox" checked="@childChecked"
249
+ @onchange="(e) => ToggleAccessSelection(child, (bool)(e.Value ?? false), isForm: true)"
250
  class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
251
  <span class="text-sm font-medium text-slate-700">@child.MenuName</span>
252
  </label>
253
 
254
+ @if (child.Permissions.Any())
255
  {
256
  <div class="flex flex-wrap gap-2 pl-7 mt-1">
257
+ @foreach (var action in child.Permissions)
258
  {
259
+ var isActionChecked = formSelectedAccessCodes.Contains(action.MenuDetailCode);
260
  <label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border cursor-pointer select-none transition-all
261
  @(isActionChecked
262
  ? "bg-violet-50 border-violet-200 text-violet-700"
263
  : "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
264
  <input type="checkbox" checked="@isActionChecked"
265
+ @onchange="(e) => TogglePermissionSelection(action.MenuDetailCode, (bool)(e.Value ?? false), isForm: true)"
266
  class="h-3 w-3 rounded accent-violet-600" />
267
  <span class="text-xs">@action.ActionName</span>
268
  </label>
 
273
  }
274
  </div>
275
  }
276
+ else if (parent.Permissions.Any())
277
  {
278
+ <!-- Direct Access under Parent -->
279
  <div class="px-5 py-2.5 bg-white flex flex-wrap gap-2">
280
+ @foreach (var action in parent.Permissions)
281
  {
282
+ var isActionChecked = formSelectedAccessCodes.Contains(action.MenuDetailCode);
283
  <label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg border cursor-pointer select-none transition-all
284
  @(isActionChecked
285
  ? "bg-violet-50 border-violet-200 text-violet-700 shadow-sm"
286
  : "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
287
  <input type="checkbox" checked="@isActionChecked"
288
+ @onchange="(e) => TogglePermissionSelection(action.MenuDetailCode, (bool)(e.Value ?? false), isForm: true)"
289
  class="h-3.5 w-3.5 rounded accent-violet-600" />
290
  <span class="text-xs font-medium">@action.ActionName</span>
291
  </label>
 
303
  <!-- Footer -->
304
  <div class="flex items-center justify-between px-6 py-4 border-t border-slate-100 bg-slate-50/50 shrink-0">
305
  <span class="text-xs text-slate-400">
306
+ @if (!isEditing && formSelectedAccessCodes.Count > 0)
307
  {
308
+ <span class="text-violet-600 font-medium">@formSelectedAccessCodes.Count access item(s)</span>
309
  <span> will be assigned</span>
310
  }
311
  </span>
 
327
  </div>
328
  }
329
 
330
+ @* ===== Assign Access Modal ===== *@
331
+ @* ===== Role Access Modal ===== *@
332
+ @if (showAccessModal)
333
  {
334
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
335
  <div class="bg-white rounded-2xl shadow-2xl w-full max-w-2xl mx-4 overflow-hidden flex flex-col" style="max-height: 85vh;">
 
341
  <i data-lucide="shield-check" class="h-4 w-4"></i>
342
  </div>
343
  <div>
344
+ <h3 class="text-base font-semibold text-slate-900">Role Access — <span class="text-violet-700">@selectedRoleName</span></h3>
345
+ <p class="text-xs text-slate-500">Configure menu access and permissions</p>
346
  </div>
347
  </div>
348
+ <button @onclick="CloseAccessModal" class="rounded-lg p-1.5 text-slate-400 hover:bg-white hover:text-slate-600 transition-all">
349
  <i data-lucide="x" class="h-5 w-5"></i>
350
  </button>
351
  </div>
 
353
  <!-- Select All Bar -->
354
  <div class="px-6 py-3 border-b border-slate-100 bg-slate-50/60 flex items-center justify-between">
355
  <label class="flex items-center gap-2 cursor-pointer">
356
+ <input type="checkbox" checked="@IsAllAccessSelected" @onchange="ToggleSelectAllAccess"
357
  class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
358
+ <span class="text-sm font-medium text-slate-700">Select All Access</span>
359
  </label>
360
+ <span class="text-xs text-slate-400">@selectedAccessCodes.Count access item(s) selected</span>
361
  </div>
362
 
363
  <!-- Menu Items Body -->
364
  <div class="overflow-y-auto flex-1 px-6 py-4">
365
+ @if (isLoadingAccessItems)
366
  {
367
  <div class="flex items-center justify-center py-12">
368
  <svg class="animate-spin h-6 w-6 text-violet-500" viewBox="0 0 24 24">
 
371
  </svg>
372
  </div>
373
  }
374
+ else if (!accessItems.Any())
375
  {
376
  <div class="text-center py-12">
377
  <i data-lucide="shield-check" class="h-10 w-10 mx-auto text-slate-300 mb-3"></i>
378
+ <p class="text-sm text-slate-500">No access items found in database.</p>
379
  </div>
380
  }
381
  else
382
  {
383
+ @foreach (var parent in accessItems.Where(m => string.IsNullOrEmpty(m.ParentCode) || m.ParentCode == "0").OrderBy(m => m.OrderNo))
384
  {
385
+ var children = accessItems.Where(c => c.ParentCode == parent.MenuCode).OrderBy(c => c.OrderNo).ToList();
386
+ var parentChecked = selectedAccessCodes.Contains(parent.MenuCode);
387
 
388
  <div class="mb-4 rounded-xl border border-slate-100 overflow-hidden shadow-sm">
389
  <!-- Parent Row -->
390
  <label class="flex items-center gap-3 px-4 py-3 bg-slate-50/80 cursor-pointer hover:bg-slate-100 transition-colors">
391
  <input type="checkbox" checked="@parentChecked"
392
+ @onchange="(e) => ToggleAccessSelection(parent, (bool)(e.Value ?? false), isForm: false)"
393
  class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
394
  <div class="flex items-center gap-2 flex-1">
395
  @if (!string.IsNullOrEmpty(parent.Icon))
 
406
  <div class="divide-y divide-slate-100 bg-white">
407
  @foreach (var child in children)
408
  {
409
+ var childChecked = selectedAccessCodes.Contains(child.MenuCode);
410
  <div class="pl-8 pr-4 py-3">
411
  <label class="flex items-center gap-3 cursor-pointer hover:text-violet-600 transition-colors mb-2">
412
  <input type="checkbox" checked="@childChecked"
413
+ @onchange="(e) => ToggleAccessSelection(child, (bool)(e.Value ?? false), isForm: false)"
414
  class="h-4 w-4 rounded border-slate-300 accent-violet-600" />
415
  <span class="text-sm font-medium text-slate-700">@child.MenuName</span>
416
  </label>
417
 
418
+ @if (child.Permissions.Any())
419
  {
420
  <div class="flex flex-wrap gap-2 pl-7 mt-1.5">
421
+ @foreach (var action in child.Permissions)
422
  {
423
+ var isActionChecked = selectedAccessCodes.Contains(action.MenuDetailCode);
424
  <label class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md border cursor-pointer select-none transition-all
425
  @(isActionChecked
426
  ? "bg-violet-50 border-violet-200 text-violet-700"
427
  : "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
428
  <input type="checkbox" checked="@isActionChecked"
429
+ @onchange="(e) => TogglePermissionSelection(action.MenuDetailCode, (bool)(e.Value ?? false), isForm: false)"
430
  class="h-3 w-3 rounded accent-violet-600" />
431
  <span class="text-xs">@action.ActionName</span>
432
  </label>
 
437
  }
438
  </div>
439
  }
440
+ else if (parent.Permissions.Any())
441
  {
442
+ <!-- Direct Access under Parent -->
443
  <div class="px-6 py-3.5 bg-white flex flex-wrap gap-2.5">
444
+ @foreach (var action in parent.Permissions)
445
  {
446
+ var isActionChecked = selectedAccessCodes.Contains(action.MenuDetailCode);
447
  <label class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border cursor-pointer select-none transition-all
448
  @(isActionChecked
449
  ? "bg-violet-50 border-violet-300 text-violet-700 shadow-sm"
450
  : "bg-white border-slate-200 text-slate-500 hover:border-violet-200 hover:text-violet-500")">
451
  <input type="checkbox" checked="@isActionChecked"
452
+ @onchange="(e) => TogglePermissionSelection(action.MenuDetailCode, (bool)(e.Value ?? false), isForm: false)"
453
  class="h-3.5 w-3.5 rounded accent-violet-600" />
454
  <span class="text-xs font-medium">@action.ActionName</span>
455
  </label>
 
462
  </div>
463
 
464
  <!-- Footer -->
465
+ @if (!string.IsNullOrEmpty(accessSaveError))
466
  {
467
  <div class="px-6 pb-2">
468
  <div class="p-3 rounded-lg bg-red-50 border border-red-100">
469
+ <p class="text-sm text-red-600">@accessSaveError</p>
470
  </div>
471
  </div>
472
  }
473
+ @if (!string.IsNullOrEmpty(accessSaveSuccess))
474
  {
475
  <div class="px-6 pb-2">
476
  <div class="p-3 rounded-lg bg-emerald-50 border border-emerald-100">
477
+ <p class="text-sm text-emerald-700">@accessSaveSuccess</p>
478
  </div>
479
  </div>
480
  }
 
485
  Changes apply immediately after saving.
486
  </span>
487
  <div class="flex items-center gap-3">
488
+ <button @onclick="CloseAccessModal" class="rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">
489
  Cancel
490
  </button>
491
+ <button @onclick="SaveAccess" disabled="@isSavingAccess"
492
  class="inline-flex items-center gap-2 rounded-lg bg-violet-600 px-5 py-2 text-sm font-medium text-white hover:bg-violet-700 disabled:opacity-50 transition-colors shadow-sm">
493
+ <svg class="animate-spin h-4 w-4 @(isSavingAccess ? "" : "hidden")" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>
494
+ <span class="@(isSavingAccess ? "" : "hidden")">Saving...</span>
495
 
496
+ <i data-lucide="save" class="h-4 w-4 @(isSavingAccess ? "hidden" : "")"></i>
497
+ <span class="@(isSavingAccess ? "hidden" : "")">Save Access</span>
498
  </button>
499
  </div>
500
  </div>
 
517
  @code {
518
  private bool isLoading = true;
519
  private bool showRoleModal = false;
520
+ private bool showAccessModal = false;
521
  private bool isEditing = false;
522
  private bool isSaving = false;
523
  private string? roleError;
 
574
  private string formName = "";
575
  private string? formDescription;
576
 
577
+ // Inline access items in Create form
578
+ private List<AccessMenuDto> formAccessItems = new();
579
+ private HashSet<string> formSelectedAccessCodes = new();
580
+ private bool isLoadingFormAccessItems = false;
581
 
582
+ private bool IsAllFormAccessSelected
583
+ => formAccessItems.Count > 0 && formSelectedAccessCodes.Count == (formAccessItems.Count + formAccessItems.Sum(m => m.Permissions.Count));
584
 
585
+ // Access modal (for editing existing role)
586
  private long selectedRoleId;
587
  private string selectedRoleName = "";
588
+ private List<AccessMenuDto> accessItems = new();
589
+ private HashSet<string> selectedAccessCodes = new();
590
+ private bool isLoadingAccessItems = false;
591
+ private bool isSavingAccess = false;
592
+ private string? accessSaveError;
593
+ private string? accessSaveSuccess;
594
 
595
+ private bool IsAllAccessSelected
596
+ => accessItems.Count > 0 && selectedAccessCodes.Count == (accessItems.Count + accessItems.Sum(m => m.Permissions.Count));
597
 
598
  protected override async Task OnInitializedAsync()
599
  {
 
645
  roleError = null;
646
  isRoleNameError = false;
647
  roleNameErrorMsg = "";
648
+ formSelectedAccessCodes = new(StringComparer.OrdinalIgnoreCase);
649
+ formAccessItems = new();
650
+ isLoadingFormAccessItems = true;
651
  showRoleModal = true;
652
  StateHasChanged();
653
 
654
  try
655
  {
656
  var client = ApiClient.CreateClient();
657
+ formAccessItems = await client.GetFromJsonAsync<List<AccessMenuDto>>("Menu/all", Serialization.CaseInsensitive) ?? new();
658
  }
659
  catch (Exception ex)
660
  {
 
662
  }
663
  finally
664
  {
665
+ isLoadingFormAccessItems = false;
666
  StateHasChanged();
667
  }
668
  }
 
685
  roleError = null;
686
  isRoleNameError = false;
687
  roleNameErrorMsg = "";
688
+ formAccessItems = new();
689
+ formSelectedAccessCodes = new(StringComparer.OrdinalIgnoreCase);
690
  }
691
 
692
+ private void ToggleAccessSelection(AccessMenuDto menu, bool isChecked, bool isForm)
693
  {
694
+ var targetSet = isForm ? formSelectedAccessCodes : selectedAccessCodes;
695
+ var allMenuSource = isForm ? formAccessItems : accessItems;
696
 
697
  if (isChecked)
698
  {
699
  targetSet.Add(menu.MenuCode);
700
  // Select direct actions
701
+ foreach (var action in menu.Permissions)
702
  {
703
  targetSet.Add(action.MenuDetailCode);
704
  }
 
707
  foreach (var child in children)
708
  {
709
  targetSet.Add(child.MenuCode);
710
+ foreach (var action in child.Permissions)
711
  {
712
  targetSet.Add(action.MenuDetailCode);
713
  }
 
717
  {
718
  targetSet.Remove(menu.MenuCode);
719
  // Unselect direct actions
720
+ foreach (var action in menu.Permissions)
721
  {
722
  targetSet.Remove(action.MenuDetailCode);
723
  }
 
726
  foreach (var child in children)
727
  {
728
  targetSet.Remove(child.MenuCode);
729
+ foreach (var action in child.Permissions)
730
  {
731
  targetSet.Remove(action.MenuDetailCode);
732
  }
 
735
  StateHasChanged();
736
  }
737
 
738
+ private void TogglePermissionSelection(string actionCode, bool isChecked, bool isForm)
739
  {
740
+ var targetSet = isForm ? formSelectedAccessCodes : selectedAccessCodes;
741
  if (isChecked)
742
  {
743
  targetSet.Add(actionCode);
 
749
  StateHasChanged();
750
  }
751
 
752
+ private void ToggleSelectAllFormAccess(ChangeEventArgs e)
753
  {
754
  var selectAll = (bool)(e.Value ?? false);
755
  if (selectAll)
756
  {
757
+ formSelectedAccessCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
758
+ foreach (var menu in formAccessItems)
759
  {
760
+ formSelectedAccessCodes.Add(menu.MenuCode);
761
+ foreach (var action in menu.Permissions)
762
  {
763
+ formSelectedAccessCodes.Add(action.MenuDetailCode);
764
  }
765
  }
766
  }
767
  else
768
  {
769
+ formSelectedAccessCodes.Clear();
770
  }
771
  }
772
 
 
861
  {
862
  Name = formName,
863
  Description = formDescription,
864
+ AccessCodes = formSelectedAccessCodes.ToList()
865
  };
866
  var response = await client.PostAsJsonAsync("Role", dto);
867
  var res = await response.Content.ReadFromJsonAsync<Result<RoleDto>>(Serialization.CaseInsensitive);
 
899
  }
900
 
901
  // ===== Menu & Permission Access =====
902
+ private async Task OpenAccessModal(RoleDto role)
903
  {
904
  selectedRoleId = role.Id;
905
  selectedRoleName = role.Name;
906
+ accessSaveError = null;
907
+ accessSaveSuccess = null;
908
+ isLoadingAccessItems = true;
909
+ showAccessModal = true;
910
  StateHasChanged();
911
 
912
  var client = ApiClient.CreateClient();
913
  try
914
  {
915
+ // Load all available access items with their nested permissions
916
+ accessItems = await client.GetFromJsonAsync<List<AccessMenuDto>>("Menu/all", Serialization.CaseInsensitive) ?? new();
917
 
918
  // Load currently assigned access items for this role
919
  var assignedResult = await client.GetFromJsonAsync<Result<List<string>>>($"Role/{role.Id}/menus", Serialization.CaseInsensitive);
920
+ selectedAccessCodes = new HashSet<string>(assignedResult?.Value ?? new(), StringComparer.OrdinalIgnoreCase);
921
  }
922
  catch (Exception ex)
923
  {
924
+ Console.WriteLine($"Load access items error: {ex.Message}");
925
+ accessSaveError = "Failed to load access items. Please try again.";
926
  }
927
  finally
928
  {
929
+ isLoadingAccessItems = false;
930
  StateHasChanged();
931
  }
932
  }
933
 
934
+ private void CloseAccessModal()
935
  {
936
+ showAccessModal = false;
937
+ accessItems = new();
938
+ selectedAccessCodes = new(StringComparer.OrdinalIgnoreCase);
939
+ accessSaveError = null;
940
+ accessSaveSuccess = null;
941
  }
942
 
943
+ private void ToggleSelectAllAccess(ChangeEventArgs e)
944
  {
945
  var selectAll = (bool)(e.Value ?? false);
946
  if (selectAll)
947
  {
948
+ selectedAccessCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
949
+ foreach (var menu in accessItems)
950
  {
951
+ selectedAccessCodes.Add(menu.MenuCode);
952
+ foreach (var action in menu.Permissions)
953
  {
954
+ selectedAccessCodes.Add(action.MenuDetailCode);
955
  }
956
  }
957
  }
958
  else
959
  {
960
+ selectedAccessCodes.Clear();
961
  }
962
  }
963
 
964
+ private async Task SaveAccess()
965
  {
966
+ isSavingAccess = true;
967
+ accessSaveError = null;
968
+ accessSaveSuccess = null;
969
  var client = ApiClient.CreateClient();
970
  try
971
  {
972
+ var dto = new AssignAccessDto { AccessCodes = selectedAccessCodes.ToList() };
973
  var response = await client.PostAsJsonAsync($"Role/{selectedRoleId}/menus", dto);
974
  if (response.IsSuccessStatusCode)
975
  {
976
+ accessSaveSuccess = $"Access for '{selectedRoleName}' saved successfully!";
977
  }
978
  else
979
  {
980
+ accessSaveError = "Failed to save access. Please try again.";
981
  }
982
  }
983
  catch (Exception ex)
984
  {
985
+ accessSaveError = ex.Message;
986
  }
987
  finally
988
  {
989
+ isSavingAccess = false;
990
  StateHasChanged();
991
  }
992
  }
993
  }
994
+
995
+
TaskTrackingSystem.WebApp/MenuAuthorizationService.cs CHANGED
@@ -146,7 +146,7 @@ public class MenuAuthorizationService
146
 
147
  if (serverVersion != _sessionState.CachedMenuVersion)
148
  {
149
- // Permissions have changed — bust cache and reload full menus.
150
  Console.WriteLine($"[MenuAuth] Access version changed ({_sessionState.CachedMenuVersion} -> {serverVersion}). Reloading menus.");
151
  _sessionState.ClearMenuCache();
152
  return await LoadMenusAsync(user, roleKey);
 
146
 
147
  if (serverVersion != _sessionState.CachedMenuVersion)
148
  {
149
+ // Access items have changed — bust cache and reload the menu tree.
150
  Console.WriteLine($"[MenuAuth] Access version changed ({_sessionState.CachedMenuVersion} -> {serverVersion}). Reloading menus.");
151
  _sessionState.ClearMenuCache();
152
  return await LoadMenusAsync(user, roleKey);