Spaces:
Sleeping
Sleeping
Yuyuqt commited on
Commit ·
a83fc52
1
Parent(s): 92812c4
add: user wallet frontend
Browse files
BlazorWebAssembly/Pages/Members.razor
CHANGED
|
@@ -229,6 +229,34 @@
|
|
| 229 |
<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>
|
| 230 |
</button>
|
| 231 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
|
| 233 |
<!-- Subscription Management -->
|
| 234 |
<div class="mt-12 pt-12 border-t border-border">
|
|
@@ -385,6 +413,8 @@
|
|
| 385 |
private UserCreateRequest _createRequest = new();
|
| 386 |
private UserUpdateRequest _updateRequest = new();
|
| 387 |
private UserDto? _editUser;
|
|
|
|
|
|
|
| 388 |
|
| 389 |
protected override async Task OnInitializedAsync()
|
| 390 |
{
|
|
@@ -606,4 +636,38 @@
|
|
| 606 |
await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
|
| 607 |
}
|
| 608 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
}
|
|
|
|
|
|
| 229 |
<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>
|
| 230 |
</button>
|
| 231 |
</div>
|
| 232 |
+
|
| 233 |
+
<!-- Wallet Management Section -->
|
| 234 |
+
<div class="mt-12 pt-12 border-t border-border">
|
| 235 |
+
<div class="flex items-center justify-between mb-8">
|
| 236 |
+
<h3 class="text-2xl font-bold text-main">Library Wallet</h3>
|
| 237 |
+
<div class="bg-primary/5 border border-primary/20 px-6 py-3 rounded-2xl">
|
| 238 |
+
<span class="text-[10px] font-black text-primary uppercase tracking-widest block mb-1">Current Balance</span>
|
| 239 |
+
<span class="text-2xl font-black text-main">@_selectedUser.User.Balance.ToString("N0") MMK</span>
|
| 240 |
+
</div>
|
| 241 |
+
</div>
|
| 242 |
+
|
| 243 |
+
<div class="bg-card border border-border rounded-3xl p-8 shadow-sm">
|
| 244 |
+
<h4 class="text-xs font-black text-muted uppercase tracking-[0.2em] mb-6">Add Credits (Cash Deposit)</h4>
|
| 245 |
+
<div class="flex flex-col sm:flex-row gap-4">
|
| 246 |
+
<div class="flex-1 relative">
|
| 247 |
+
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
| 248 |
+
<span class="text-muted text-xs font-black">MMK</span>
|
| 249 |
+
</div>
|
| 250 |
+
<input type="number" @bind="_topUpAmount" class="w-full bg-body border border-border rounded-xl pl-16 pr-4 py-4 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all font-bold" placeholder="0.00" />
|
| 251 |
+
</div>
|
| 252 |
+
<button @onclick="TopUpWallet" class="bg-primary text-white px-10 py-4 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all whitespace-nowrap" disabled="@(_topUpAmount <= 0)">
|
| 253 |
+
Confirm Deposit
|
| 254 |
+
</button>
|
| 255 |
+
</div>
|
| 256 |
+
<p class="mt-4 text-xs text-muted font-medium italic">Credits will be added instantly for automated subscription purchases.</p>
|
| 257 |
+
</div>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
|
| 261 |
<!-- Subscription Management -->
|
| 262 |
<div class="mt-12 pt-12 border-t border-border">
|
|
|
|
| 413 |
private UserCreateRequest _createRequest = new();
|
| 414 |
private UserUpdateRequest _updateRequest = new();
|
| 415 |
private UserDto? _editUser;
|
| 416 |
+
private decimal _topUpAmount;
|
| 417 |
+
|
| 418 |
|
| 419 |
protected override async Task OnInitializedAsync()
|
| 420 |
{
|
|
|
|
| 636 |
await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
|
| 637 |
}
|
| 638 |
}
|
| 639 |
+
|
| 640 |
+
private async Task TopUpWallet()
|
| 641 |
+
{
|
| 642 |
+
if (_selectedUser == null || _topUpAmount <= 0) return;
|
| 643 |
+
|
| 644 |
+
bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Add {_topUpAmount:N0} MMK to {_selectedUser.User.FullName}'s wallet?");
|
| 645 |
+
if (!confirmed) return;
|
| 646 |
+
|
| 647 |
+
try
|
| 648 |
+
{
|
| 649 |
+
var success = await ApiClient.TopUpWalletAsync(new TopUpRequest
|
| 650 |
+
{
|
| 651 |
+
UserId = _selectedUser.User.Id,
|
| 652 |
+
Amount = _topUpAmount,
|
| 653 |
+
Description = "Librarian Cash Top-up"
|
| 654 |
+
});
|
| 655 |
+
|
| 656 |
+
if (success)
|
| 657 |
+
{
|
| 658 |
+
await JS.InvokeVoidAsync("alert", "Wallet topped up successfully!");
|
| 659 |
+
_topUpAmount = 0;
|
| 660 |
+
await ShowDetails(_selectedUser.User.Id); // Refresh
|
| 661 |
+
}
|
| 662 |
+
else
|
| 663 |
+
{
|
| 664 |
+
await JS.InvokeVoidAsync("alert", "Failed to top up wallet.");
|
| 665 |
+
}
|
| 666 |
+
}
|
| 667 |
+
catch (Exception ex)
|
| 668 |
+
{
|
| 669 |
+
await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
|
| 670 |
+
}
|
| 671 |
+
}
|
| 672 |
}
|
| 673 |
+
|
BlazorWebAssembly/Pages/Membership.razor
CHANGED
|
@@ -51,8 +51,35 @@
|
|
| 51 |
</div>
|
| 52 |
</div>
|
| 53 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
}
|
| 55 |
|
|
|
|
| 56 |
@if (_queuedRewards.Any())
|
| 57 |
{
|
| 58 |
<div class="mb-20 animate-fade-in-down">
|
|
@@ -149,12 +176,23 @@
|
|
| 149 |
</div>
|
| 150 |
</div>
|
| 151 |
|
| 152 |
-
<
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
</div>
|
|
|
|
| 158 |
}
|
| 159 |
</div>
|
| 160 |
|
|
@@ -176,6 +214,9 @@
|
|
| 176 |
private List<MembershipDto> _memberships = new();
|
| 177 |
private SubscriptionDto? _currentSub;
|
| 178 |
private List<SubscriptionDto> _queuedRewards = new();
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
protected override async Task OnInitializedAsync()
|
| 181 |
{
|
|
@@ -195,6 +236,9 @@
|
|
| 195 |
.Where(s => !string.IsNullOrEmpty(s.ExternalRedemptionId) && s.StartDate > DateTime.UtcNow)
|
| 196 |
.OrderBy(s => s.StartDate)
|
| 197 |
.ToList();
|
|
|
|
|
|
|
|
|
|
| 198 |
}
|
| 199 |
catch { }
|
| 200 |
finally { _isLoading = false; }
|
|
@@ -204,18 +248,63 @@
|
|
| 204 |
|
| 205 |
private async Task SubscribeAsync(MembershipDto plan)
|
| 206 |
{
|
| 207 |
-
bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Are you sure you want to subscribe to {plan.Type} for {plan.Price} mmk?");
|
| 208 |
if (!confirmed) return;
|
| 209 |
|
| 210 |
var result = await ApiClient.SubscribeAsync(new SubscribeRequest { MembershipId = plan.Id });
|
| 211 |
if (result != null)
|
| 212 |
{
|
| 213 |
-
await JS.InvokeVoidAsync("alert", "
|
| 214 |
await LoadData();
|
| 215 |
}
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
{
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
}
|
| 220 |
}
|
| 221 |
}
|
|
|
|
|
|
| 51 |
</div>
|
| 52 |
</div>
|
| 53 |
</div>
|
| 54 |
+
|
| 55 |
+
<!-- Wallet & Loyalty Summary -->
|
| 56 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-12">
|
| 57 |
+
<div class="bg-card border border-border rounded-3xl p-6 flex items-center justify-between shadow-sm">
|
| 58 |
+
<div class="flex items-center gap-4">
|
| 59 |
+
<div class="w-12 h-12 bg-primary/10 rounded-2xl flex items-center justify-center text-primary">
|
| 60 |
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
| 61 |
+
</div>
|
| 62 |
+
<div>
|
| 63 |
+
<p class="text-[10px] font-black text-muted uppercase tracking-widest">Loyalty Points</p>
|
| 64 |
+
<p class="text-xl font-black text-main">@(_loyaltyAccount?.CurrentBalance.ToString("N0") ?? "0")</p>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
<div class="bg-card border border-border rounded-3xl p-6 flex items-center justify-between shadow-sm">
|
| 69 |
+
<div class="flex items-center gap-4">
|
| 70 |
+
<div class="w-12 h-12 bg-emerald-500/10 rounded-2xl flex items-center justify-center text-emerald-500">
|
| 71 |
+
<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="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
| 72 |
+
</div>
|
| 73 |
+
<div>
|
| 74 |
+
<p class="text-[10px] font-black text-muted uppercase tracking-widest">Wallet Balance</p>
|
| 75 |
+
<p class="text-xl font-black text-main">@_walletBalance.ToString("N0") <span class="text-xs font-bold text-muted">MMK</span></p>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
}
|
| 81 |
|
| 82 |
+
|
| 83 |
@if (_queuedRewards.Any())
|
| 84 |
{
|
| 85 |
<div class="mb-20 animate-fade-in-down">
|
|
|
|
| 176 |
</div>
|
| 177 |
</div>
|
| 178 |
|
| 179 |
+
<div class="space-y-4 mt-auto">
|
| 180 |
+
<button @onclick="() => SubscribeAsync(plan)"
|
| 181 |
+
disabled="@IsCurrentPlan(plan.Id)"
|
| 182 |
+
class='w-full py-5 rounded-[2rem] font-black uppercase tracking-widest text-[10px] transition-all @(IsCurrentPlan(plan.Id) ? "bg-body text-muted border border-border cursor-not-allowed" : "bg-main text-white hover:bg-black transform hover:scale-[1.02] active:scale-95")'>
|
| 183 |
+
@(IsCurrentPlan(plan.Id) ? "Active Now" : "Pay Cash at Library")
|
| 184 |
+
</button>
|
| 185 |
+
|
| 186 |
+
@if (!IsCurrentPlan(plan.Id))
|
| 187 |
+
{
|
| 188 |
+
<button @onclick="() => BuyWithWallet(plan)"
|
| 189 |
+
class="w-full py-5 rounded-[2rem] bg-primary text-white font-black uppercase tracking-widest text-[10px] hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all transform hover:scale-[1.02] active:scale-95">
|
| 190 |
+
Buy with Wallet
|
| 191 |
+
</button>
|
| 192 |
+
}
|
| 193 |
+
</div>
|
| 194 |
</div>
|
| 195 |
+
|
| 196 |
}
|
| 197 |
</div>
|
| 198 |
|
|
|
|
| 214 |
private List<MembershipDto> _memberships = new();
|
| 215 |
private SubscriptionDto? _currentSub;
|
| 216 |
private List<SubscriptionDto> _queuedRewards = new();
|
| 217 |
+
private decimal _walletBalance;
|
| 218 |
+
private LoyaltyAccountDto? _loyaltyAccount;
|
| 219 |
+
|
| 220 |
|
| 221 |
protected override async Task OnInitializedAsync()
|
| 222 |
{
|
|
|
|
| 236 |
.Where(s => !string.IsNullOrEmpty(s.ExternalRedemptionId) && s.StartDate > DateTime.UtcNow)
|
| 237 |
.OrderBy(s => s.StartDate)
|
| 238 |
.ToList();
|
| 239 |
+
|
| 240 |
+
_walletBalance = await ApiClient.GetWalletBalanceAsync();
|
| 241 |
+
_loyaltyAccount = await ApiClient.GetMyLoyaltyAccountAsync();
|
| 242 |
}
|
| 243 |
catch { }
|
| 244 |
finally { _isLoading = false; }
|
|
|
|
| 248 |
|
| 249 |
private async Task SubscribeAsync(MembershipDto plan)
|
| 250 |
{
|
| 251 |
+
bool confirmed = await JS.InvokeAsync<bool>("confirm", $"Are you sure you want to subscribe to {plan.Type} for {plan.Price:N0} mmk? Please pay the cash at the library counter to activate.");
|
| 252 |
if (!confirmed) return;
|
| 253 |
|
| 254 |
var result = await ApiClient.SubscribeAsync(new SubscribeRequest { MembershipId = plan.Id });
|
| 255 |
if (result != null)
|
| 256 |
{
|
| 257 |
+
await JS.InvokeVoidAsync("alert", "Request submitted! Please visit the librarian to pay and activate.");
|
| 258 |
await LoadData();
|
| 259 |
}
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
private async Task BuyWithWallet(MembershipDto plan)
|
| 263 |
+
{
|
| 264 |
+
try
|
| 265 |
{
|
| 266 |
+
var preview = await ApiClient.GetUpgradePreviewAsync(plan.Id);
|
| 267 |
+
if (preview == null || !preview.CanUpgrade)
|
| 268 |
+
{
|
| 269 |
+
await JS.InvokeVoidAsync("alert", preview?.Message ?? "Cannot upgrade to this plan.");
|
| 270 |
+
return;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
string confirmMsg = $"Purchase {plan.Type} membership?\n\n" +
|
| 274 |
+
$"Price: {preview.OriginalPrice:N0} MMK\n";
|
| 275 |
+
|
| 276 |
+
if (preview.DiscountAmount > 0)
|
| 277 |
+
{
|
| 278 |
+
confirmMsg += $"Upgrade Discount: -{preview.DiscountAmount:N0} MMK\n";
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
confirmMsg += $"Total: {preview.FinalPrice:N0} MMK\n\n" +
|
| 282 |
+
$"Your Balance: {_walletBalance:N0} MMK";
|
| 283 |
+
|
| 284 |
+
bool confirmed = await JS.InvokeAsync<bool>("confirm", confirmMsg);
|
| 285 |
+
if (!confirmed) return;
|
| 286 |
+
|
| 287 |
+
if (_walletBalance < preview.FinalPrice)
|
| 288 |
+
{
|
| 289 |
+
await JS.InvokeVoidAsync("alert", "Insufficient wallet balance. Please top up at the library counter.");
|
| 290 |
+
return;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
var result = await ApiClient.SubscribeWithWalletAsync(new SubscribeRequest { MembershipId = plan.Id });
|
| 294 |
+
if (result != null)
|
| 295 |
+
{
|
| 296 |
+
await JS.InvokeVoidAsync("alert", "Subscription purchased successfully!");
|
| 297 |
+
await LoadData();
|
| 298 |
+
}
|
| 299 |
+
else
|
| 300 |
+
{
|
| 301 |
+
await JS.InvokeVoidAsync("alert", "Failed to complete purchase.");
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
catch (Exception ex)
|
| 305 |
+
{
|
| 306 |
+
await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
|
| 307 |
}
|
| 308 |
}
|
| 309 |
}
|
| 310 |
+
|
BlazorWebAssembly/Services/LibraryApiClient.cs
CHANGED
|
@@ -167,8 +167,50 @@ namespace BlazorWebAssembly.Services
|
|
| 167 |
{
|
| 168 |
return await _httpClient.GetFromJsonAsync<IEnumerable<SubscriptionDto>>("api/subscriptions/all") ?? Enumerable.Empty<SubscriptionDto>();
|
| 169 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
#endregion
|
| 171 |
|
|
|
|
| 172 |
public async Task<LoyaltyAccountDto?> GetMyLoyaltyAccountAsync()
|
| 173 |
{
|
| 174 |
try
|
|
|
|
| 167 |
{
|
| 168 |
return await _httpClient.GetFromJsonAsync<IEnumerable<SubscriptionDto>>("api/subscriptions/all") ?? Enumerable.Empty<SubscriptionDto>();
|
| 169 |
}
|
| 170 |
+
|
| 171 |
+
public async Task<SubscriptionUpgradePreviewDto?> GetUpgradePreviewAsync(int membershipId)
|
| 172 |
+
{
|
| 173 |
+
try
|
| 174 |
+
{
|
| 175 |
+
return await _httpClient.GetFromJsonAsync<SubscriptionUpgradePreviewDto>($"api/subscriptions/preview-upgrade/{membershipId}");
|
| 176 |
+
}
|
| 177 |
+
catch { return null; }
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
public async Task<SubscriptionDto?> SubscribeWithWalletAsync(SubscribeRequest request)
|
| 181 |
+
{
|
| 182 |
+
var response = await _httpClient.PostAsJsonAsync("api/subscriptions/subscribe-wallet", request);
|
| 183 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<SubscriptionDto>() : null;
|
| 184 |
+
}
|
| 185 |
+
#endregion
|
| 186 |
+
|
| 187 |
+
#region Wallet
|
| 188 |
+
public async Task<decimal> GetWalletBalanceAsync()
|
| 189 |
+
{
|
| 190 |
+
try
|
| 191 |
+
{
|
| 192 |
+
return await _httpClient.GetFromJsonAsync<decimal>("api/wallet/balance");
|
| 193 |
+
}
|
| 194 |
+
catch { return 0; }
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
public async Task<IEnumerable<WalletTransactionDto>> GetWalletHistoryAsync()
|
| 198 |
+
{
|
| 199 |
+
try
|
| 200 |
+
{
|
| 201 |
+
return await _httpClient.GetFromJsonAsync<IEnumerable<WalletTransactionDto>>("api/wallet/history") ?? Enumerable.Empty<WalletTransactionDto>();
|
| 202 |
+
}
|
| 203 |
+
catch { return Enumerable.Empty<WalletTransactionDto>(); }
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
public async Task<bool> TopUpWalletAsync(TopUpRequest request)
|
| 207 |
+
{
|
| 208 |
+
var response = await _httpClient.PostAsJsonAsync("api/wallet/topup", request);
|
| 209 |
+
return response.IsSuccessStatusCode;
|
| 210 |
+
}
|
| 211 |
#endregion
|
| 212 |
|
| 213 |
+
|
| 214 |
public async Task<LoyaltyAccountDto?> GetMyLoyaltyAccountAsync()
|
| 215 |
{
|
| 216 |
try
|