User
add: language switch
8c88a89
Raw
History Blame Contribute Delete
38 kB
@page "/users"
@using Microsoft.AspNetCore.Authorization
@rendermode @(new InteractiveServerRenderMode(prerender: false))
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@inject ApiClientService ApiClient
@inject IJSRuntime JS
@using Microsoft.AspNetCore.Components.Authorization
@inject AuthenticationStateProvider AuthStateProvider
@inject MenuAuthorizationService MenuAuthorization
<PageTitle>@AppLocalization.PageTitle("users", "Users")</PageTitle>
<SuccessAlert Message="@successMessage" />
<div class="space-y-6">
<!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h2 class="text-3xl font-bold tracking-tight text-slate-900">@AppLocalization.PageTitle("users", "Users")</h2>
<p class="text-slate-500 mt-1">@AppLocalization.PageDescription("users", "Manage people, status, and account details.")</p>
</div>
@if (canCreateUser)
{
<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">
<i data-lucide="plus" class="h-4 w-4 mr-2"></i>
@AppLocalization.Text("common.newUser", "New User")
</button>
}
</div>
<!-- Search and Filters -->
<div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm mb-4">
<div class="flex flex-wrap items-end gap-4">
<div class="flex-1 min-w-[240px]">
<label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.searchUsers", "Search Users")</label>
<input @bind="searchInput" type="text" placeholder="@AppLocalization.Text("common.searchPlaceholder", "Search by name, email, username...")" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" />
</div>
<div>
<label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.role", "Role")</label>
<select @bind="filterRoleIdInput" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
<option value="0">@AppLocalization.Text("common.all", "All Roles")</option>
@foreach (var r in roles)
{
<option value="@r.Id">@r.Name</option>
}
</select>
</div>
<div>
<label class="block text-xs font-medium text-slate-500 mb-1.5">@AppLocalization.Text("common.status", "Status")</label>
<select @bind="filterStatusInput" class="rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all">
<option value="all">@AppLocalization.Text("common.all", "All")</option>
<option value="active">@AppLocalization.Text("status.active", "Active")</option>
<option value="inactive">@AppLocalization.Text("status.inactive", "Inactive")</option>
</select>
</div>
<button @onclick="ApplySearch" 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">
<i data-lucide="search" class="h-4 w-4 mr-2"></i>
@AppLocalization.Text("action.search", "Search")
</button>
<button @onclick="ResetSearch" class="inline-flex items-center rounded-lg border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 hover:bg-slate-50 transition-colors shadow-sm">
<i data-lucide="rotate-ccw" class="h-4 w-4 mr-2 text-slate-500"></i>
@AppLocalization.Text("action.reset", "Reset")
</button>
</div>
</div>
@if (isLoading)
{
<LoadingSpinner Message="@AppLocalization.Text("common.loadingUsers", "Loading users...")" />
}
else
{
<!-- Users Table -->
<div class="rounded-xl border border-slate-200 bg-white shadow-sm overflow-hidden">
<table class="w-full">
<thead>
<tr class="border-b border-slate-100 bg-slate-50/60">
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">No.</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.user", "User")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Email</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Phone</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.role", "Role")</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.status", "Status")</th>
<th class="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">@AppLocalization.Text("common.actions", "Actions")</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@if (PagedUsers.Any())
{
@foreach (var entry in PagedUsers.Select((user, index) => (user, index)))
{
var user = entry.user;
<tr class="hover:bg-slate-50/50 transition-colors">
<td class="px-6 py-4 text-sm font-semibold text-slate-500">@((currentPage - 1) * pageSize + entry.index + 1)</td>
<td class="px-6 py-4">
<div class="flex items-center space-x-3">
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-100 text-slate-700 font-semibold text-xs">
@(user.FirstName.Length > 0 ? user.FirstName[0].ToString().ToUpper() : "")@(user.LastName.Length > 0 ? user.LastName[0].ToString().ToUpper() : "")
</span>
<div>
<p class="text-sm font-semibold text-slate-900">@user.FirstName @user.LastName</p>
<p class="text-xs text-slate-400">@@@user.Username</p>
</div>
</div>
</td>
<td class="px-6 py-4 text-sm text-slate-600">@user.Email</td>
<td class="px-6 py-4 text-sm text-slate-500">@(user.Phone ?? "-")</td>
<td class="px-6 py-4">
@{ var role = roles.FirstOrDefault(r => r.Id == user.RoleId); }
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-50 text-indigo-700">
@(role?.Name ?? $"Role #{user.RoleId}")
</span>
</td>
<td class="px-6 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium @(user.IsActive ? "bg-emerald-50 text-emerald-700" : "bg-slate-100 text-slate-500")">
@(user.IsActive ? AppLocalization.Text("status.active", "Active") : AppLocalization.Text("status.inactive", "Inactive"))
</span>
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end space-x-2">
@if (canEditUser)
{
<button @onclick="() => OpenEditModal(user)" class="rounded-lg p-1.5 text-slate-400 hover:bg-amber-50 hover:text-amber-600 transition-all" title="@AppLocalization.Text("action.edit", "Edit")">
<i data-lucide="pencil" class="h-4 w-4"></i>
</button>
}
@if (canDeleteUser)
{
<button @onclick="() => RequestDeleteConfirmation(user.Id)" class="rounded-lg p-1.5 text-slate-400 hover:bg-rose-50 hover:text-rose-600 transition-all" title="@AppLocalization.Text("action.delete", "Delete")">
<i data-lucide="trash-2" class="h-4 w-4"></i>
</button>
}
</div>
</td>
</tr>
}
}
else
{
<EmptyState ColSpan="7" Icon="users" Title="@AppLocalization.Text("common.noResults", "No users found")" Subtitle="@AppLocalization.Text("common.createFirstEntry", "Try adjusting your search or create a new user.")" />
}
</tbody>
</table>
<!-- Pagination -->
<Pagination CurrentPage="currentPage"
TotalPages="TotalPages"
PageSize="pageSize"
TotalCount="TotalCount"
OnPageChanged="HandlePageChanged"
OnPageSizeChanged="HandlePageSizeChanged" />
</div>
}
</div>
@* ===== Create / Edit User Modal ===== *@
@if (showModal)
{
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div class="bg-white rounded-2xl shadow-lg w-full max-w-lg mx-4 overflow-hidden">
<div class="flex items-center justify-between px-6 py-4 border-b border-slate-100">
<h3 class="text-lg font-semibold text-slate-900">@(isEditing ? AppLocalization.Text("user.editUser", "Edit User") : AppLocalization.Text("common.newUser", "New User"))</h3>
<button @onclick="CloseModal" class="rounded-lg p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-600 transition-all">
<i data-lucide="x" class="h-5 w-5"></i>
</button>
</div>
<div class="p-6 space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("user.firstName", "First Name") <span class="text-red-500">*</span></label>
<div class="relative">
<input @bind="formFirstName" type="text" placeholder="@AppLocalization.Text("user.firstName", "First name")" class="w-full rounded-lg border @(isFirstNameError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
@if (isFirstNameError) { <svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> }
</div>
@if (isFirstNameError) { <p class="mt-1 text-xs text-red-500">@firstNameErrorMsg</p> }
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("user.lastName", "Last Name") <span class="text-red-500">*</span></label>
<div class="relative">
<input @bind="formLastName" type="text" placeholder="@AppLocalization.Text("user.lastName", "Last name")" class="w-full rounded-lg border @(isLastNameError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
@if (isLastNameError) { <svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> }
</div>
@if (isLastNameError) { <p class="mt-1 text-xs text-red-500">@lastNameErrorMsg</p> }
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.username", "Username") <span class="text-red-500">*</span></label>
<div class="relative">
<input @bind="formUsername" type="text" placeholder="@AppLocalization.Text("common.username", "username")" class="w-full rounded-lg border @(isUsernameError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
@if (isUsernameError) { <svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> }
</div>
@if (isUsernameError) { <p class="mt-1 text-xs text-red-500">@usernameErrorMsg</p> }
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">Email <span class="text-red-500">*</span></label>
<div class="relative">
<input @bind="formEmail" type="email" placeholder="user@example.com" class="w-full rounded-lg border @(isEmailError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
@if (isEmailError) { <svg class="absolute right-3 top-2.5 h-4 w-4 text-red-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> }
</div>
@if (isEmailError) { <p class="mt-1 text-xs text-red-500">@emailErrorMsg</p> }
</div>
@if (!isEditing)
{
<div class="relative">
<label class="block text-sm font-medium text-slate-700 mb-1.5">Password <span class="text-red-500">*</span></label>
<input @bind="formPassword" type="@(showPassword ? "text" : "password")" placeholder="@AppLocalization.Text("validation.passwordMinLengthRule", "Min 8 characters (A-Z, a-z, 0-9, special)")" class="w-full rounded-lg border @(isPasswordError ? "border-red-500 focus:ring-red-500" : "border-slate-200 focus:ring-slate-900") px-3 py-2 pr-10 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:border-transparent transition-all" />
<button type="button" @onclick="TogglePassword" class="absolute right-3 top-[34px] text-slate-400 hover:text-slate-600 focus:outline-none">
@if (showPassword)
{
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"></path><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"></path><path d="M6.61 6.61A13.52 13.52 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"></path><line x1="2" y1="2" x2="22" y2="22"></line></svg>
}
else
{
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></svg>
}
</button>
@if (isPasswordError) { <p class="mt-1 text-xs text-red-500">@passwordErrorMsg</p> }
</div>
}
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.phone", "Phone")</label>
<input @bind="formPhone" type="text" placeholder="@AppLocalization.Text("common.phoneNumber", "Phone number")" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all" />
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1.5">@AppLocalization.Text("common.role", "Role")</label>
<select @bind="formRoleId" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent transition-all">
@foreach (var r in roles)
{
<option value="@r.Id">@r.Name</option>
}
</select>
</div>
</div>
@if (isEditing)
{
<div class="flex items-center space-x-3">
<input @bind="formIsActive" type="checkbox" id="isActiveToggle" class="h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-900" />
<label for="isActiveToggle" class="text-sm font-medium text-slate-700">@AppLocalization.Text("status.active", "Active")</label>
</div>
}
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="rounded-lg bg-rose-50 p-3 border border-rose-200">
<p class="text-xs text-rose-600 font-medium flex items-center gap-1.5">
<svg class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
@errorMessage
</p>
</div>
}
</div>
<div class="flex items-center justify-end space-x-3 px-6 py-4 border-t border-slate-100 bg-slate-50/50">
<button @onclick="CloseModal" class="rounded-lg px-4 py-2 text-sm font-medium text-slate-600 hover:bg-slate-100 transition-colors">@AppLocalization.Text("common.cancel", "Cancel")</button>
<button @onclick="RequestSaveConfirmation" disabled="@isSaving" class="rounded-lg bg-violet-600 px-4 py-2 text-sm font-medium text-white hover:bg-violet-700 disabled:opacity-50 transition-colors">
@(isSaving ? AppLocalization.Text("common.saving", "Saving...") : (isEditing ? AppLocalization.Text("action.update", "Update") : AppLocalization.Text("action.create", "Create")))
</button>
</div>
</div>
</div>
}
@* ===== Confirm Dialog ===== *@
<ConfirmDialog IsVisible="showConfirmDialog"
Title="@confirmTitle"
Message="@confirmMessage"
ConfirmText="@confirmButtonText"
Icon="@confirmIcon"
IconBgClass="@confirmIconBg"
IconTextClass="@confirmIconText"
ButtonClass="@confirmButtonClass"
OnConfirm="HandleConfirmAction"
OnCancel="CloseConfirmDialog" />
@code {
private bool isLoading = true;
private bool showModal = false;
private bool isEditing = false;
private bool isSaving = false;
private bool canCreateUser = false;
private bool canEditUser = false;
private bool canDeleteUser = false;
private string? errorMessage;
// Per-field validation errors
private bool isFirstNameError = false; private string firstNameErrorMsg = "";
private bool isLastNameError = false; private string lastNameErrorMsg = "";
private bool isUsernameError = false; private string usernameErrorMsg = "";
private bool isEmailError = false; private string emailErrorMsg = "";
private bool isPasswordError = false; private string passwordErrorMsg = "";
// Confirm dialog state
private bool showConfirmDialog = false;
private string confirmTitle = "";
private string confirmMessage = "";
private string confirmButtonText = "";
private string confirmIcon = "";
private string confirmIconBg = "";
private string confirmIconText = "";
private string confirmButtonClass = "";
private Func<Task>? confirmAction;
private List<UserDto> users = new();
private List<RoleDto> roles = new();
private string? successMessage;
private bool showPassword = false;
// Search / filter
private string searchInput = "";
private long filterRoleIdInput = 0;
private string filterStatusInput = "all";
// 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<UserDto> PagedUsers => users;
// Form fields
private long editingId;
private string formUsername = "";
private string formFirstName = "";
private string formLastName = "";
private string formEmail = "";
private string formPassword = "";
private string? formPhone;
private long formRoleId;
private bool formIsActive = true;
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
canCreateUser = await MenuAuthorization.HasAccessCodeAsync(user, "Users_Create");
canEditUser = await MenuAuthorization.HasAccessCodeAsync(user, "Users_Update");
canDeleteUser = await MenuAuthorization.HasAccessCodeAsync(user, "Users_Delete");
var client = ApiClient.CreateClient();
try
{
var usersTask = LoadUsersPageAsync(client);
var rolesTask = client.GetFromJsonAsync<List<RoleDto>>("Role", Serialization.CaseInsensitive);
await System.Threading.Tasks.Task.WhenAll(usersTask, rolesTask);
users = usersTask.Result?.Items ?? new();
totalCount = usersTask.Result?.TotalCount ?? 0;
totalPages = usersTask.Result?.TotalPages ?? 0;
currentPage = usersTask.Result?.Page ?? currentPage;
pageSize = usersTask.Result?.PageSize ?? pageSize;
roles = rolesTask.Result ?? new();
}
catch (Exception ex)
{
Console.WriteLine($"Load error: {ex.Message}");
}
finally
{
isLoading = false;
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await JS.InvokeVoidAsync("initIcons");
}
private async Task<PagedResult<UserDto>?> LoadUsersPageAsync(HttpClient? client = null)
{
client ??= ApiClient.CreateClient();
return await client.GetFromJsonAsync<PagedResult<UserDto>>(BuildUsersUrl(), Serialization.CaseInsensitive);
}
// Pagination handlers
private async Task HandlePageChanged(int page)
{
currentPage = page;
await LoadUsers();
}
private async Task HandlePageSizeChanged(int size)
{
pageSize = size;
currentPage = 1;
await LoadUsers();
}
private async Task ApplySearch()
{
currentPage = 1;
await LoadUsers();
}
private async Task ResetSearch()
{
searchInput = "";
filterRoleIdInput = 0;
filterStatusInput = "all";
currentPage = 1;
await LoadUsers();
}
private async Task LoadUsers()
{
var client = ApiClient.CreateClient();
var result = await LoadUsersPageAsync(client);
users = result?.Items ?? new();
totalCount = result?.TotalCount ?? 0;
totalPages = result?.TotalPages ?? 0;
currentPage = result?.Page ?? currentPage;
pageSize = result?.PageSize ?? pageSize;
}
private string BuildUsersUrl()
{
var query = new List<string>
{
$"page={currentPage}",
$"limit={pageSize}"
};
if (!string.IsNullOrWhiteSpace(searchInput))
{
query.Add($"search={Uri.EscapeDataString(searchInput.Trim())}");
}
if (filterRoleIdInput > 0)
{
query.Add($"roleId={filterRoleIdInput}");
}
if (!string.Equals(filterStatusInput, "all", StringComparison.OrdinalIgnoreCase))
{
query.Add($"status={Uri.EscapeDataString(filterStatusInput)}");
}
return $"User?{string.Join("&", query)}";
}
private void OpenCreateModal()
{
isEditing = false;
editingId = 0;
formUsername = "";
formFirstName = "";
formLastName = "";
formEmail = "";
formPassword = "";
formPhone = null;
formRoleId = roles.FirstOrDefault()?.Id ?? 0;
formIsActive = true;
errorMessage = null;
isFirstNameError = false; firstNameErrorMsg = "";
isLastNameError = false; lastNameErrorMsg = "";
isUsernameError = false; usernameErrorMsg = "";
isEmailError = false; emailErrorMsg = "";
isPasswordError = false; passwordErrorMsg = "";
showModal = true;
}
private void OpenEditModal(UserDto user)
{
isEditing = true;
editingId = user.Id;
formUsername = user.Username;
formFirstName = user.FirstName;
formLastName = user.LastName;
formEmail = user.Email;
formPhone = user.Phone;
formRoleId = user.RoleId;
formIsActive = user.IsActive;
errorMessage = null;
isFirstNameError = false; firstNameErrorMsg = "";
isLastNameError = false; lastNameErrorMsg = "";
isUsernameError = false; usernameErrorMsg = "";
isEmailError = false; emailErrorMsg = "";
isPasswordError = false; passwordErrorMsg = "";
showModal = true;
}
private void CloseModal()
{
showModal = false;
errorMessage = null;
isFirstNameError = false; isLastNameError = false; isUsernameError = false;
isEmailError = false; isPasswordError = false;
}
private void RequestSaveConfirmation()
{
isFirstNameError = false; firstNameErrorMsg = "";
isLastNameError = false; lastNameErrorMsg = "";
isUsernameError = false; usernameErrorMsg = "";
isEmailError = false; emailErrorMsg = "";
isPasswordError = false; passwordErrorMsg = "";
bool hasError = false;
if (string.IsNullOrWhiteSpace(formFirstName)) { isFirstNameError = true; firstNameErrorMsg = AppLocalization.Text("validation.firstNameRequired", "First name is required."); hasError = true; }
if (string.IsNullOrWhiteSpace(formLastName)) { isLastNameError = true; lastNameErrorMsg = AppLocalization.Text("validation.lastNameRequired", "Last name is required."); hasError = true; }
if (string.IsNullOrWhiteSpace(formUsername)) { isUsernameError = true; usernameErrorMsg = AppLocalization.Text("validation.usernameRequired", "Username is required."); hasError = true; }
else if (formUsername.Length < 3) { isUsernameError = true; usernameErrorMsg = ResultMessages.UsernameMinLength; hasError = true; }
else if (!System.Text.RegularExpressions.Regex.IsMatch(formUsername, @"^[a-zA-Z0-9._]+$")) { isUsernameError = true; usernameErrorMsg = ResultMessages.UsernameInvalidCharacters; hasError = true; }
if (string.IsNullOrWhiteSpace(formEmail)) { isEmailError = true; emailErrorMsg = AppLocalization.Text("validation.emailRequired", "Email is required."); hasError = true; }
if (!isEditing)
{
if (string.IsNullOrWhiteSpace(formPassword))
{
isPasswordError = true;
passwordErrorMsg = AppLocalization.Text("validation.passwordRequired", "Password is required.");
hasError = true;
}
else if (formPassword.Length < 8)
{
isPasswordError = true;
passwordErrorMsg = ResultMessages.PasswordMinLengthRule;
hasError = true;
}
else if (!System.Text.RegularExpressions.Regex.IsMatch(formPassword, @"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9]).+$"))
{
isPasswordError = true;
passwordErrorMsg = ResultMessages.PasswordComplexityRule;
hasError = true;
}
}
if (hasError) return;
if (isEditing)
{
confirmTitle = AppLocalization.Text("user.updateUser", "Update User?");
confirmMessage = AppLocalization.Text("user.updateUserConfirm", "Are you sure you want to update this user?");
confirmButtonText = AppLocalization.Text("action.update", "Update");
confirmIcon = "pencil";
confirmIconBg = "bg-amber-50";
confirmIconText = "text-amber-500";
confirmButtonClass = "bg-violet-600 hover:bg-violet-700";
}
else
{
confirmTitle = AppLocalization.Text("user.createUser", "Create User?");
confirmMessage = AppLocalization.Text("user.createUserConfirm", "Are you sure you want to create this user?");
confirmButtonText = AppLocalization.Text("action.create", "Create");
confirmIcon = "plus-circle";
confirmIconBg = "bg-emerald-50";
confirmIconText = "text-emerald-500";
confirmButtonClass = "bg-violet-600 hover:bg-violet-700";
}
confirmAction = SaveUser;
showConfirmDialog = true;
}
private void RequestDeleteConfirmation(long id)
{
confirmTitle = AppLocalization.Text("user.deleteUser", "Delete User?");
confirmMessage = AppLocalization.Text("user.deleteUserConfirm", "Are you sure you want to delete this user? This action cannot be undone.");
confirmButtonText = AppLocalization.Text("action.delete", "Delete");
confirmIcon = "trash-2";
confirmIconBg = "bg-rose-50";
confirmIconText = "text-rose-500";
confirmButtonClass = "bg-violet-600 hover:bg-violet-700";
confirmAction = () => DeleteUser(id);
showConfirmDialog = true;
}
private async Task HandleConfirmAction()
{
showConfirmDialog = false;
if (confirmAction != null)
await confirmAction();
}
private void CloseConfirmDialog()
{
showConfirmDialog = false;
}
private async Task SaveUser()
{
if (string.IsNullOrWhiteSpace(formUsername) || string.IsNullOrWhiteSpace(formFirstName) || string.IsNullOrWhiteSpace(formLastName) || string.IsNullOrWhiteSpace(formEmail))
{
errorMessage = ResultMessages.FillAllFields;
return;
}
isSaving = true;
errorMessage = null;
var client = ApiClient.CreateClient();
try
{
if (isEditing)
{
var dto = new UpdateUserDto
{
Username = formUsername,
FirstName = formFirstName,
LastName = formLastName,
Phone = formPhone,
RoleId = formRoleId,
IsActive = formIsActive
};
var response = await client.PutAsJsonAsync($"User/{editingId}", dto);
var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
if (res == null || !res.IsSuccess)
{
errorMessage = res?.ErrorMessage ?? ResultMessages.FailedToUpdateUser;
return;
}
}
else
{
if (string.IsNullOrWhiteSpace(formPassword))
{
errorMessage = AppLocalization.Text("validation.passwordRequired", "Password is required.");
isSaving = false;
return;
}
if (formPassword.Length < 8)
{
errorMessage = ResultMessages.PasswordMinLengthRule;
isSaving = false;
return;
}
if (!System.Text.RegularExpressions.Regex.IsMatch(formPassword, @"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9]).+$"))
{
errorMessage = ResultMessages.PasswordComplexityRule;
isSaving = false;
return;
}
var dto = new CreateUserDto
{
Username = formUsername,
FirstName = formFirstName,
LastName = formLastName,
Email = formEmail,
Password = formPassword,
Phone = formPhone,
RoleId = formRoleId
};
var response = await client.PostAsJsonAsync("User", dto);
var res = await response.Content.ReadFromJsonAsync<Result<UserDto>>(Serialization.CaseInsensitive);
if (res == null || !res.IsSuccess)
{
errorMessage = res?.ErrorMessage ?? ResultMessages.FailedToCreateUser;
return;
}
}
CloseModal();
await LoadUsers();
successMessage = isEditing ? AppLocalization.Text("result.userUpdated", "User updated successfully!") : AppLocalization.Text("result.userCreated", "User created successfully!");
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
isSaving = false;
}
}
private async Task DeleteUser(long id)
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var currentUserIdStr = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (currentUserIdStr != null && long.TryParse(currentUserIdStr, out var currentUserId) && currentUserId == id)
{
await JS.InvokeVoidAsync("alert", AppLocalization.Text("user.cannotDeleteSelf", "You cannot delete your own account."));
return;
}
var client = ApiClient.CreateClient();
try
{
var response = await client.DeleteAsync($"User/{id}");
var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
if (res == null || !res.IsSuccess)
{
await JS.InvokeVoidAsync("alert", res?.ErrorMessage ?? AppLocalization.Text("result.failedToDeleteUser", "Failed to delete user."));
return;
}
await LoadUsers();
if (currentPage > TotalPages && TotalPages > 0)
currentPage = TotalPages;
successMessage = AppLocalization.Text("result.userDeleted", "User deleted successfully!");
}
catch (Exception ex)
{
Console.WriteLine($"Delete error: {ex.Message}");
}
}
private async Task TogglePassword()
{
showPassword = !showPassword;
await JS.InvokeVoidAsync("initIcons");
}
}