Spaces:
Running
Running
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} +3 -2
- TaskTrackingSystem.Shared/Models/Menu/{MenuAdminDetailDto.cs → AccessPermissionDto.cs} +2 -1
- TaskTrackingSystem.Shared/Models/Role/{AssignMenusDto.cs → AssignAccessDto.cs} +3 -2
- TaskTrackingSystem.Shared/Models/Role/CreateRoleDto.cs +2 -1
- TaskTrackingSystem.WebApi/Features/Menu/MenuController.cs +4 -2
- TaskTrackingSystem.WebApi/Features/Menu/MenuService.cs +19 -3
- TaskTrackingSystem.WebApi/Features/Role/RoleController.cs +4 -2
- TaskTrackingSystem.WebApi/Features/Role/RoleService.cs +11 -9
- TaskTrackingSystem.WebApp/Components/Layout/MainLayout.razor +1 -1
- TaskTrackingSystem.WebApp/Components/Pages/Roles.razor +144 -142
- TaskTrackingSystem.WebApp/MenuAuthorizationService.cs +1 -1
TaskTrackingSystem.Shared/Models/Menu/{MenuAdminDto.cs → AccessMenuDto.cs}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
namespace TaskTrackingSystem.Shared.Models.Menu
|
| 2 |
{
|
| 3 |
-
public class
|
| 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<
|
| 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
|
| 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
|
| 7 |
{
|
| 8 |
[Required]
|
| 9 |
/// <summary>
|
| 10 |
/// Menu codes and permission codes selected for the role.
|
| 11 |
/// </summary>
|
| 12 |
-
public 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>
|
| 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<
|
| 46 |
{
|
| 47 |
-
var menus = await _menuService.
|
| 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<
|
| 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
|
| 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.
|
| 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>
|
| 75 |
{
|
| 76 |
-
var result = await _roleService.
|
| 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.
|
| 67 |
{
|
| 68 |
-
var validCodesResult = await ValidatePermissionCodesAsync(dto.
|
| 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
|
| 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>
|
| 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.
|
| 179 |
{
|
| 180 |
-
return Result.Failure("
|
| 181 |
}
|
| 182 |
|
| 183 |
-
var selectedCodes = dto.
|
| 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
|
| 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 =
|
| 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 &
|
| 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 &
|
| 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 &
|
| 20 |
-
<p class="text-slate-500 mt-1">Manage roles, assign permissions, and control
|
| 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="() =>
|
| 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
|
| 83 |
<i data-lucide="shield-check" class="h-3.5 w-3.5"></i>
|
| 84 |
-
|
| 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
|
| 176 |
</p>
|
| 177 |
}
|
| 178 |
</div>
|
| 179 |
|
| 180 |
-
<!-- RIGHT:
|
| 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
|
| 189 |
</div>
|
| 190 |
-
@if (
|
| 191 |
{
|
| 192 |
<label class="flex items-center gap-1.5 cursor-pointer group">
|
| 193 |
<input type="checkbox"
|
| 194 |
-
checked="@
|
| 195 |
-
@onchange="
|
| 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 (
|
| 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 (!
|
| 214 |
{
|
| 215 |
-
<p class="text-sm text-slate-400 text-center py-8">No
|
| 216 |
}
|
| 217 |
else
|
| 218 |
{
|
| 219 |
-
@foreach (var parent in
|
| 220 |
{
|
| 221 |
-
var children =
|
| 222 |
-
var parentChecked =
|
| 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) =>
|
| 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 =
|
| 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) =>
|
| 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.
|
| 255 |
{
|
| 256 |
<div class="flex flex-wrap gap-2 pl-7 mt-1">
|
| 257 |
-
@foreach (var action in child.
|
| 258 |
{
|
| 259 |
-
var isActionChecked =
|
| 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) =>
|
| 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.
|
| 277 |
{
|
| 278 |
-
|
| 279 |
<div class="px-5 py-2.5 bg-white flex flex-wrap gap-2">
|
| 280 |
-
@foreach (var action in parent.
|
| 281 |
{
|
| 282 |
-
var isActionChecked =
|
| 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) =>
|
| 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 &&
|
| 307 |
{
|
| 308 |
-
<span class="text-violet-600 font-medium">@
|
| 309 |
<span> will be assigned</span>
|
| 310 |
}
|
| 311 |
</span>
|
|
@@ -327,9 +327,9 @@
|
|
| 327 |
</div>
|
| 328 |
}
|
| 329 |
|
| 330 |
-
@* ===== Assign
|
| 331 |
-
@* ===== Role
|
| 332 |
-
@if (
|
| 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
|
| 345 |
-
<p class="text-xs text-slate-500">Configure menu access and
|
| 346 |
</div>
|
| 347 |
</div>
|
| 348 |
-
<button @onclick="
|
| 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="@
|
| 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
|
| 359 |
</label>
|
| 360 |
-
<span class="text-xs text-slate-400">@
|
| 361 |
</div>
|
| 362 |
|
| 363 |
<!-- Menu Items Body -->
|
| 364 |
<div class="overflow-y-auto flex-1 px-6 py-4">
|
| 365 |
-
@if (
|
| 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 (!
|
| 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
|
| 379 |
</div>
|
| 380 |
}
|
| 381 |
else
|
| 382 |
{
|
| 383 |
-
@foreach (var parent in
|
| 384 |
{
|
| 385 |
-
var children =
|
| 386 |
-
var parentChecked =
|
| 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) =>
|
| 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 =
|
| 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) =>
|
| 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.
|
| 419 |
{
|
| 420 |
<div class="flex flex-wrap gap-2 pl-7 mt-1.5">
|
| 421 |
-
@foreach (var action in child.
|
| 422 |
{
|
| 423 |
-
var isActionChecked =
|
| 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) =>
|
| 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.
|
| 441 |
{
|
| 442 |
-
|
| 443 |
<div class="px-6 py-3.5 bg-white flex flex-wrap gap-2.5">
|
| 444 |
-
@foreach (var action in parent.
|
| 445 |
{
|
| 446 |
-
var isActionChecked =
|
| 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) =>
|
| 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(
|
| 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">@
|
| 470 |
</div>
|
| 471 |
</div>
|
| 472 |
}
|
| 473 |
-
@if (!string.IsNullOrEmpty(
|
| 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">@
|
| 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="
|
| 489 |
Cancel
|
| 490 |
</button>
|
| 491 |
-
<button @onclick="
|
| 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 @(
|
| 494 |
-
<span class="@(
|
| 495 |
|
| 496 |
-
<i data-lucide="save" class="h-4 w-4 @(
|
| 497 |
-
<span class="@(
|
| 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
|
| 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
|
| 578 |
-
private List<
|
| 579 |
-
private HashSet<string>
|
| 580 |
-
private bool
|
| 581 |
|
| 582 |
-
private bool
|
| 583 |
-
=>
|
| 584 |
|
| 585 |
-
//
|
| 586 |
private long selectedRoleId;
|
| 587 |
private string selectedRoleName = "";
|
| 588 |
-
private List<
|
| 589 |
-
private HashSet<string>
|
| 590 |
-
private bool
|
| 591 |
-
private bool
|
| 592 |
-
private string?
|
| 593 |
-
private string?
|
| 594 |
|
| 595 |
-
private bool
|
| 596 |
-
=>
|
| 597 |
|
| 598 |
protected override async Task OnInitializedAsync()
|
| 599 |
{
|
|
@@ -645,16 +645,16 @@
|
|
| 645 |
roleError = null;
|
| 646 |
isRoleNameError = false;
|
| 647 |
roleNameErrorMsg = "";
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
showRoleModal = true;
|
| 652 |
StateHasChanged();
|
| 653 |
|
| 654 |
try
|
| 655 |
{
|
| 656 |
var client = ApiClient.CreateClient();
|
| 657 |
-
|
| 658 |
}
|
| 659 |
catch (Exception ex)
|
| 660 |
{
|
|
@@ -662,7 +662,7 @@
|
|
| 662 |
}
|
| 663 |
finally
|
| 664 |
{
|
| 665 |
-
|
| 666 |
StateHasChanged();
|
| 667 |
}
|
| 668 |
}
|
|
@@ -685,20 +685,20 @@
|
|
| 685 |
roleError = null;
|
| 686 |
isRoleNameError = false;
|
| 687 |
roleNameErrorMsg = "";
|
| 688 |
-
|
| 689 |
-
|
| 690 |
}
|
| 691 |
|
| 692 |
-
private void
|
| 693 |
{
|
| 694 |
-
var targetSet = isForm ?
|
| 695 |
-
var allMenuSource = isForm ?
|
| 696 |
|
| 697 |
if (isChecked)
|
| 698 |
{
|
| 699 |
targetSet.Add(menu.MenuCode);
|
| 700 |
// Select direct actions
|
| 701 |
-
foreach (var action in menu.
|
| 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.
|
| 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.
|
| 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.
|
| 730 |
{
|
| 731 |
targetSet.Remove(action.MenuDetailCode);
|
| 732 |
}
|
|
@@ -735,9 +735,9 @@
|
|
| 735 |
StateHasChanged();
|
| 736 |
}
|
| 737 |
|
| 738 |
-
private void
|
| 739 |
{
|
| 740 |
-
var targetSet = isForm ?
|
| 741 |
if (isChecked)
|
| 742 |
{
|
| 743 |
targetSet.Add(actionCode);
|
|
@@ -749,24 +749,24 @@
|
|
| 749 |
StateHasChanged();
|
| 750 |
}
|
| 751 |
|
| 752 |
-
private void
|
| 753 |
{
|
| 754 |
var selectAll = (bool)(e.Value ?? false);
|
| 755 |
if (selectAll)
|
| 756 |
{
|
| 757 |
-
|
| 758 |
-
foreach (var menu in
|
| 759 |
{
|
| 760 |
-
|
| 761 |
-
foreach (var action in menu.
|
| 762 |
{
|
| 763 |
-
|
| 764 |
}
|
| 765 |
}
|
| 766 |
}
|
| 767 |
else
|
| 768 |
{
|
| 769 |
-
|
| 770 |
}
|
| 771 |
}
|
| 772 |
|
|
@@ -861,7 +861,7 @@
|
|
| 861 |
{
|
| 862 |
Name = formName,
|
| 863 |
Description = formDescription,
|
| 864 |
-
|
| 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
|
| 903 |
{
|
| 904 |
selectedRoleId = role.Id;
|
| 905 |
selectedRoleName = role.Name;
|
| 906 |
-
|
| 907 |
-
|
| 908 |
-
|
| 909 |
-
|
| 910 |
StateHasChanged();
|
| 911 |
|
| 912 |
var client = ApiClient.CreateClient();
|
| 913 |
try
|
| 914 |
{
|
| 915 |
-
// Load all available
|
| 916 |
-
|
| 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 |
-
|
| 921 |
}
|
| 922 |
catch (Exception ex)
|
| 923 |
{
|
| 924 |
-
Console.WriteLine($"Load
|
| 925 |
-
|
| 926 |
}
|
| 927 |
finally
|
| 928 |
{
|
| 929 |
-
|
| 930 |
StateHasChanged();
|
| 931 |
}
|
| 932 |
}
|
| 933 |
|
| 934 |
-
private void
|
| 935 |
{
|
| 936 |
-
|
| 937 |
-
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
}
|
| 942 |
|
| 943 |
-
private void
|
| 944 |
{
|
| 945 |
var selectAll = (bool)(e.Value ?? false);
|
| 946 |
if (selectAll)
|
| 947 |
{
|
| 948 |
-
|
| 949 |
-
foreach (var menu in
|
| 950 |
{
|
| 951 |
-
|
| 952 |
-
foreach (var action in menu.
|
| 953 |
{
|
| 954 |
-
|
| 955 |
}
|
| 956 |
}
|
| 957 |
}
|
| 958 |
else
|
| 959 |
{
|
| 960 |
-
|
| 961 |
}
|
| 962 |
}
|
| 963 |
|
| 964 |
-
private async Task
|
| 965 |
{
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
var client = ApiClient.CreateClient();
|
| 970 |
try
|
| 971 |
{
|
| 972 |
-
var dto = new
|
| 973 |
var response = await client.PostAsJsonAsync($"Role/{selectedRoleId}/menus", dto);
|
| 974 |
if (response.IsSuccessStatusCode)
|
| 975 |
{
|
| 976 |
-
|
| 977 |
}
|
| 978 |
else
|
| 979 |
{
|
| 980 |
-
|
| 981 |
}
|
| 982 |
}
|
| 983 |
catch (Exception ex)
|
| 984 |
{
|
| 985 |
-
|
| 986 |
}
|
| 987 |
finally
|
| 988 |
{
|
| 989 |
-
|
| 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 |
-
//
|
| 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);
|