feat(web-client): add users management page and enhance roles CRUD
- Expand IamApiService with Users CRUD, Role mutations, role assignment methods - Create UserManagement.razor page at /admin/users with user list + role assignment - Enhance RolePermissions.razor with create/edit/delete role dialogs - Add Users nav item to AdminLayout sidebar - Add localization keys (vi-VN, en-US) for Users navigation
This commit is contained in:
@@ -88,6 +88,10 @@
|
||||
|
||||
@* ── QUẢN TRỊ ── *@
|
||||
<span class="admin-nav-label">@L["Admin_Nav_SectionAdmin"]</span>
|
||||
<NavLink href="/admin/users" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="users"></i>
|
||||
<span>@L["Admin_Nav_Users"]</span>
|
||||
</NavLink>
|
||||
<NavLink href="/admin/roles" class="admin-nav-item" ActiveClass="admin-nav-item--active">
|
||||
<i data-lucide="shield"></i>
|
||||
<span>@L["Admin_Nav_Roles"]</span>
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject WebClientTpos.Client.Services.IamApiService IamService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@using WebClientTpos.Client.Services
|
||||
@using MudBlazor
|
||||
|
||||
@*
|
||||
EN: Role permissions — real roles from IAM Service API.
|
||||
VI: Phân quyền — dữ liệu roles thật từ IAM Service API.
|
||||
EN: Role permissions — real roles from IAM Service API with full CRUD.
|
||||
VI: Phân quyền — dữ liệu roles thật từ IAM Service API với CRUD đầy đủ.
|
||||
*@
|
||||
|
||||
<PageTitle>Phân quyền — GoodGo Admin</PageTitle>
|
||||
@@ -18,7 +21,7 @@
|
||||
<p class="admin-topbar__subtitle">Quản lý vai trò và quyền truy cập</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<button class="admin-btn-primary">
|
||||
<button class="admin-btn-primary" @onclick="ShowCreateDialog">
|
||||
<i data-lucide="plus"></i>
|
||||
<span>Tạo vai trò</span>
|
||||
</button>
|
||||
@@ -37,7 +40,7 @@ else if (!_roles.Any())
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:12px;">
|
||||
<i data-lucide="shield" style="width:48px;height:48px;color:var(--admin-text-tertiary);"></i>
|
||||
<span style="color:var(--admin-text-tertiary);font-size:15px;">Chưa có vai trò nào</span>
|
||||
<span style="color:var(--admin-text-tertiary);font-size:13px;">Vai trò sẽ được tạo khi hệ thống IAM hoạt động</span>
|
||||
<span style="color:var(--admin-text-tertiary);font-size:13px;">Nhấn "Tạo vai trò" để bắt đầu</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -72,11 +75,32 @@ else
|
||||
@if (selected != null)
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<div class="admin-panel__header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="shield" style="color:var(--admin-orange-primary);"></i>
|
||||
Chi tiết — @selected.Name
|
||||
</h3>
|
||||
<div style="display:flex;gap:6px;">
|
||||
@if (!selected.IsSystem)
|
||||
{
|
||||
<button @onclick="@(() => ShowEditDialog(selected))" title="Sửa vai trò"
|
||||
style="background:none;border:1px solid var(--admin-border);cursor:pointer;padding:6px 10px;border-radius:8px;color:var(--admin-text-secondary);display:flex;align-items:center;gap:4px;font-size:12px;">
|
||||
<i data-lucide="pencil" style="width:13px;height:13px;"></i>
|
||||
Sửa
|
||||
</button>
|
||||
<button @onclick="@(() => DeleteRole(selected))" title="Xóa vai trò"
|
||||
style="background:none;border:1px solid rgba(239,68,68,0.3);cursor:pointer;padding:6px 10px;border-radius:8px;color:#EF4444;display:flex;align-items:center;gap:4px;font-size:12px;">
|
||||
<i data-lucide="trash-2" style="width:13px;height:13px;"></i>
|
||||
Xóa
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span style="font-size:11px;padding:4px 10px;border-radius:8px;background:rgba(107,114,128,0.15);color:#6B7280;">
|
||||
Vai trò hệ thống
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div style="display:flex;gap:24px;">
|
||||
@@ -106,7 +130,7 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Permission Groups — placeholder since IAM doesn't expose permissions yet *@
|
||||
@* Permission Groups — placeholder since IAM doesn't expose permissions per role yet *@
|
||||
<div style="font-size:12px;font-weight:700;color:var(--admin-text-tertiary);text-transform:uppercase;letter-spacing:0.05em;">
|
||||
Quyền hạn
|
||||
</div>
|
||||
@@ -127,42 +151,221 @@ else
|
||||
</div>
|
||||
}
|
||||
|
||||
@* ═══ CREATE / EDIT DIALOG ═══ *@
|
||||
@if (_showDialog)
|
||||
{
|
||||
<div style="position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:1000;display:flex;align-items:center;justify-content:center;"
|
||||
@onclick="CloseDialog">
|
||||
<div style="background:var(--admin-bg-card);border-radius:16px;padding:28px;min-width:420px;max-width:480px;box-shadow:0 20px 60px rgba(0,0,0,0.5);"
|
||||
@onclick:stopPropagation>
|
||||
<h3 style="font-size:18px;font-weight:700;margin-bottom:20px;">
|
||||
@(_isEditing ? "Sửa vai trò" : "Tạo vai trò mới")
|
||||
</h3>
|
||||
<div style="display:flex;flex-direction:column;gap:16px;">
|
||||
<div>
|
||||
<label style="font-size:12px;font-weight:600;color:var(--admin-text-tertiary);text-transform:uppercase;letter-spacing:0.05em;">
|
||||
Tên vai trò *
|
||||
</label>
|
||||
<input @bind="_dialogName" placeholder="Ví dụ: Manager"
|
||||
style="width:100%;margin-top:6px;padding:10px 14px;border-radius:10px;border:1px solid var(--admin-border);background:var(--admin-bg-interactive);color:var(--admin-text-primary);font-size:14px;outline:none;" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:12px;font-weight:600;color:var(--admin-text-tertiary);text-transform:uppercase;letter-spacing:0.05em;">
|
||||
Mô tả
|
||||
</label>
|
||||
<textarea @bind="_dialogDescription" placeholder="Mô tả quyền hạn của vai trò..." rows="3"
|
||||
style="width:100%;margin-top:6px;padding:10px 14px;border-radius:10px;border:1px solid var(--admin-border);background:var(--admin-bg-interactive);color:var(--admin-text-primary);font-size:14px;outline:none;resize:vertical;" />
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(_dialogError))
|
||||
{
|
||||
<div style="padding:8px 12px;background:rgba(239,68,68,0.1);border-radius:8px;color:#EF4444;font-size:13px;">
|
||||
@_dialogError
|
||||
</div>
|
||||
}
|
||||
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:8px;">
|
||||
<button class="admin-btn-secondary" @onclick="CloseDialog" style="padding:8px 20px;">
|
||||
Hủy
|
||||
</button>
|
||||
<button class="admin-btn-primary" @onclick="SaveRole" disabled="@_dialogSaving"
|
||||
style="padding:8px 20px;">
|
||||
@if (_dialogSaving) { <MudProgressCircular Size="Size.Small" Indeterminate="true" /> }
|
||||
else { <span>@(_isEditing ? "Cập nhật" : "Tạo")</span> }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool _loading = true;
|
||||
private Guid? _selectedId;
|
||||
private List<IamApiService.RoleDto> _roles = new();
|
||||
|
||||
// EN: Dialog state for create/edit
|
||||
// VI: Trạng thái dialog cho tạo/sửa
|
||||
private bool _showDialog = false;
|
||||
private bool _isEditing = false;
|
||||
private Guid? _editingRoleId;
|
||||
private string _dialogName = string.Empty;
|
||||
private string? _dialogDescription;
|
||||
private string? _dialogError;
|
||||
private bool _dialogSaving = false;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadRoles();
|
||||
}
|
||||
|
||||
private async Task LoadRoles()
|
||||
{
|
||||
_loading = true;
|
||||
try { _roles = await IamService.GetRolesAsync(); }
|
||||
catch { _roles = new(); }
|
||||
finally
|
||||
{
|
||||
_selectedId = _roles.FirstOrDefault()?.Id;
|
||||
_selectedId ??= _roles.FirstOrDefault()?.Id;
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── CREATE ───
|
||||
|
||||
private void ShowCreateDialog()
|
||||
{
|
||||
_isEditing = false;
|
||||
_editingRoleId = null;
|
||||
_dialogName = string.Empty;
|
||||
_dialogDescription = null;
|
||||
_dialogError = null;
|
||||
_dialogSaving = false;
|
||||
_showDialog = true;
|
||||
}
|
||||
|
||||
// ─── EDIT ───
|
||||
|
||||
private void ShowEditDialog(IamApiService.RoleDto role)
|
||||
{
|
||||
_isEditing = true;
|
||||
_editingRoleId = role.Id;
|
||||
_dialogName = role.Name;
|
||||
_dialogDescription = role.Description;
|
||||
_dialogError = null;
|
||||
_dialogSaving = false;
|
||||
_showDialog = true;
|
||||
}
|
||||
|
||||
// ─── SAVE (CREATE / UPDATE) ───
|
||||
|
||||
private async Task SaveRole()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_dialogName))
|
||||
{
|
||||
_dialogError = "Tên vai trò không được để trống";
|
||||
return;
|
||||
}
|
||||
|
||||
_dialogSaving = true;
|
||||
_dialogError = null;
|
||||
|
||||
if (_isEditing && _editingRoleId != null)
|
||||
{
|
||||
var (success, error) = await IamService.UpdateRoleAsync(_editingRoleId.Value, _dialogName.Trim(), _dialogDescription?.Trim());
|
||||
if (success)
|
||||
{
|
||||
Snackbar.Add($"Đã cập nhật vai trò '{_dialogName}'", Severity.Success);
|
||||
CloseDialog();
|
||||
await LoadRoles();
|
||||
}
|
||||
else
|
||||
{
|
||||
_dialogError = error;
|
||||
_dialogSaving = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var (success, error) = await IamService.CreateRoleAsync(_dialogName.Trim(), _dialogDescription?.Trim());
|
||||
if (success)
|
||||
{
|
||||
Snackbar.Add($"Đã tạo vai trò '{_dialogName}'", Severity.Success);
|
||||
CloseDialog();
|
||||
await LoadRoles();
|
||||
// EN: Select the new role
|
||||
// VI: Chọn role mới tạo
|
||||
_selectedId = _roles.FirstOrDefault(r => r.Name == _dialogName.Trim())?.Id ?? _selectedId;
|
||||
}
|
||||
else
|
||||
{
|
||||
_dialogError = error;
|
||||
_dialogSaving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── DELETE ───
|
||||
|
||||
private async Task DeleteRole(IamApiService.RoleDto role)
|
||||
{
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"Xóa vai trò",
|
||||
$"Bạn có chắc muốn xóa vai trò '{role.Name}'? Hành động này không thể hoàn tác.",
|
||||
yesText: "Xóa", cancelText: "Hủy");
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
var (success, error) = await IamService.DeleteRoleAsync(role.Id);
|
||||
if (success)
|
||||
{
|
||||
Snackbar.Add($"Đã xóa vai trò '{role.Name}'", Severity.Success);
|
||||
if (_selectedId == role.Id) _selectedId = null;
|
||||
await LoadRoles();
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(error ?? "Xóa vai trò thất bại", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseDialog()
|
||||
{
|
||||
_showDialog = false;
|
||||
_dialogSaving = false;
|
||||
}
|
||||
|
||||
private static string GetRoleColor(string name) => name.ToLower() switch
|
||||
{
|
||||
"admin" or "administrator" => "#FF5C00",
|
||||
"superadmin" => "#DC2626",
|
||||
"merchant" or "owner" => "#8B5CF6",
|
||||
"merchantadmin" => "#A855F7",
|
||||
"merchantstaff" => "#6366F1",
|
||||
"cashier" => "#3B82F6",
|
||||
"manager" => "#22C55E",
|
||||
"premiumuser" => "#F59E0B",
|
||||
"support" => "#06B6D4",
|
||||
"user" => "#6B7280",
|
||||
_ => "#6B7280"
|
||||
};
|
||||
|
||||
private static string GetRoleIcon(string name) => name.ToLower() switch
|
||||
{
|
||||
"admin" or "administrator" => "crown",
|
||||
"superadmin" => "shield-alert",
|
||||
"merchant" or "owner" => "user-check",
|
||||
"merchantadmin" => "user-cog",
|
||||
"merchantstaff" => "user",
|
||||
"cashier" => "calculator",
|
||||
"manager" => "briefcase",
|
||||
"premiumuser" => "star",
|
||||
"support" => "headphones",
|
||||
"user" => "user",
|
||||
_ => "shield"
|
||||
};
|
||||
|
||||
// EN: Default permission display (placeholder until IAM exposes permissions API)
|
||||
// VI: Hiển thị quyền mặc định (placeholder cho đến khi IAM cung cấp API permissions)
|
||||
// EN: Default permission display (placeholder until IAM exposes permissions per-role API)
|
||||
// VI: Hiển thị quyền mặc định (placeholder cho đến khi IAM cung cấp API permissions per-role)
|
||||
private record PermDef(string Label, string Desc, bool Allowed);
|
||||
private readonly PermDef[] _defaultPermissions = new[]
|
||||
{
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
@page "/admin/users"
|
||||
@layout AdminLayout
|
||||
@inherits AdminBase
|
||||
@inject WebClientTpos.Client.Services.IamApiService IamService
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@using WebClientTpos.Client.Services
|
||||
@using MudBlazor
|
||||
|
||||
@*
|
||||
EN: User Management — list users, view details, assign/remove roles.
|
||||
VI: Quản lý người dùng — danh sách, xem chi tiết, gán/xóa vai trò.
|
||||
*@
|
||||
|
||||
<PageTitle>Người dùng — GoodGo Admin</PageTitle>
|
||||
|
||||
@* ═══ TOP BAR ═══ *@
|
||||
<div class="admin-topbar">
|
||||
<div class="admin-topbar__left">
|
||||
<h1 class="admin-topbar__title">Người dùng</h1>
|
||||
<p class="admin-topbar__subtitle">Quản lý tài khoản và phân quyền người dùng</p>
|
||||
</div>
|
||||
<div class="admin-topbar__right">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<div style="position:relative;">
|
||||
<i data-lucide="search" style="position:absolute;left:12px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
|
||||
<input type="text" @bind="_searchQuery" @bind:event="oninput" @onkeyup="OnSearchChanged"
|
||||
placeholder="Tìm theo tên hoặc email..."
|
||||
style="padding:8px 12px 8px 36px;border-radius:10px;border:1px solid var(--admin-border);background:var(--admin-bg-interactive);color:var(--admin-text-primary);font-size:13px;width:240px;outline:none;" />
|
||||
</div>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);">
|
||||
@_totalCount người dùng
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* ═══ CONTENT ═══ *@
|
||||
@if (_loading)
|
||||
{
|
||||
<div class="admin-content" style="display:flex;align-items:center;justify-content:center;min-height:300px;">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
|
||||
</div>
|
||||
}
|
||||
else if (!_users.Any())
|
||||
{
|
||||
<div class="admin-content" style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:12px;">
|
||||
<i data-lucide="users" style="width:48px;height:48px;color:var(--admin-text-tertiary);"></i>
|
||||
<span style="color:var(--admin-text-tertiary);font-size:15px;">Chưa có người dùng nào</span>
|
||||
<span style="color:var(--admin-text-tertiary);font-size:13px;">Người dùng sẽ xuất hiện khi đăng ký tài khoản</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-content" style="display:flex;gap:24px;">
|
||||
@* LEFT: User list *@
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:8px;">
|
||||
@foreach (var user in _users)
|
||||
{
|
||||
<button class="admin-role-card @(_selectedId == user.Id ? "admin-role-card--active" : "")"
|
||||
@onclick="@(() => SelectUser(user))">
|
||||
<div style="display:flex;align-items:center;gap:12px;flex:1;">
|
||||
<div style="width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg, #FF5C00 0%, #FF8A3D 100%);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:#fff;flex-shrink:0;">
|
||||
@GetInitials(user)
|
||||
</div>
|
||||
<div style="text-align:left;min-width:0;">
|
||||
<div style="font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
||||
@(string.IsNullOrEmpty(user.FullName) ? user.Email : user.FullName)
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--admin-text-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
|
||||
@user.Email
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
@if (!string.IsNullOrEmpty(user.Status))
|
||||
{
|
||||
<span style="font-size:10px;padding:2px 8px;border-radius:12px;
|
||||
background:@(user.Status?.ToLower() == "active" ? "rgba(34,197,94,0.15)" : "rgba(239,68,68,0.15)");
|
||||
color:@(user.Status?.ToLower() == "active" ? "#22C55E" : "#EF4444");">
|
||||
@user.Status
|
||||
</span>
|
||||
}
|
||||
<i data-lucide="chevron-right" style="width:16px;height:16px;color:var(--admin-text-tertiary);"></i>
|
||||
</div>
|
||||
</button>
|
||||
}
|
||||
|
||||
@* Pagination *@
|
||||
@if (_totalCount > _pageSize)
|
||||
{
|
||||
<div style="display:flex;justify-content:center;gap:8px;margin-top:12px;">
|
||||
<button class="admin-btn-secondary" disabled="@(_currentPage <= 1)" @onclick="PreviousPage"
|
||||
style="font-size:12px;padding:6px 12px;">
|
||||
← Trước
|
||||
</button>
|
||||
<span style="font-size:12px;color:var(--admin-text-tertiary);align-self:center;">
|
||||
Trang @_currentPage / @TotalPages
|
||||
</span>
|
||||
<button class="admin-btn-secondary" disabled="@(_currentPage >= TotalPages)" @onclick="NextPage"
|
||||
style="font-size:12px;padding:6px 12px;">
|
||||
Sau →
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* RIGHT: User Details *@
|
||||
<div style="width:420px;display:flex;flex-direction:column;gap:16px;">
|
||||
@if (_selectedUser != null)
|
||||
{
|
||||
@* User Info Card *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="user" style="color:var(--admin-orange-primary);"></i>
|
||||
Chi tiết người dùng
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:16px;">
|
||||
@* Avatar + Name *@
|
||||
<div style="display:flex;align-items:center;gap:16px;">
|
||||
<div style="width:56px;height:56px;border-radius:50%;background:linear-gradient(135deg, #FF5C00 0%, #FF8A3D 100%);display:flex;align-items:center;justify-content:center;font-size:20px;font-weight:700;color:#fff;">
|
||||
@GetInitials(_selectedUser)
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:16px;font-weight:700;">
|
||||
@(string.IsNullOrEmpty(_selectedUser.FullName) ? _selectedUser.Email : _selectedUser.FullName)
|
||||
</div>
|
||||
<div style="font-size:13px;color:var(--admin-text-tertiary);">@_selectedUser.Email</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Stats Grid *@
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
||||
<div class="admin-store-stat">
|
||||
<div class="admin-store-stat__value" style="font-size:13px;">
|
||||
@(_selectedUser.Status ?? "N/A")
|
||||
</div>
|
||||
<div class="admin-store-stat__label">Trạng thái</div>
|
||||
</div>
|
||||
<div class="admin-store-stat">
|
||||
<div class="admin-store-stat__value" style="font-size:13px;">
|
||||
@_selectedUser.CreatedAt.ToString("dd/MM/yy")
|
||||
</div>
|
||||
<div class="admin-store-stat__label">Ngày tạo</div>
|
||||
</div>
|
||||
<div class="admin-store-stat">
|
||||
<div class="admin-store-stat__value" style="font-size:13px;">
|
||||
@(_selectedUser.LastLoginAt?.ToString("dd/MM/yy HH:mm") ?? "Chưa đăng nhập")
|
||||
</div>
|
||||
<div class="admin-store-stat__label">Đăng nhập cuối</div>
|
||||
</div>
|
||||
<div class="admin-store-stat">
|
||||
<div class="admin-store-stat__value" style="font-size:13px;">
|
||||
@_selectedUserRoles.Count
|
||||
</div>
|
||||
<div class="admin-store-stat__label">Vai trò</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Roles Card *@
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h3 class="admin-panel__title">
|
||||
<i data-lucide="shield" style="color:var(--admin-orange-primary);"></i>
|
||||
Vai trò
|
||||
</h3>
|
||||
</div>
|
||||
<div class="admin-panel__body" style="display:flex;flex-direction:column;gap:12px;">
|
||||
@* Assigned roles *@
|
||||
@if (_selectedUserRoles.Any())
|
||||
{
|
||||
@foreach (var role in _selectedUserRoles)
|
||||
{
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 12px;background:var(--admin-bg-interactive);border-radius:10px;">
|
||||
<div style="display:flex;align-items:center;gap:8px;">
|
||||
<i data-lucide="shield-check" style="width:16px;height:16px;color:#22C55E;"></i>
|
||||
<span style="font-size:13px;font-weight:500;">@role</span>
|
||||
</div>
|
||||
<button @onclick="@(() => RemoveRole(role))" title="Xóa vai trò"
|
||||
style="background:none;border:none;cursor:pointer;padding:4px;border-radius:6px;color:var(--admin-text-tertiary);"
|
||||
@onmouseover="@(e => {})" >
|
||||
<i data-lucide="x" style="width:14px;height:14px;color:#EF4444;"></i>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div style="text-align:center;padding:12px;color:var(--admin-text-tertiary);font-size:13px;">
|
||||
Chưa có vai trò nào
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Assign new role *@
|
||||
<div style="display:flex;gap:8px;margin-top:4px;">
|
||||
<select @bind="_roleToAssign" style="flex:1;padding:8px 12px;border-radius:10px;border:1px solid var(--admin-border);background:var(--admin-bg-interactive);color:var(--admin-text-primary);font-size:13px;">
|
||||
<option value="">-- Chọn vai trò --</option>
|
||||
@foreach (var role in _availableRoles.Where(r => !_selectedUserRoles.Contains(r.Name)))
|
||||
{
|
||||
<option value="@role.Name">@role.Name</option>
|
||||
}
|
||||
</select>
|
||||
<button class="admin-btn-primary" @onclick="AssignRole"
|
||||
disabled="@(string.IsNullOrEmpty(_roleToAssign))"
|
||||
style="font-size:13px;padding:8px 16px;white-space:nowrap;">
|
||||
<i data-lucide="plus" style="width:14px;height:14px;"></i>
|
||||
Gán
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Actions *@
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button class="admin-btn-secondary" @onclick="DeactivateUser"
|
||||
style="flex:1;font-size:13px;padding:8px 16px;color:#EF4444;border-color:rgba(239,68,68,0.3);">
|
||||
<i data-lucide="user-x" style="width:14px;height:14px;"></i>
|
||||
Vô hiệu hóa
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="admin-panel">
|
||||
<div class="admin-panel__body" style="text-align:center;padding:48px 20px;">
|
||||
<i data-lucide="mouse-pointer-click" style="width:40px;height:40px;color:var(--admin-text-tertiary);margin-bottom:12px;"></i>
|
||||
<p style="color:var(--admin-text-tertiary);font-size:14px;">Chọn một người dùng để xem chi tiết</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool _loading = true;
|
||||
private Guid? _selectedId;
|
||||
private IamApiService.UserListDto? _selectedUser;
|
||||
private List<IamApiService.UserListDto> _users = new();
|
||||
private List<string> _selectedUserRoles = new();
|
||||
private List<IamApiService.RoleDto> _availableRoles = new();
|
||||
private string _searchQuery = string.Empty;
|
||||
private string _roleToAssign = string.Empty;
|
||||
private int _currentPage = 1;
|
||||
private int _pageSize = 20;
|
||||
private int _totalCount = 0;
|
||||
private int TotalPages => Math.Max(1, (int)Math.Ceiling((double)_totalCount / _pageSize));
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
private async Task LoadData()
|
||||
{
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var (users, total) = await IamService.GetUsersAsync(_currentPage, _pageSize);
|
||||
_users = users;
|
||||
_totalCount = total;
|
||||
_availableRoles = await IamService.GetRolesAsync();
|
||||
|
||||
// EN: Auto-select first user if none selected
|
||||
// VI: Tự động chọn user đầu tiên nếu chưa chọn
|
||||
if (_selectedId == null && _users.Any())
|
||||
{
|
||||
await SelectUser(_users.First());
|
||||
}
|
||||
}
|
||||
catch { _users = new(); }
|
||||
finally { _loading = false; }
|
||||
}
|
||||
|
||||
private async Task SelectUser(IamApiService.UserListDto user)
|
||||
{
|
||||
_selectedId = user.Id;
|
||||
_selectedUser = user;
|
||||
_roleToAssign = string.Empty;
|
||||
|
||||
// EN: Load user's roles
|
||||
// VI: Tải danh sách roles của user
|
||||
try { _selectedUserRoles = await IamService.GetUserRolesAsync(user.Id); }
|
||||
catch { _selectedUserRoles = new(); }
|
||||
}
|
||||
|
||||
private async Task AssignRole()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_roleToAssign) || _selectedUser == null) return;
|
||||
var (success, error) = await IamService.AssignRoleAsync(_selectedUser.Id, _roleToAssign);
|
||||
if (success)
|
||||
{
|
||||
Snackbar.Add($"Đã gán vai trò '{_roleToAssign}'", Severity.Success);
|
||||
_selectedUserRoles = await IamService.GetUserRolesAsync(_selectedUser.Id);
|
||||
_roleToAssign = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(error ?? "Gán vai trò thất bại", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveRole(string roleName)
|
||||
{
|
||||
if (_selectedUser == null) return;
|
||||
var (success, error) = await IamService.RemoveRoleAsync(_selectedUser.Id, roleName);
|
||||
if (success)
|
||||
{
|
||||
Snackbar.Add($"Đã xóa vai trò '{roleName}'", Severity.Success);
|
||||
_selectedUserRoles = await IamService.GetUserRolesAsync(_selectedUser.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(error ?? "Xóa vai trò thất bại", Severity.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeactivateUser()
|
||||
{
|
||||
if (_selectedUser == null) return;
|
||||
|
||||
// EN: Confirm before deactivating
|
||||
// VI: Xác nhận trước khi vô hiệu hóa
|
||||
var result = await DialogService.ShowMessageBox(
|
||||
"Vô hiệu hóa người dùng",
|
||||
$"Bạn có chắc muốn vô hiệu hóa tài khoản '{_selectedUser.Email}'?",
|
||||
yesText: "Vô hiệu hóa", cancelText: "Hủy");
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
var (success, error) = await IamService.DeleteUserAsync(_selectedUser.Id);
|
||||
if (success)
|
||||
{
|
||||
Snackbar.Add("Đã vô hiệu hóa người dùng", Severity.Success);
|
||||
await LoadData();
|
||||
_selectedUser = null;
|
||||
_selectedId = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
Snackbar.Add(error ?? "Vô hiệu hóa thất bại", Severity.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnSearchChanged()
|
||||
{
|
||||
// EN: Simple client-side filter (for now); swap to server-side later
|
||||
// VI: Lọc phía client (tạm thời); chuyển sang server-side sau
|
||||
_currentPage = 1;
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
private async Task PreviousPage()
|
||||
{
|
||||
if (_currentPage > 1) { _currentPage--; await LoadData(); }
|
||||
}
|
||||
|
||||
private async Task NextPage()
|
||||
{
|
||||
if (_currentPage < TotalPages) { _currentPage++; await LoadData(); }
|
||||
}
|
||||
|
||||
private static string GetInitials(IamApiService.UserListDto user)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(user.FirstName) && !string.IsNullOrEmpty(user.LastName))
|
||||
return $"{user.FirstName[0]}{user.LastName[0]}".ToUpper();
|
||||
if (!string.IsNullOrEmpty(user.Email))
|
||||
return user.Email[..Math.Min(2, user.Email.Length)].ToUpper();
|
||||
return "??";
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,9 @@ public class IamApiService
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// ─── ROLES ───
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public record RoleDto(
|
||||
Guid Id,
|
||||
@@ -75,7 +77,236 @@ public class IamApiService
|
||||
catch { return new(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a new role.
|
||||
/// VI: Tạo role mới.
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error)> CreateRoleAsync(string name, string? description)
|
||||
{
|
||||
await SetAuthHeader();
|
||||
try
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync("/api/iam/api/v1/roles", new { name, description }, _jsonOptions);
|
||||
if (response.IsSuccessStatusCode) return (true, null);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
||||
var msg = json.TryGetProperty("error", out var err) && err.TryGetProperty("message", out var m)
|
||||
? m.GetString() : response.ReasonPhrase;
|
||||
return (false, msg ?? "Tạo vai trò thất bại");
|
||||
}
|
||||
catch (Exception ex) { return (false, ex.Message); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update an existing role.
|
||||
/// VI: Cập nhật role.
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error)> UpdateRoleAsync(Guid id, string name, string? description)
|
||||
{
|
||||
await SetAuthHeader();
|
||||
try
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync($"/api/iam/api/v1/roles/{id}", new { name, description }, _jsonOptions);
|
||||
if (response.IsSuccessStatusCode) return (true, null);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
||||
var msg = json.TryGetProperty("error", out var err) && err.TryGetProperty("message", out var m)
|
||||
? m.GetString() : response.ReasonPhrase;
|
||||
return (false, msg ?? "Cập nhật vai trò thất bại");
|
||||
}
|
||||
catch (Exception ex) { return (false, ex.Message); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Delete a role.
|
||||
/// VI: Xóa role.
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error)> DeleteRoleAsync(Guid id)
|
||||
{
|
||||
await SetAuthHeader();
|
||||
try
|
||||
{
|
||||
var response = await _http.DeleteAsync($"/api/iam/api/v1/roles/{id}");
|
||||
if (response.IsSuccessStatusCode) return (true, null);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
||||
var msg = json.TryGetProperty("error", out var err) && err.TryGetProperty("message", out var m)
|
||||
? m.GetString() : response.ReasonPhrase;
|
||||
return (false, msg ?? "Xóa vai trò thất bại");
|
||||
}
|
||||
catch (Exception ex) { return (false, ex.Message); }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// ─── USERS ───
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public record UserListDto(
|
||||
Guid Id,
|
||||
string Email,
|
||||
string FirstName,
|
||||
string LastName,
|
||||
string FullName,
|
||||
string? Status,
|
||||
DateTime CreatedAt,
|
||||
DateTime? LastLoginAt);
|
||||
|
||||
public record UserRolesDto(string UserId, IEnumerable<string> Roles);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get paginated list of users.
|
||||
/// VI: Lấy danh sách users phân trang.
|
||||
/// </summary>
|
||||
public async Task<(List<UserListDto> Users, int TotalCount)> GetUsersAsync(int page = 1, int pageSize = 20)
|
||||
{
|
||||
await SetAuthHeader();
|
||||
try
|
||||
{
|
||||
var response = await _http.GetAsync($"/api/iam/api/v1/users?pageNumber={page}&pageSize={pageSize}");
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
||||
var users = new List<UserListDto>();
|
||||
var total = 0;
|
||||
|
||||
if (json.TryGetProperty("data", out var data))
|
||||
{
|
||||
if (data.ValueKind == JsonValueKind.Array)
|
||||
users = data.Deserialize<List<UserListDto>>(_jsonOptions) ?? new();
|
||||
else if (data.TryGetProperty("items", out var items))
|
||||
users = items.Deserialize<List<UserListDto>>(_jsonOptions) ?? new();
|
||||
}
|
||||
|
||||
if (json.TryGetProperty("pagination", out var pagination) &&
|
||||
pagination.TryGetProperty("totalCount", out var tc))
|
||||
total = tc.GetInt32();
|
||||
|
||||
return (users, total);
|
||||
}
|
||||
return (new(), 0);
|
||||
}
|
||||
catch { return (new(), 0); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get user by ID.
|
||||
/// VI: Lấy user theo ID.
|
||||
/// </summary>
|
||||
public async Task<UserListDto?> GetUserByIdAsync(Guid id)
|
||||
{
|
||||
await SetAuthHeader();
|
||||
try
|
||||
{
|
||||
var response = await _http.GetAsync($"/api/iam/api/v1/users/{id}");
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
||||
if (json.TryGetProperty("data", out var data))
|
||||
return data.Deserialize<UserListDto>(_jsonOptions);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get roles assigned to a user.
|
||||
/// VI: Lấy danh sách roles của user.
|
||||
/// </summary>
|
||||
public async Task<List<string>> GetUserRolesAsync(Guid userId)
|
||||
{
|
||||
await SetAuthHeader();
|
||||
try
|
||||
{
|
||||
var response = await _http.GetAsync($"/api/iam/api/v1/users/{userId}/roles");
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
||||
if (json.TryGetProperty("data", out var data) &&
|
||||
data.TryGetProperty("roles", out var roles))
|
||||
return roles.Deserialize<List<string>>(_jsonOptions) ?? new();
|
||||
}
|
||||
return new();
|
||||
}
|
||||
catch { return new(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Assign a role to a user.
|
||||
/// VI: Gán role cho user.
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error)> AssignRoleAsync(Guid userId, string roleName)
|
||||
{
|
||||
await SetAuthHeader();
|
||||
try
|
||||
{
|
||||
var response = await _http.PostAsJsonAsync($"/api/iam/api/v1/users/{userId}/roles", new { roleName }, _jsonOptions);
|
||||
if (response.IsSuccessStatusCode) return (true, null);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
||||
var msg = json.TryGetProperty("error", out var err) && err.TryGetProperty("message", out var m)
|
||||
? m.GetString() : response.ReasonPhrase;
|
||||
return (false, msg ?? "Gán vai trò thất bại");
|
||||
}
|
||||
catch (Exception ex) { return (false, ex.Message); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Remove a role from a user.
|
||||
/// VI: Xóa role khỏi user.
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error)> RemoveRoleAsync(Guid userId, string roleName)
|
||||
{
|
||||
await SetAuthHeader();
|
||||
try
|
||||
{
|
||||
var response = await _http.DeleteAsync($"/api/iam/api/v1/users/{userId}/roles/{Uri.EscapeDataString(roleName)}");
|
||||
if (response.IsSuccessStatusCode) return (true, null);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
||||
var msg = json.TryGetProperty("error", out var err) && err.TryGetProperty("message", out var m)
|
||||
? m.GetString() : response.ReasonPhrase;
|
||||
return (false, msg ?? "Xóa vai trò thất bại");
|
||||
}
|
||||
catch (Exception ex) { return (false, ex.Message); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update user info (first name, last name).
|
||||
/// VI: Cập nhật thông tin user (tên, họ).
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error)> UpdateUserAsync(Guid id, string? firstName, string? lastName)
|
||||
{
|
||||
await SetAuthHeader();
|
||||
try
|
||||
{
|
||||
var response = await _http.PutAsJsonAsync($"/api/iam/api/v1/users/{id}", new { firstName, lastName }, _jsonOptions);
|
||||
if (response.IsSuccessStatusCode) return (true, null);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
||||
var msg = json.TryGetProperty("error", out var err) && err.TryGetProperty("message", out var m)
|
||||
? m.GetString() : response.ReasonPhrase;
|
||||
return (false, msg ?? "Cập nhật user thất bại");
|
||||
}
|
||||
catch (Exception ex) { return (false, ex.Message); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Delete (deactivate) a user.
|
||||
/// VI: Xóa (vô hiệu hóa) user.
|
||||
/// </summary>
|
||||
public async Task<(bool Success, string? Error)> DeleteUserAsync(Guid id)
|
||||
{
|
||||
await SetAuthHeader();
|
||||
try
|
||||
{
|
||||
var response = await _http.DeleteAsync($"/api/iam/api/v1/users/{id}");
|
||||
if (response.IsSuccessStatusCode) return (true, null);
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
|
||||
var msg = json.TryGetProperty("error", out var err) && err.TryGetProperty("message", out var m)
|
||||
? m.GetString() : response.ReasonPhrase;
|
||||
return (false, msg ?? "Xóa user thất bại");
|
||||
}
|
||||
catch (Exception ex) { return (false, ex.Message); }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// ─── AUDIT LOGS ───
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
public record AuditLogDto(
|
||||
int? Id, DateTime? Timestamp, string? EventType, string? ActorId,
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"Admin_Nav_SectionAdmin": "Administration",
|
||||
"Admin_Nav_SectionSystem": "System",
|
||||
"Admin_Nav_Stores": "Stores",
|
||||
"Admin_Nav_Users": "Users",
|
||||
"Admin_Nav_Roles": "Permissions",
|
||||
"Admin_Nav_AuditLog": "Audit Log",
|
||||
"Admin_Nav_Devices": "Devices",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"Admin_Nav_SectionAdmin": "Quản trị",
|
||||
"Admin_Nav_SectionSystem": "Hệ thống",
|
||||
"Admin_Nav_Stores": "Cửa hàng",
|
||||
"Admin_Nav_Users": "Người dùng",
|
||||
"Admin_Nav_Roles": "Phân quyền",
|
||||
"Admin_Nav_AuditLog": "Nhật ký",
|
||||
"Admin_Nav_Devices": "Thiết bị",
|
||||
|
||||
Reference in New Issue
Block a user