@page "/roles" @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using TaskTrackingSystem.Shared.Models.Menu @using TaskTrackingSystem.Shared.Models.User @rendermode @(new InteractiveServerRenderMode(prerender: false)) @attribute [Microsoft.AspNetCore.Authorization.Authorize] @inject ApiClientService ApiClient @inject IJSRuntime JS @inject AuthenticationStateProvider AuthStateProvider @inject MenuAuthorizationService MenuAuthorization Roles & Access

Roles & Access

Manage roles, assign access and permissions, and control visibility.

@if (canCreateRole) { }
@if (isLoading) { } else {
@if (PagedRoles.Any()) { @foreach (var entry in PagedRoles.Select((role, index) => (role, index))) { var role = entry.role; } } else { }
No. Role Description Created Actions
@((currentPage - 1) * pageSize + entry.index + 1)
@role.Name
@(role.Description ?? "-") @DisplayFormats.Date(role.CreatedAt)
@if (canManageAccess) { } @if (canEditRole) { } @if (canDeleteRole) { }
}
@* ===== Create / Edit Role Modal ===== *@ @if (showRoleModal) {

@(isEditing ? "Edit Role" : "Create New Role")

@if (isRoleNameError) { }
@if (isRoleNameError) {

@roleNameErrorMsg

}
@if (!string.IsNullOrEmpty(roleError)) {

@roleError

} @if (!isEditing) {

Choose what this role can see and do on the right. It will be saved with the role.

}
@if (!isEditing) {
Assign Access
@if (formAccessItems.Any()) { }
@if (isLoadingFormAccessItems) {
} else if (!formAccessItems.Any()) {

No access items found in database.

} else {
Page access lets the role open this section. Permissions let it use the buttons and actions inside. @formSelectedAccessCodes.Count selected
@if (GetRootAccessItems(true).Any()) {
@foreach (var parent in GetRootAccessItems(true)) { var children = GetChildAccessItems(parent, true).ToList(); var parentChecked = formSelectedAccessCodes.Contains(parent.MenuCode);
@if (parent.Permissions.Any()) {
Actions CRUD permissions for this page
@foreach (var action in parent.Permissions) { var isActionChecked = formSelectedAccessCodes.Contains(action.PermissionCode); }
} @if (children.Any()) {
@foreach (var child in children) { var childChecked = formSelectedAccessCodes.Contains(child.MenuCode);
@if (child.Permissions.Any()) {
@foreach (var action in child.Permissions) { var isActionChecked = formSelectedAccessCodes.Contains(action.PermissionCode); }
}
}
} else if (!parent.Permissions.Any()) {

Nothing else is available inside this page.

}
}
} else {

No pages or permissions match your search.

} }
}
@if (!isEditing && formSelectedAccessCodes.Count > 0) { @formSelectedAccessCodes.Count selected item(s) will be saved }
} @* ===== Assign Access Modal ===== *@ @* ===== Role Access Modal ===== *@ @if (showAccessModal) {

Role Access - @selectedRoleName

Configure access and permissions

@selectedAccessCodes.Count selected
@if (isLoadingAccessItems) {
} else if (!accessItems.Any()) {

No access items found in database.

} else {
Choosing an action also turns on its page. Turning off a page clears the actions inside it. @selectedAccessCodes.Count selected
@if (GetRootAccessItems(false).Any()) {
@foreach (var parent in GetRootAccessItems(false)) { var children = GetChildAccessItems(parent, false).ToList(); var parentChecked = selectedAccessCodes.Contains(parent.MenuCode);
@if (parent.Permissions.Any()) {
Actions CRUD permissions for this page
@foreach (var action in parent.Permissions) { var isActionChecked = selectedAccessCodes.Contains(action.PermissionCode); }
} @if (children.Any()) {
@foreach (var child in children) { var childChecked = selectedAccessCodes.Contains(child.MenuCode);
@if (child.Permissions.Any()) {
@foreach (var action in child.Permissions) { var isActionChecked = selectedAccessCodes.Contains(action.PermissionCode); }
}
}
} else if (!parent.Permissions.Any()) {

Nothing else is available inside this page.

}
}
} else {

No pages or permissions match your search.

} }
@if (!string.IsNullOrEmpty(accessSaveError)) {

@accessSaveError

} @if (!string.IsNullOrEmpty(accessSaveSuccess)) {

@accessSaveSuccess

}
Changes apply immediately after saving.
} @* ===== Confirm Dialog ===== *@ @code { private bool isLoading = true; private bool showRoleModal = false; private bool showAccessModal = false; private bool isEditing = false; private bool isSaving = false; private bool canCreateRole = false; private bool canEditRole = false; private bool canDeleteRole = false; private bool canManageAccess = false; private string? roleError; private bool isRoleNameError = false; private string roleNameErrorMsg = ""; // Confirm dialog state private bool showConfirmDialog = false; private bool confirmHideCancel = false; private string? successMessage; private string confirmTitle = ""; private string confirmMessage = ""; private string confirmButtonText = ""; private string confirmIcon = ""; private string confirmIconBg = ""; private string confirmIconText = ""; private string confirmButtonClass = ""; private Func? confirmAction; private List roleList = new(); private List userList = new(); // Search state private string searchInput = ""; // Pagination state private int currentPage = 1; private int pageSize = 10; private int totalCount = 0; private int totalPages = 0; private int TotalPages => totalPages; private int TotalCount => totalCount; private IEnumerable PagedRoles => roleList; // Role form private long editingId; private string formName = ""; private string? formDescription; // Inline access items in Create form private List formAccessItems = new(); private HashSet formSelectedAccessCodes = new(); private string formAccessSearch = ""; private bool isLoadingFormAccessItems = false; private bool IsAllFormAccessSelected => formAccessItems.Count > 0 && formSelectedAccessCodes.Count == (formAccessItems.Count + formAccessItems.Sum(m => m.Permissions.Count)); // Access modal (for editing existing role) private long selectedRoleId; private string selectedRoleName = ""; private List accessItems = new(); private HashSet selectedAccessCodes = new(); private string accessSearch = ""; private bool isLoadingAccessItems = false; private bool isSavingAccess = false; private string? accessSaveError; private string? accessSaveSuccess; private bool IsAllAccessSelected => accessItems.Count > 0 && selectedAccessCodes.Count == (accessItems.Count + accessItems.Sum(m => m.Permissions.Count)); protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var user = authState.User; canCreateRole = await MenuAuthorization.HasAccessCodeAsync(user, "Roles_Create"); canEditRole = await MenuAuthorization.HasAccessCodeAsync(user, "Roles_Update"); canDeleteRole = await MenuAuthorization.HasAccessCodeAsync(user, "Roles_Delete"); canManageAccess = canEditRole; await LoadRoles(); } protected override async Task OnAfterRenderAsync(bool firstRender) { await JS.InvokeVoidAsync("initIcons"); } private async Task LoadRoles() { isLoading = true; var client = ApiClient.CreateClient(); try { var rolesTask = client.GetFromJsonAsync>(BuildRolesUrl(), Serialization.CaseInsensitive); var usersTask = client.GetFromJsonAsync>("User", Serialization.CaseInsensitive); await Task.WhenAll(rolesTask, usersTask); roleList = rolesTask.Result?.Items ?? new(); totalCount = rolesTask.Result?.TotalCount ?? 0; totalPages = rolesTask.Result?.TotalPages ?? 0; currentPage = rolesTask.Result?.Page ?? currentPage; pageSize = rolesTask.Result?.PageSize ?? pageSize; userList = usersTask.Result ?? new(); } catch (Exception ex) { Console.WriteLine($"Load roles error: {ex.Message}"); } finally { isLoading = false; } } // Pagination handlers private async Task HandlePageChanged(int page) { currentPage = page; await LoadRoles(); } private async Task HandlePageSizeChanged(int size) { pageSize = size; currentPage = 1; await LoadRoles(); } private async Task ApplySearch() { currentPage = 1; await LoadRoles(); } private async Task ResetSearch() { searchInput = ""; currentPage = 1; await LoadRoles(); } private string BuildRolesUrl() { var query = new List { $"page={currentPage}", $"limit={pageSize}" }; if (!string.IsNullOrWhiteSpace(searchInput)) { query.Add($"search={Uri.EscapeDataString(searchInput.Trim())}"); } return $"Role?{string.Join("&", query)}"; } // ===== Role CRUD ===== private async Task OpenCreateModal() { isEditing = false; editingId = 0; formName = ""; formDescription = null; roleError = null; isRoleNameError = false; roleNameErrorMsg = ""; formSelectedAccessCodes = new(StringComparer.OrdinalIgnoreCase); formAccessItems = new(); formAccessSearch = ""; isLoadingFormAccessItems = true; showRoleModal = true; StateHasChanged(); try { var client = ApiClient.CreateClient(); formAccessItems = await client.GetFromJsonAsync>("Menu/access-items", Serialization.CaseInsensitive) ?? new(); } catch (Exception ex) { Console.WriteLine($"Load form menus error: {ex.Message}"); } finally { isLoadingFormAccessItems = false; StateHasChanged(); } } private void OpenEditModal(RoleDto role) { isEditing = true; editingId = role.Id; formName = role.Name; formDescription = role.Description; roleError = null; isRoleNameError = false; roleNameErrorMsg = ""; showRoleModal = true; } private void CloseRoleModal() { showRoleModal = false; roleError = null; isRoleNameError = false; roleNameErrorMsg = ""; formAccessItems = new(); formSelectedAccessCodes = new(StringComparer.OrdinalIgnoreCase); formAccessSearch = ""; } private void ToggleAccessSelection(AccessMenuDto menu, bool isChecked, bool isForm) { var targetSet = isForm ? formSelectedAccessCodes : selectedAccessCodes; var allMenuSource = isForm ? formAccessItems : accessItems; if (isChecked) { AddMenuSelection(menu, allMenuSource, targetSet); } else { RemoveMenuSelection(menu, allMenuSource, targetSet); } StateHasChanged(); } private void TogglePermissionSelection(string actionCode, bool isChecked, bool isForm) { var targetSet = isForm ? formSelectedAccessCodes : selectedAccessCodes; var menuSource = isForm ? formAccessItems : accessItems; if (isChecked) { targetSet.Add(actionCode); var permission = FindPermission(actionCode, menuSource); if (permission != null) { EnsureMenuPathSelected(permission.ParentMenuCode, menuSource, targetSet); EnsureMenuBranchVisible(permission.ParentMenuCode, menuSource, targetSet); } } else { targetSet.Remove(actionCode); } StateHasChanged(); } private void ToggleSelectAllFormAccess(ChangeEventArgs e) { var selectAll = (bool)(e.Value ?? false); if (selectAll) { formSelectedAccessCodes = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var menu in formAccessItems) { formSelectedAccessCodes.Add(menu.MenuCode); foreach (var action in menu.Permissions) { formSelectedAccessCodes.Add(action.PermissionCode); } } } else { formSelectedAccessCodes.Clear(); } } private static bool IsRootMenu(AccessMenuDto menu) => string.IsNullOrWhiteSpace(menu.ParentCode) || menu.ParentCode == "0"; private static bool MatchesSearch(string? value, string search) => !string.IsNullOrWhiteSpace(value) && value.Contains(search, StringComparison.OrdinalIgnoreCase); private IEnumerable GetRootAccessItems(bool isForm) { var source = isForm ? formAccessItems : accessItems; var search = (isForm ? formAccessSearch : accessSearch).Trim(); return source .Where(IsRootMenu) .Where(menu => string.IsNullOrWhiteSpace(search) || AccessBranchMatchesSearch(menu, source, search)) .OrderBy(m => m.OrderNo); } private IEnumerable GetChildAccessItems(AccessMenuDto parent, bool isForm) { var source = isForm ? formAccessItems : accessItems; var search = (isForm ? formAccessSearch : accessSearch).Trim(); var children = source .Where(c => c.ParentCode == parent.MenuCode) .OrderBy(c => c.OrderNo); if (string.IsNullOrWhiteSpace(search)) { return children; } return children.Where(child => AccessBranchMatchesSearch(child, source, search)); } private static bool AccessBranchMatchesSearch(AccessMenuDto menu, IReadOnlyList source, string search) { if (MatchesSearch(menu.MenuName, search) || MatchesSearch(menu.MenuCode, search) || MatchesSearch(menu.MenuUrl, search)) { return true; } if (menu.Permissions.Any(permission => MatchesSearch(permission.ActionName, search) || MatchesSearch(permission.PermissionCode, search) || MatchesSearch(permission.ApiName, search))) { return true; } return source .Where(child => child.ParentCode == menu.MenuCode) .Any(child => AccessBranchMatchesSearch(child, source, search)); } private static AccessPermissionDto? FindPermission(string actionCode, IReadOnlyList source) { foreach (var menu in source) { var permission = menu.Permissions.FirstOrDefault(p => string.Equals(p.PermissionCode, actionCode, StringComparison.OrdinalIgnoreCase)); if (permission != null) { return permission; } } return null; } private static AccessMenuDto? FindMenu(string menuCode, IReadOnlyList source) { return source.FirstOrDefault(m => string.Equals(m.MenuCode, menuCode, StringComparison.OrdinalIgnoreCase)); } private static void EnsureMenuPathSelected(string menuCode, IReadOnlyList source, ISet targetSet) { var current = FindMenu(menuCode, source); while (current != null) { targetSet.Add(current.MenuCode); if (string.IsNullOrWhiteSpace(current.ParentCode) || current.ParentCode == "0") { break; } current = FindMenu(current.ParentCode, source); } } private static void EnsureMenuBranchVisible(string menuCode, IReadOnlyList source, ISet targetSet) { var menu = FindMenu(menuCode, source); if (menu == null) { return; } targetSet.Add(menu.MenuCode); foreach (var child in GetAutoVisibleChildren(menu, source)) { EnsureMenuBranchVisible(child.MenuCode, source, targetSet); } } private static void AddMenuSelection(AccessMenuDto menu, IReadOnlyList source, ISet targetSet) { targetSet.Add(menu.MenuCode); foreach (var permission in menu.Permissions) { targetSet.Add(permission.PermissionCode); } EnsureMenuPathSelected(menu.ParentCode, source, targetSet); foreach (var child in GetAutoVisibleChildren(menu, source)) { EnsureMenuBranchVisible(child.MenuCode, source, targetSet); } } private static void RemoveMenuSelection(AccessMenuDto menu, IReadOnlyList source, ISet targetSet) { targetSet.Remove(menu.MenuCode); foreach (var permission in menu.Permissions) { targetSet.Remove(permission.PermissionCode); } foreach (var child in GetAutoVisibleChildren(menu, source)) { RemoveMenuSelection(child, source, targetSet); } CleanupAncestorSelection(menu.ParentCode, source, targetSet); } private static void CleanupAncestorSelection(string parentCode, IReadOnlyList source, ISet targetSet) { if (string.IsNullOrWhiteSpace(parentCode) || parentCode == "0") { return; } var parent = FindMenu(parentCode, source); if (parent == null) { return; } if (HasAnySelectedInBranch(parent, source, targetSet)) { return; } targetSet.Remove(parent.MenuCode); CleanupAncestorSelection(parent.ParentCode, source, targetSet); } private static bool HasAnySelectedInBranch(AccessMenuDto menu, IReadOnlyList source, ISet targetSet) { if (targetSet.Contains(menu.MenuCode) || menu.Permissions.Any(p => targetSet.Contains(p.PermissionCode))) { return true; } return GetAutoVisibleChildren(menu, source) .Any(child => HasAnySelectedInBranch(child, source, targetSet)); } private static void NormalizeAccessSelection(IReadOnlyList source, ISet targetSet) { var selectedPermissions = source .SelectMany(menu => menu.Permissions) .Where(permission => targetSet.Contains(permission.PermissionCode)) .ToList(); foreach (var permission in selectedPermissions) { EnsureMenuPathSelected(permission.ParentMenuCode, source, targetSet); EnsureMenuBranchVisible(permission.ParentMenuCode, source, targetSet); } } private static IEnumerable GetAutoVisibleChildren(AccessMenuDto menu, IReadOnlyList source) { return source .Where(m => m.ParentCode == menu.MenuCode && IsAutoVisiblePageMenu(m)) .OrderBy(m => m.OrderNo) .ThenBy(m => m.MenuName, StringComparer.OrdinalIgnoreCase); } private static bool IsAutoVisiblePageMenu(AccessMenuDto menu) { var text = $"{menu.MenuName} {menu.MenuCode} {menu.MenuUrl}".Trim(); return text.Contains("list", StringComparison.OrdinalIgnoreCase) || text.Contains("view", StringComparison.OrdinalIgnoreCase) || text.Contains("dashboard", StringComparison.OrdinalIgnoreCase) || text.Contains("home", StringComparison.OrdinalIgnoreCase); } private void RequestSaveConfirmation() { isRoleNameError = false; roleNameErrorMsg = ""; roleError = null; bool hasError = false; if (string.IsNullOrWhiteSpace(formName)) { isRoleNameError = true; roleNameErrorMsg = ResultMessages.RoleNameRequired; hasError = true; } if (hasError) return; if (isEditing) { confirmTitle = "Update Role?"; confirmMessage = "Are you sure you want to update this role?"; confirmButtonText = "Update"; confirmIcon = "pencil"; confirmIconBg = "bg-amber-50"; confirmIconText = "text-amber-500"; confirmButtonClass = "bg-violet-600 hover:bg-violet-700"; } else { confirmTitle = "Create Role?"; confirmMessage = "Are you sure you want to create this role?"; confirmButtonText = "Create"; confirmIcon = "plus-circle"; confirmIconBg = "bg-emerald-50"; confirmIconText = "text-emerald-500"; confirmButtonClass = "bg-violet-600 hover:bg-violet-700"; } confirmAction = SaveRole; showConfirmDialog = true; } private void RequestDeleteConfirmation(long id) { confirmTitle = "Delete Role?"; confirmMessage = "Are you sure you want to delete this role? This action cannot be undone."; confirmButtonText = "Delete"; confirmIcon = "trash-2"; confirmIconBg = "bg-rose-50"; confirmIconText = "text-rose-500"; confirmButtonClass = "bg-violet-600 hover:bg-violet-700"; confirmAction = () => DeleteRole(id); confirmHideCancel = false; showConfirmDialog = true; } private async Task HandleConfirmAction() { showConfirmDialog = false; if (confirmAction != null) await confirmAction(); } private void CloseConfirmDialog() { showConfirmDialog = false; confirmHideCancel = false; } private async Task SaveRole() { if (string.IsNullOrWhiteSpace(formName)) { return; } isSaving = true; roleError = null; var client = ApiClient.CreateClient(); try { if (isEditing) { var dto = new UpdateRoleDto { Name = formName, Description = formDescription }; var response = await client.PutAsJsonAsync($"Role/{editingId}", dto); var res = await response.Content.ReadFromJsonAsync(Serialization.CaseInsensitive); if (res == null || !res.IsSuccess) { roleError = res?.ErrorMessage ?? ResultMessages.FailedToUpdateRole; return; } } else { NormalizeAccessSelection(formAccessItems, formSelectedAccessCodes); var dto = new CreateRoleDto { Name = formName, Description = formDescription, AccessCodes = formSelectedAccessCodes.ToList() }; var response = await client.PostAsJsonAsync("Role", dto); var res = await response.Content.ReadFromJsonAsync>(Serialization.CaseInsensitive); if (res == null || !res.IsSuccess) { roleError = res?.ErrorMessage ?? ResultMessages.FailedToCreateRole; return; } } CloseRoleModal(); await LoadRoles(); successMessage = isEditing ? "Role updated successfully!" : "Role created successfully!"; } catch (Exception ex) { roleError = ex.Message; } finally { isSaving = false; } } private async Task DeleteRole(long id) { var usersCount = userList.Count(u => u.RoleId == id); if (usersCount > 0) { confirmTitle = "Cannot Delete Role"; confirmMessage = $"you can't delete this role because there are {usersCount} users in this role."; confirmButtonText = "OK"; confirmIcon = "alert-circle"; confirmIconBg = "bg-rose-50"; confirmIconText = "text-rose-500"; confirmButtonClass = "bg-rose-600 hover:bg-rose-700"; confirmAction = null; confirmHideCancel = true; showConfirmDialog = true; StateHasChanged(); return; } var client = ApiClient.CreateClient(); try { var response = await client.DeleteAsync($"Role/{id}"); var res = await response.Content.ReadFromJsonAsync(Serialization.CaseInsensitive); if (res == null || !res.IsSuccess) { confirmTitle = "Cannot Delete Role"; confirmMessage = res?.ErrorMessage ?? "Failed to delete role."; confirmButtonText = "OK"; confirmIcon = "alert-circle"; confirmIconBg = "bg-rose-50"; confirmIconText = "text-rose-500"; confirmButtonClass = "bg-rose-600 hover:bg-rose-700"; confirmAction = null; confirmHideCancel = true; showConfirmDialog = true; StateHasChanged(); return; } await LoadRoles(); if (currentPage > TotalPages && TotalPages > 0) currentPage = TotalPages; successMessage = "Role deleted successfully!"; } catch (Exception ex) { Console.WriteLine($"Delete error: {ex.Message}"); confirmTitle = "Error"; confirmMessage = $"Delete error: {ex.Message}"; confirmButtonText = "OK"; confirmIcon = "alert-circle"; confirmIconBg = "bg-rose-50"; confirmIconText = "text-rose-500"; confirmButtonClass = "bg-rose-600 hover:bg-rose-700"; confirmAction = null; confirmHideCancel = true; showConfirmDialog = true; StateHasChanged(); } } // ===== Access Management ===== private async Task OpenAccessModal(RoleDto role) { selectedRoleId = role.Id; selectedRoleName = role.Name; accessSaveError = null; accessSaveSuccess = null; accessSearch = ""; isLoadingAccessItems = true; showAccessModal = true; StateHasChanged(); var client = ApiClient.CreateClient(); try { // Load all available access items with their nested permissions accessItems = await client.GetFromJsonAsync>("Menu/access-items", Serialization.CaseInsensitive) ?? new(); // Load currently assigned access items for this role var assignedResult = await client.GetFromJsonAsync>>($"Role/{role.Id}/access", Serialization.CaseInsensitive); selectedAccessCodes = new HashSet(assignedResult?.Value ?? new(), StringComparer.OrdinalIgnoreCase); NormalizeAccessSelection(accessItems, selectedAccessCodes); } catch (Exception ex) { Console.WriteLine($"Load access items error: {ex.Message}"); accessSaveError = "Failed to load access items. Please try again."; } finally { isLoadingAccessItems = false; StateHasChanged(); } } private void CloseAccessModal() { showAccessModal = false; accessItems = new(); selectedAccessCodes = new(StringComparer.OrdinalIgnoreCase); accessSearch = ""; accessSaveError = null; accessSaveSuccess = null; } private void ToggleSelectAllAccess(ChangeEventArgs e) { var selectAll = (bool)(e.Value ?? false); if (selectAll) { selectedAccessCodes = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var menu in accessItems) { selectedAccessCodes.Add(menu.MenuCode); foreach (var action in menu.Permissions) { selectedAccessCodes.Add(action.PermissionCode); } } } else { selectedAccessCodes.Clear(); } } private async Task SaveAccess() { isSavingAccess = true; accessSaveError = null; accessSaveSuccess = null; var client = ApiClient.CreateClient(); try { NormalizeAccessSelection(accessItems, selectedAccessCodes); var dto = new AssignAccessDto { AccessCodes = selectedAccessCodes.ToList() }; var response = await client.PostAsJsonAsync($"Role/{selectedRoleId}/access", dto); if (response.IsSuccessStatusCode) { accessSaveSuccess = $"Access for '{selectedRoleName}' saved successfully!"; } else { var body = await response.Content.ReadAsStringAsync(); if (string.IsNullOrWhiteSpace(body)) { accessSaveError = $"Failed to save access. Server returned {(int)response.StatusCode}."; } else { try { using var doc = System.Text.Json.JsonDocument.Parse(body); accessSaveError = doc.RootElement.TryGetProperty("message", out var message) ? message.GetString() : body; } catch { accessSaveError = body; } } } } catch (Exception ex) { accessSaveError = ex.Message; } finally { isSavingAccess = false; StateHasChanged(); } } }