Yuyuqt
add: user balance
ee79726
Raw
History Blame Contribute Delete
38.1 kB
@page "/Members"
@using BlazorFrontend.Services
@using LibraryManagement.Shared.Models
@using BlazorFrontend.Models
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "Librarian")]
@inject LibraryApiClient ApiClient
@inject IJSRuntime JSRuntime
@inject NavigationManager Navigation
<PageTitle>Members Directory</PageTitle>
@if (_currentView == ViewMode.List)
{
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 animate-in fade-in slide-in-from-top-4 duration-500">
<div>
<h1 class="text-3xl font-bold tracking-tight text-mystic-900">Members Directory</h1>
<p class="mt-2 text-mystic-500">
Manage library members and administrator accounts.
<span class="ml-2 text-xs text-mystic-400">(@(_allUsers?.Count() ?? 0) total)</span>
</p>
</div>
<div class="flex items-center gap-3">
<button @onclick="ShowCreate" class="btn-primary">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
Add New Member
</button>
</div>
</div>
@if (_isLoading)
{
<div class="flex items-center justify-center min-h-[400px]">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-mystic-900"></div>
</div>
}
else if (_pagedResult != null)
{
<div class="card overflow-hidden animate-in fade-in duration-700">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-mystic-200">
<thead class="bg-mystic-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-mystic-500 uppercase tracking-wider">Member</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-mystic-500 uppercase tracking-wider">Contact</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-mystic-500 uppercase tracking-wider">Role</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-mystic-500 uppercase tracking-wider">Status</th>
<th scope="col" class="relative px-6 py-3"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-mystic-200">
@foreach (var user in _pagedResult.Items)
{
<tr class="hover:bg-mystic-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="h-10 w-10 flex-shrink-0 rounded-full bg-mystic-900 flex items-center justify-center text-white font-bold text-sm">
@(user.FullName?.Substring(0, Math.Min(2, user.FullName.Length)).ToUpper() ?? "U")
</div>
<div class="ml-4">
<div class="text-sm font-medium text-mystic-900">
<button @onclick="() => ShowDetails(user.Id)" class="hover:underline text-left">@user.FullName</button>
</div>
@if (!string.IsNullOrEmpty(user.StudentId))
{
<div class="text-xs text-mystic-500">ID: @user.StudentId</div>
}
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-mystic-900">@user.Email</div>
<div class="text-xs text-mystic-500">@(string.IsNullOrEmpty(user.PhoneNumber) ? "No phone" : user.PhoneNumber)</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5 @(user.Role == "Admin" ? "bg-amber-100 text-amber-800" : "bg-blue-100 text-blue-800")">
@user.Role
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@if (user.BanStatus == true)
{
<span class="inline-flex rounded-full bg-rose-100 px-2 text-xs font-semibold leading-5 text-rose-800">
Suspended
</span>
}
else if (user.IsActive)
{
<span class="inline-flex rounded-full bg-emerald-100 px-2 text-xs font-semibold leading-5 text-emerald-800">
Active
</span>
}
else
{
<span class="inline-flex rounded-full bg-mystic-100 px-2 text-xs font-semibold leading-5 text-mystic-800">
Inactive
</span>
}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end gap-3">
<button @onclick="() => ShowDetails(user.Id)" class="text-mystic-600 hover:text-mystic-900 transition-colors">Details</button>
<button @onclick="() => ShowEdit(user.Id)" class="text-indigo-600 hover:text-indigo-900 transition-colors">Edit</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
@* Pagination *@
@if (_pagedResult.TotalPages > 1)
{
<div class="px-6 py-4 border-t border-mystic-200 flex items-center justify-between bg-white">
<p class="text-sm text-mystic-500">
Showing <span class="font-medium text-mystic-700">@((_pagedResult.CurrentPage - 1) * _pagedResult.PageSize + 1)</span>
<span class="font-medium text-mystic-700">@(Math.Min(_pagedResult.CurrentPage * _pagedResult.PageSize, _pagedResult.TotalCount))</span>
of <span class="font-medium text-mystic-700">@_pagedResult.TotalCount</span> members
</p>
<nav class="flex items-center gap-1" aria-label="Pagination">
@* Previous *@
<button @onclick="() => ChangePage(_pagedResult.CurrentPage - 1)" disabled="@(!_pagedResult.HasPreviousPage)"
class="inline-flex items-center px-3 py-1.5 rounded-md border @(_pagedResult.HasPreviousPage ? "border-mystic-200 text-mystic-600 hover:bg-mystic-50 hover:text-mystic-900" : "border-mystic-100 text-mystic-300 cursor-not-allowed") text-sm font-medium transition-colors">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
Prev
</button>
@* Page numbers *@
@for (int p = 1; p <= _pagedResult.TotalPages; p++)
{
var pageNumber = p;
if (p == _pagedResult.CurrentPage)
{
<span class="inline-flex items-center justify-center w-8 h-8 rounded-md bg-mystic-900 text-white text-sm font-semibold">@p</span>
}
else if (p == 1 || p == _pagedResult.TotalPages || (p >= _pagedResult.CurrentPage - 2 && p <= _pagedResult.CurrentPage + 2))
{
<button @onclick="() => ChangePage(pageNumber)"
class="inline-flex items-center justify-center w-8 h-8 rounded-md border border-mystic-200 text-sm font-medium text-mystic-600 hover:bg-mystic-50 hover:text-mystic-900 transition-colors">@p</button>
}
else if (p == _pagedResult.CurrentPage - 3 || p == _pagedResult.CurrentPage + 3)
{
<span class="inline-flex items-center justify-center w-8 h-8 text-sm text-mystic-400"></span>
}
}
@* Next *@
<button @onclick="() => ChangePage(_pagedResult.CurrentPage + 1)" disabled="@(!_pagedResult.HasNextPage)"
class="inline-flex items-center px-3 py-1.5 rounded-md border @(_pagedResult.HasNextPage ? "border-mystic-200 text-mystic-600 hover:bg-mystic-50 hover:text-mystic-900" : "border-mystic-100 text-mystic-300 cursor-not-allowed") text-sm font-medium transition-colors">
Next
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</nav>
</div>
}
</div>
}
}
else if (_currentView == ViewMode.Details && _selectedUser != null)
{
<div class="animate-in fade-in slide-in-from-left-4 duration-500">
<button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-mystic-500 hover:text-mystic-900 mb-6 group">
<svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
Back to Directory
</button>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
<div class="lg:col-span-1">
<div class="aspect-square w-full rounded-2xl bg-mystic-900 flex flex-col items-center justify-center text-white p-6 shadow-xl">
<div class="text-6xl font-black mb-4 tracking-tighter">
@(_selectedUser.User.FullName?.Substring(0, Math.Min(2, _selectedUser.User.FullName.Length)).ToUpper() ?? "U")
</div>
<h2 class="text-xl font-bold text-center">@_selectedUser.User.FullName</h2>
<div class="mt-2 inline-flex rounded-full px-3 py-1 text-xs font-semibold leading-5 @(_selectedUser.User.Role == "Admin" ? "bg-amber-500/20 text-amber-200" : "bg-blue-500/20 text-blue-200")">
@_selectedUser.User.Role
</div>
</div>
</div>
<div class="lg:col-span-2 space-y-8">
<div>
<h1 class="text-4xl font-extrabold text-mystic-900 tracking-tight mb-2">Member Profile</h1>
<div class="flex items-center gap-3">
@if (_selectedUser.User.BanStatus == true)
{
<span class="inline-flex items-center rounded-full bg-rose-100 px-3 py-1 text-xs font-semibold text-rose-800 tracking-wide uppercase">Suspended</span>
}
else if (_selectedUser.User.IsActive)
{
<span class="inline-flex items-center rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-800 tracking-wide uppercase">Active</span>
}
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-8 border-y border-mystic-100 py-6">
<div>
<h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">Email</h4>
<p class="text-sm font-semibold text-mystic-900">@_selectedUser.User.Email</p>
</div>
<div>
<h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">Phone</h4>
<p class="text-sm font-semibold text-mystic-900">@(string.IsNullOrEmpty(_selectedUser.User.PhoneNumber) ? "-" : _selectedUser.User.PhoneNumber)</p>
</div>
<div>
<h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">Student ID</h4>
<p class="text-sm font-semibold text-mystic-900">@(string.IsNullOrEmpty(_selectedUser.User.StudentId) ? "-" : _selectedUser.User.StudentId)</p>
</div>
<div>
<h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">Joined</h4>
<p class="text-sm font-semibold text-mystic-900">@_selectedUser.User.CreatedAt.ToString("MMM dd, yyyy")</p>
</div>
<div class="sm:col-span-2">
<h4 class="text-[10px] font-bold text-mystic-400 uppercase tracking-widest mb-1">Address</h4>
<p class="text-sm font-semibold text-mystic-900">@(string.IsNullOrEmpty(_selectedUser.User.Address) ? "-" : _selectedUser.User.Address)</p>
</div>
</div>
<div class="flex items-center gap-4 pt-4 flex-wrap">
<button @onclick="() => ShowEdit(_selectedUser.User.Id)" class="btn-primary py-3 px-8 text-base shadow-lg shadow-mystic-200">Edit Details</button>
<button @onclick="UpdateRole" class="btn-secondary py-3 px-8 text-base">
Make @(_selectedUser.User.Role == "Admin" ? "Member" : "Admin")
</button>
<button @onclick="DeleteUser" class="p-3 text-rose-600 hover:text-rose-700 hover:bg-rose-50 rounded-xl transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</button>
</div>
<!-- Wallet Management Section -->
<div class="mt-8 pt-8 border-t border-mystic-100">
<div class="flex items-center justify-between mb-6">
<h3 class="text-2xl font-bold text-mystic-900">Library Wallet</h3>
<div class="bg-indigo-50 border border-indigo-100 px-4 py-2 rounded-xl">
<span class="text-xs font-bold text-indigo-400 uppercase tracking-widest block">Current Balance</span>
<span class="text-xl font-black text-indigo-900">@_selectedUser.User.Balance.ToString("N0") MMK</span>
</div>
</div>
<div class="bg-white border border-mystic-200 rounded-2xl p-6 shadow-sm">
<h4 class="text-sm font-bold text-mystic-700 mb-4">Add Credits (Cash Deposit)</h4>
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex-1">
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="text-mystic-400 text-sm font-bold">MMK</span>
</div>
<input type="number" @bind="_topUpAmount" class="block w-full pl-12 pr-3 py-3 border border-mystic-300 rounded-xl focus:ring-mystic-900 focus:border-mystic-900 sm:text-sm" placeholder="Amount (e.g. 5000)" />
</div>
</div>
<button @onclick="TopUpWallet" class="btn-primary px-8 whitespace-nowrap" disabled="@(_topUpAmount <= 0)">
Confirm Deposit
</button>
</div>
<p class="mt-3 text-xs text-mystic-400 italic">This will instantly add credits to the user's library wallet.</p>
</div>
</div>
<!-- Subscription Management -->
<div class="mt-8 pt-8 border-t border-mystic-100">
<h3 class="text-2xl font-bold text-mystic-900 mb-6">Manage Subscription</h3>
@if (_selectedUser.CurrentSubscription != null && _selectedUser.CurrentSubscription.IsActive)
{
<div class="bg-green-50 border border-green-200 rounded-xl p-4 mb-6">
<div class="flex justify-between items-center">
<div>
<span class="text-sm text-green-800 font-semibold uppercase tracking-wider">Active Plan</span>
<h4 class="text-xl font-bold text-green-900">@_selectedUser.CurrentSubscription.MembershipType</h4>
</div>
<div class="text-right">
<div class="text-xs text-green-700">Expires On</div>
<div class="font-bold text-green-900">@_selectedUser.CurrentSubscription.ExpiryDate.ToString("MMM dd, yyyy")</div>
</div>
</div>
</div>
}
else
{
<div class="bg-mystic-50 border border-mystic-200 rounded-xl p-4 mb-6">
<p class="text-mystic-600 font-medium">No active subscription for this member.</p>
</div>
}
<h4 class="text-lg font-semibold text-mystic-900 mb-4">Assign New Plan</h4>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
@foreach (var plan in _selectedUser.AvailableMemberships)
{
var isCurrent = _selectedUser.CurrentSubscription?.IsActive == true && _selectedUser.CurrentSubscription.MembershipId == plan.Id;
<div class="card p-4 relative @(isCurrent ? "ring-2 ring-mystic-900 border-mystic-900" : "")">
@if (isCurrent)
{
<div class="absolute top-0 right-0 -mt-2 -mr-2 bg-mystic-900 text-white text-xs font-bold px-2 py-1 rounded-md">Current</div>
}
<h5 class="font-bold text-mystic-900 text-lg mb-1">@plan.Type</h5>
<p class="text-mystic-500 font-medium text-sm mb-4">@plan.Price.ToString("N0") MMK / @plan.DurationMonths mo</p>
@if (!isCurrent)
{
<button @onclick="() => AssignSubscription(plan.Id, plan.Type)" class="w-full btn-secondary text-sm">Assign Plan</button>
}
else
{
<button disabled class="w-full btn-secondary text-sm opacity-50 cursor-not-allowed">Current Plan</button>
}
</div>
}
</div>
</div>
</div>
</div>
</div>
}
else if (_currentView == ViewMode.Create || _currentView == ViewMode.Edit)
{
<div class="animate-in fade-in slide-in-from-bottom-4 duration-500">
<button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-mystic-500 hover:text-mystic-900 mb-6 group">
<svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
Back to Directory
</button>
<div class="max-w-3xl mx-auto">
<div class="mb-8">
<h1 class="text-3xl font-bold tracking-tight text-mystic-900">@(_currentView == ViewMode.Create ? "Add New Member" : "Edit Member Details")</h1>
<p class="mt-2 text-mystic-500">@(_currentView == ViewMode.Create ? "Register a new member to the library system." : $"Update details for {_editUser?.FullName}.")</p>
</div>
<div class="card p-6 sm:p-8">
@if (_currentView == ViewMode.Create)
{
<EditForm Model="@_createRequest" OnValidSubmit="HandleCreate" class="space-y-6">
<DataAnnotationsValidator />
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-mystic-700">Full Name <span class="text-rose-500">*</span></label>
<div class="mt-1">
<InputText @bind-Value="_createRequest.FullName" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="John Doe" />
<ValidationMessage For="@(() => _createRequest.FullName)" class="mt-1 text-xs text-rose-500" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-mystic-700">Email Address <span class="text-rose-500">*</span></label>
<div class="mt-1">
<InputText @bind-Value="_createRequest.Email" type="email" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="you@domain.com" />
<ValidationMessage For="@(() => _createRequest.Email)" class="mt-1 text-xs text-rose-500" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-mystic-700">Password <span class="text-rose-500">*</span></label>
<div class="mt-1">
<InputText @bind-Value="_createRequest.Password" type="password" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" />
<ValidationMessage For="@(() => _createRequest.Password)" class="mt-1 text-xs text-rose-500" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-mystic-700">Phone Number</label>
<div class="mt-1">
<InputText @bind-Value="_createRequest.PhoneNumber" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="+123456789" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-mystic-700">Student ID</label>
<div class="mt-1">
<InputText @bind-Value="_createRequest.StudentId" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="Optional" />
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-mystic-700">Address</label>
<div class="mt-1">
<InputTextArea @bind-Value="_createRequest.Address" rows="3" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" placeholder="Full address details" />
</div>
</div>
</div>
<div class="pt-5 border-t border-mystic-100 flex justify-end gap-3">
<button type="button" @onclick="BackToList" class="btn-secondary">Cancel</button>
<button type="submit" class="btn-primary">Create Member</button>
</div>
</EditForm>
}
else if (_currentView == ViewMode.Edit)
{
<EditForm Model="@_updateRequest" OnValidSubmit="HandleUpdate" class="space-y-6">
<DataAnnotationsValidator />
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-mystic-700">Full Name <span class="text-rose-500">*</span></label>
<div class="mt-1">
<InputText @bind-Value="_updateRequest.FullName" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" />
<ValidationMessage For="@(() => _updateRequest.FullName)" class="mt-1 text-xs text-rose-500" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-mystic-700">Phone Number</label>
<div class="mt-1">
<InputText @bind-Value="_updateRequest.PhoneNumber" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" />
</div>
</div>
<div>
<label class="block text-sm font-medium text-mystic-700">Student ID</label>
<div class="mt-1">
<InputText @bind-Value="_updateRequest.StudentId" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" />
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium text-mystic-700">Address</label>
<div class="mt-1">
<InputTextArea @bind-Value="_updateRequest.Address" rows="3" class="block w-full rounded-md border-mystic-300 shadow-sm focus:border-mystic-900 focus:ring-mystic-900 sm:text-sm px-3 py-2 border bg-white" />
</div>
</div>
</div>
<div class="pt-5 border-t border-mystic-100 flex justify-end gap-3">
<button type="button" @onclick="BackToList" class="btn-secondary">Cancel</button>
<button type="submit" class="btn-primary">Update Member</button>
</div>
</EditForm>
}
</div>
</div>
</div>
}
@code {
private enum ViewMode { List, Details, Create, Edit }
private ViewMode _currentView = ViewMode.List;
private IEnumerable<UserDto>? _allUsers;
private PagedResult<UserDto>? _pagedResult;
private bool _isLoading = true;
private const int PageSize = 10;
private MemberDetailsViewModel? _selectedUser;
private UserCreateRequest _createRequest = new();
private UserUpdateRequest _updateRequest = new();
private UserDto? _editUser;
private decimal _topUpAmount;
protected override async Task OnInitializedAsync()
{
await LoadUsers();
}
private async Task LoadUsers()
{
_isLoading = true;
try
{
_allUsers = await ApiClient.GetUsersAsync();
ApplyPagination(1);
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("showToast", "Error loading users: " + ex.Message, "error");
}
finally
{
_isLoading = false;
}
}
private void ApplyPagination(int page)
{
if (_allUsers == null) return;
var totalCount = _allUsers.Count();
var totalPages = (int)Math.Ceiling(totalCount / (double)PageSize);
page = Math.Max(1, Math.Min(page, Math.Max(1, totalPages)));
_pagedResult = new PagedResult<UserDto>
{
Items = _allUsers.Skip((page - 1) * PageSize).Take(PageSize),
CurrentPage = page,
TotalPages = totalPages,
TotalCount = totalCount,
PageSize = PageSize
};
}
private void ChangePage(int page)
{
ApplyPagination(page);
}
private async Task ShowDetails(Guid userId)
{
_isLoading = true;
try
{
var user = await ApiClient.GetUserAsync(userId);
if (user == null) return;
var memberships = await ApiClient.GetMembershipsAsync();
var subscription = await ApiClient.GetUserSubscriptionAsync(userId);
_selectedUser = new MemberDetailsViewModel
{
User = user,
AvailableMemberships = memberships.ToList(),
CurrentSubscription = subscription
};
_currentView = ViewMode.Details;
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("showToast", "Error loading member details: " + ex.Message, "error");
}
finally
{
_isLoading = false;
}
}
private void ShowCreate()
{
_createRequest = new();
_currentView = ViewMode.Create;
}
private async Task ShowEdit(Guid userId)
{
_isLoading = true;
try
{
_editUser = _allUsers?.FirstOrDefault(u => u.Id == userId);
if (_editUser == null)
{
_editUser = await ApiClient.GetUserAsync(userId);
}
if (_editUser != null)
{
_updateRequest = new UserUpdateRequest
{
FullName = _editUser.FullName,
PhoneNumber = _editUser.PhoneNumber,
StudentId = _editUser.StudentId,
Address = _editUser.Address
};
_currentView = ViewMode.Edit;
}
}
finally
{
_isLoading = false;
}
}
private void BackToList()
{
_currentView = ViewMode.List;
_selectedUser = null;
_editUser = null;
}
private async Task HandleCreate()
{
try
{
var result = await ApiClient.CreateUserAsync(_createRequest);
if (result != null)
{
await JSRuntime.InvokeVoidAsync("showToast", "Member created successfully!", "success");
await LoadUsers();
BackToList();
}
else
{
await JSRuntime.InvokeVoidAsync("showToast", "Failed to create member.", "error");
}
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("showToast", "Error: " + ex.Message, "error");
}
}
private async Task HandleUpdate()
{
if (_editUser == null) return;
try
{
var result = await ApiClient.UpdateUserAsync(_editUser.Id, _updateRequest);
if (result != null)
{
await JSRuntime.InvokeVoidAsync("showToast", "Member updated successfully!", "success");
await LoadUsers();
BackToList();
}
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("showToast", "Error: " + ex.Message, "error");
}
}
private async Task DeleteUser()
{
if (_selectedUser == null) return;
bool confirmed = await JSRuntime.InvokeAsync<bool>("confirm", "Are you sure you want to completely remove this member? This action cannot be undone.");
if (!confirmed) return;
try
{
var success = await ApiClient.DeleteUserAsync(_selectedUser.User.Id);
if (success)
{
await JSRuntime.InvokeVoidAsync("showToast", "Member removed.", "success");
await LoadUsers();
BackToList();
}
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("showToast", "Error: " + ex.Message, "error");
}
}
private async Task UpdateRole()
{
if (_selectedUser == null) return;
var newRole = _selectedUser.User.Role == "Admin" ? "Member" : "Admin";
try
{
var success = await ApiClient.UpdateUserRoleAsync(_selectedUser.User.Id, newRole);
if (success)
{
await JSRuntime.InvokeVoidAsync("showToast", $"Role updated to {newRole}.", "success");
await ShowDetails(_selectedUser.User.Id); // Refresh details
}
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("showToast", "Error: " + ex.Message, "error");
}
}
private async Task AssignSubscription(int membershipId, string planName)
{
if (_selectedUser == null) return;
bool confirmed = await JSRuntime.InvokeAsync<bool>("confirm", $"Assign {planName} plan to this member?");
if (!confirmed) return;
try
{
var request = new AdminSubscribeRequest { UserId = _selectedUser.User.Id, MembershipId = membershipId };
var result = await ApiClient.AdminSubscribeAsync(request);
if (result != null)
{
await JSRuntime.InvokeVoidAsync("showToast", "Subscription assigned successfully.", "success");
await ShowDetails(_selectedUser.User.Id); // Refresh details
}
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("showToast", "Error: " + ex.Message, "error");
}
}
private async Task TopUpWallet()
{
if (_selectedUser == null || _topUpAmount <= 0) return;
bool confirmed = await JSRuntime.InvokeAsync<bool>("confirm", $"Add {_topUpAmount:N0} MMK to {_selectedUser.User.FullName}'s wallet?");
if (!confirmed) return;
try
{
var success = await ApiClient.TopUpWalletAsync(new TopUpRequest
{
UserId = _selectedUser.User.Id,
Amount = _topUpAmount,
Description = "Librarian Cash Top-up"
});
if (success)
{
await JSRuntime.InvokeVoidAsync("showToast", "Wallet topped up successfully!", "success");
_topUpAmount = 0;
await ShowDetails(_selectedUser.User.Id); // Refresh details to show new balance
}
else
{
await JSRuntime.InvokeVoidAsync("showToast", "Failed to top up wallet.", "error");
}
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("showToast", "Error: " + ex.Message, "error");
}
}
}