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
- <button @onclick="() => SubscribeAsync(plan)"
153
- disabled="@IsCurrentPlan(plan.Id)"
154
- class='w-full py-6 rounded-[2rem] font-black uppercase tracking-widest text-sm transition-all @(IsCurrentPlan(plan.Id) ? "bg-body text-muted border border-border cursor-not-allowed" : "bg-primary text-white hover:bg-primary-hover hover:shadow-2xl hover:shadow-primary/30 transform hover:scale-[1.02] active:scale-95")'>
155
- @(IsCurrentPlan(plan.Id) ? "Current Plan" : "Select Plan")
156
- </button>
 
 
 
 
 
 
 
 
 
 
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", "Subscription successful!");
214
  await LoadData();
215
  }
216
- else
 
 
 
 
217
  {
218
- await JS.InvokeVoidAsync("alert", "Subscription failed. Please check your account balance or try again.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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