Yuyuqt commited on
Commit
6469525
·
1 Parent(s): 0cba109

fix: add Librarian approval for pay in cash, balance update loading time fix, membership approval page added

Browse files
Backend/Features/Subscriptions/SubscriptionController.cs CHANGED
@@ -60,9 +60,11 @@ namespace Backend.Features.Subscriptions
60
  if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
61
 
62
  var userId = Guid.Parse(userIdStr);
63
- var subscription = await _subscriptionService.SubscribeUserAsync(userId, request.MembershipId);
 
64
  return Ok(subscription);
65
  }
 
66
  catch (Exception ex)
67
  {
68
  return BadRequest(new { message = ex.Message });
@@ -129,5 +131,22 @@ namespace Backend.Features.Subscriptions
129
  return BadRequest(new { message = ex.Message });
130
  }
131
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  }
133
  }
 
 
60
  if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
61
 
62
  var userId = Guid.Parse(userIdStr);
63
+ // For cash payments, we create a pending subscription
64
+ var subscription = await _subscriptionService.CreatePendingSubscriptionAsync(userId, request.MembershipId);
65
  return Ok(subscription);
66
  }
67
+
68
  catch (Exception ex)
69
  {
70
  return BadRequest(new { message = ex.Message });
 
131
  return BadRequest(new { message = ex.Message });
132
  }
133
  }
134
+
135
+ [HttpGet("subscriptions/pending")]
136
+ [Authorize(Roles = "Librarian")]
137
+ public async Task<ActionResult<IEnumerable<SubscriptionDto>>> GetPendingSubscriptions()
138
+ {
139
+ return Ok(await _subscriptionService.GetPendingSubscriptionsAsync());
140
+ }
141
+
142
+ [HttpPost("subscriptions/approve")]
143
+ [Authorize(Roles = "Librarian")]
144
+ public async Task<IActionResult> ApproveSubscription([FromBody] ApproveSubscriptionRequest request)
145
+ {
146
+ var success = await _subscriptionService.ApproveSubscriptionAsync(request.SubscriptionId, request.Approve);
147
+ if (!success) return NotFound(new { message = "Subscription not found." });
148
+ return Ok(new { message = request.Approve ? "Subscription approved." : "Subscription rejected." });
149
+ }
150
  }
151
  }
152
+
Backend/Features/Subscriptions/SubscriptionService.cs CHANGED
@@ -14,13 +14,17 @@ namespace Backend.Features.Subscriptions
14
  Task<SubscriptionDto?> GetUserSubscriptionAsync(Guid userId);
15
  Task<IEnumerable<SubscriptionDto>> GetUserAllSubscriptionsAsync(Guid userId);
16
  Task<bool> HandleLoyaltyRedemptionAsync(Guid userId, string rewardId, string rewardName, string redemptionId);
17
- Task<SubscriptionDto> SubscribeUserAsync(Guid userId, int membershipId, string? redemptionId = null);
18
  Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync();
 
 
 
19
  Task<SubscriptionUpgradePreviewDto> GetUpgradePreviewAsync(Guid userId, int newMembershipId);
20
  Task<SubscriptionDto> SubscribeWithWalletAsync(Guid userId, int membershipId);
21
  }
22
 
23
 
 
24
  public class SubscriptionService : ISubscriptionService
25
  {
26
  private readonly AppDbContext _context;
@@ -113,7 +117,8 @@ namespace Backend.Features.Subscriptions
113
  }
114
  }
115
 
116
- public async Task<SubscriptionDto> SubscribeUserAsync(Guid userId, int membershipId, string? redemptionId = null)
 
117
  {
118
  var membership = await _context.Memberships.FindAsync(membershipId);
119
  if (membership == null) throw new Exception("Membership plan not found.");
@@ -161,11 +166,13 @@ namespace Backend.Features.Subscriptions
161
  MembershipId = membershipId,
162
  StartDate = startDate,
163
  ExpiryDate = startDate.AddMonths(membership.DurationMonths),
164
- IsActive = true,
165
  ExternalRedemptionId = redemptionId,
166
- Status = "Active"
 
167
  };
168
 
 
169
  _context.UserSubscriptions.Add(subscription);
170
  await _context.SaveChangesAsync();
171
 
@@ -176,15 +183,19 @@ namespace Backend.Features.Subscriptions
176
  decimal paidAmount = membership.Price - discount;
177
  if (paidAmount < 0) paidAmount = 0;
178
 
179
- await _loyaltyService.ProcessEventAsync(
180
- externalUserId: userId.ToString(),
181
- eventKey: "SUBSCRIBE",
182
- eventValue: (double)paidAmount,
183
- referenceId: $"SUB-{subscription.Id}",
184
- description: $"Purchased Membership: {membership.Type} (Discount applied: {discount})",
185
- email: subscription.User?.Email ?? "No Email",
186
- mobile: subscription.User?.PhoneNumber ?? "0000000000"
187
- );
 
 
 
 
188
 
189
  return MapToDto(subscription);
190
  }
@@ -263,9 +274,66 @@ namespace Backend.Features.Subscriptions
263
  if (!deducted) throw new Exception("Failed to deduct from wallet.");
264
 
265
  // Create subscription
266
- return await SubscribeUserAsync(userId, membershipId);
267
  }
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
  public async Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync()
271
  {
@@ -291,8 +359,11 @@ namespace Backend.Features.Subscriptions
291
  StartDate = subscription.StartDate,
292
  ExpiryDate = subscription.ExpiryDate,
293
  IsActive = subscription.IsActive,
 
 
294
  ExternalRedemptionId = subscription.ExternalRedemptionId
295
  };
 
296
  }
297
  }
298
  }
 
14
  Task<SubscriptionDto?> GetUserSubscriptionAsync(Guid userId);
15
  Task<IEnumerable<SubscriptionDto>> GetUserAllSubscriptionsAsync(Guid userId);
16
  Task<bool> HandleLoyaltyRedemptionAsync(Guid userId, string rewardId, string rewardName, string redemptionId);
17
+ Task<SubscriptionDto> SubscribeUserAsync(Guid userId, int membershipId, string? redemptionId = null, string status = "Active", string paymentMethod = "Cash");
18
  Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync();
19
+ Task<IEnumerable<SubscriptionDto>> GetPendingSubscriptionsAsync();
20
+ Task<SubscriptionDto> CreatePendingSubscriptionAsync(Guid userId, int membershipId);
21
+ Task<bool> ApproveSubscriptionAsync(Guid subscriptionId, bool approve);
22
  Task<SubscriptionUpgradePreviewDto> GetUpgradePreviewAsync(Guid userId, int newMembershipId);
23
  Task<SubscriptionDto> SubscribeWithWalletAsync(Guid userId, int membershipId);
24
  }
25
 
26
 
27
+
28
  public class SubscriptionService : ISubscriptionService
29
  {
30
  private readonly AppDbContext _context;
 
117
  }
118
  }
119
 
120
+ public async Task<SubscriptionDto> SubscribeUserAsync(Guid userId, int membershipId, string? redemptionId = null, string status = "Active", string paymentMethod = "Cash")
121
+
122
  {
123
  var membership = await _context.Memberships.FindAsync(membershipId);
124
  if (membership == null) throw new Exception("Membership plan not found.");
 
166
  MembershipId = membershipId,
167
  StartDate = startDate,
168
  ExpiryDate = startDate.AddMonths(membership.DurationMonths),
169
+ IsActive = status == "Active",
170
  ExternalRedemptionId = redemptionId,
171
+ Status = status,
172
+ PaymentMethod = paymentMethod
173
  };
174
 
175
+
176
  _context.UserSubscriptions.Add(subscription);
177
  await _context.SaveChangesAsync();
178
 
 
183
  decimal paidAmount = membership.Price - discount;
184
  if (paidAmount < 0) paidAmount = 0;
185
 
186
+ if (status == "Active")
187
+ {
188
+ await _loyaltyService.ProcessEventAsync(
189
+ externalUserId: userId.ToString(),
190
+ eventKey: "SUBSCRIBE",
191
+ eventValue: (double)paidAmount,
192
+ referenceId: $"SUB-{subscription.Id}",
193
+ description: $"Purchased Membership: {membership.Type} (Discount applied: {discount})",
194
+ email: subscription.User?.Email ?? "No Email",
195
+ mobile: subscription.User?.PhoneNumber ?? "0000000000"
196
+ );
197
+ }
198
+
199
 
200
  return MapToDto(subscription);
201
  }
 
274
  if (!deducted) throw new Exception("Failed to deduct from wallet.");
275
 
276
  // Create subscription
277
+ return await SubscribeUserAsync(userId, membershipId, status: "Active", paymentMethod: "Wallet");
278
  }
279
 
280
+ public async Task<IEnumerable<SubscriptionDto>> GetPendingSubscriptionsAsync()
281
+ {
282
+ var pending = await _context.UserSubscriptions
283
+ .Include(s => s.Membership)
284
+ .Include(s => s.User)
285
+ .Where(s => s.Status == "PendingApproval")
286
+ .OrderByDescending(s => s.StartDate)
287
+ .ToListAsync();
288
+
289
+ return pending.Select(MapToDto);
290
+ }
291
+
292
+ public async Task<SubscriptionDto> CreatePendingSubscriptionAsync(Guid userId, int membershipId)
293
+ {
294
+ // Similar logic to SubscribeUserAsync but set to PendingApproval
295
+ return await SubscribeUserAsync(userId, membershipId, status: "PendingApproval", paymentMethod: "Cash");
296
+ }
297
+
298
+ public async Task<bool> ApproveSubscriptionAsync(Guid subscriptionId, bool approve)
299
+ {
300
+ var subscription = await _context.UserSubscriptions
301
+ .Include(s => s.Membership)
302
+ .Include(s => s.User)
303
+ .FirstOrDefaultAsync(s => s.Id == subscriptionId);
304
+
305
+ if (subscription == null) return false;
306
+
307
+ if (approve)
308
+ {
309
+ subscription.Status = "Active";
310
+ subscription.IsActive = true;
311
+
312
+ // Award points now
313
+ decimal discount = 0; // Simplified for approval flow for now
314
+ decimal paidAmount = subscription.Membership.Price - discount;
315
+
316
+ await _loyaltyService.ProcessEventAsync(
317
+ externalUserId: subscription.UserId.ToString(),
318
+ eventKey: "SUBSCRIBE",
319
+ eventValue: (double)paidAmount,
320
+ referenceId: $"SUB-{subscription.Id}",
321
+ description: $"Membership Approved: {subscription.Membership.Type}",
322
+ email: subscription.User?.Email ?? "No Email",
323
+ mobile: subscription.User?.PhoneNumber ?? "0000000000"
324
+ );
325
+ }
326
+ else
327
+ {
328
+ subscription.Status = "Rejected";
329
+ subscription.IsActive = false;
330
+ }
331
+
332
+ await _context.SaveChangesAsync();
333
+ return true;
334
+ }
335
+
336
+
337
 
338
  public async Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync()
339
  {
 
359
  StartDate = subscription.StartDate,
360
  ExpiryDate = subscription.ExpiryDate,
361
  IsActive = subscription.IsActive,
362
+ Status = subscription.Status,
363
+ PaymentMethod = subscription.PaymentMethod,
364
  ExternalRedemptionId = subscription.ExternalRedemptionId
365
  };
366
+
367
  }
368
  }
369
  }
BlazorWebAssembly/Pages/Members.razor CHANGED
@@ -9,403 +9,484 @@
9
  @inject NavigationManager Navigation
10
 
11
  <div class="max-w-7xl mx-auto py-10 px-6 animate-fade">
12
- @if (_currentView == ViewMode.List)
13
- {
14
- <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
15
- <div>
16
- <h1 class="text-4xl font-extrabold tracking-tight text-main mb-2">Members Directory</h1>
17
- <p class="text-muted text-lg">
18
- Manage library members and administrator accounts.
19
- <span class="ml-2 text-xs text-muted font-bold">(@(_allUsers?.Count() ?? 0) total)</span>
20
- </p>
21
- </div>
22
- <div class="flex items-center gap-3">
23
- <button @onclick="ShowCreate" class="bg-primary text-white px-6 py-3 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all whitespace-nowrap flex items-center">
24
- <svg class="w-5 h-5 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>
25
- Add Member
26
- </button>
27
- </div>
28
- </div>
29
 
30
- @if (_isLoading)
 
 
31
  {
32
- <div class="space-y-4">
33
- @for (int i = 0; i < 5; i++)
34
- {
35
- <div class="animate-pulse bg-card rounded-xl border border-border h-24 w-full"></div>
36
- }
 
 
 
 
 
 
 
 
 
37
  </div>
38
- }
39
- else if (_pagedResult != null)
40
- {
41
- <div class="bg-card rounded-2xl border border-border overflow-hidden shadow-sm">
42
- <div class="overflow-x-auto">
43
- <table class="w-full text-left border-collapse min-w-max whitespace-nowrap">
44
- <thead class="bg-body border-b border-border">
45
- <tr>
46
- <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Member</th>
47
- <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Contact</th>
48
- <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Role</th>
49
- <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Status</th>
50
- <th class="px-6 py-4 text-right text-[10px] font-bold text-muted uppercase tracking-widest">Actions</th>
51
- </tr>
52
- </thead>
53
- <tbody class="divide-y divide-border">
54
- @foreach (var user in _pagedResult.Items)
55
- {
56
- <tr class="hover:bg-body transition-colors group">
57
- <td class="px-6 py-4">
58
- <div class="flex items-center gap-4">
59
- <div class="h-10 w-10 flex-shrink-0 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-sm">
60
- @(user.FullName?.Substring(0, Math.Min(2, user.FullName.Length)).ToUpper() ?? "U")
61
- </div>
62
- <div>
63
- <button @onclick="() => ShowDetails(user.Id)" class="text-sm font-bold text-main hover:text-primary transition-colors text-left">@user.FullName</button>
64
- @if (!string.IsNullOrEmpty(user.StudentId))
65
- {
66
- <div class="text-xs text-muted font-medium mt-0.5">ID: @user.StudentId</div>
67
- }
 
 
 
 
 
 
 
 
 
 
68
  </div>
69
- </div>
70
- </td>
71
- <td class="px-6 py-4">
72
- <div class="text-sm text-main font-medium">@user.Email</div>
73
- <div class="text-xs text-muted">@(string.IsNullOrEmpty(user.PhoneNumber) ? "No phone" : user.PhoneNumber)</div>
74
- </td>
75
- <td class="px-6 py-4">
76
- <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase @(user.Role == "Admin" ? "bg-amber-500/10 text-amber-500" : "bg-blue-500/10 text-blue-500")">
77
- @user.Role
78
- </span>
79
- </td>
80
- <td class="px-6 py-4">
81
- @if (user.BanStatus == true)
82
- {
83
- <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase bg-red-500/10 text-red-500">
84
- Suspended
85
  </span>
86
- }
87
- else if (user.IsActive)
88
- {
89
- <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase bg-green-500/10 text-green-500">
90
- Active
91
- </span>
92
- }
93
- else
94
- {
95
- <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase bg-body text-muted border border-border">
96
- Inactive
97
- </span>
98
- }
99
- </td>
100
- <td class="px-6 py-4 text-right">
101
- <div class="flex items-center justify-end gap-2">
102
- <button @onclick="() => ShowDetails(user.Id)" class="p-2 rounded-lg border border-border text-muted hover:bg-primary hover:text-white hover:border-primary transition-all">
103
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
104
- </button>
105
- <button @onclick="() => ShowEdit(user.Id)" class="p-2 rounded-lg border border-border text-muted hover:bg-primary hover:text-white hover:border-primary transition-all">
106
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
107
- </button>
108
- </div>
109
- </td>
110
- </tr>
111
- }
112
- </tbody>
113
- </table>
114
- </div>
 
 
 
 
 
 
 
115
 
116
- @if (_pagedResult.TotalPages > 1)
117
- {
118
- <div class="px-6 py-4 border-t border-border flex items-center justify-between bg-body">
119
- <p class="text-xs text-muted">
120
- Showing <span class="font-bold text-main">@((_pagedResult.CurrentPage - 1) * _pagedResult.PageSize + 1)</span>
121
- to
122
- <span class="font-bold text-main">@(Math.Min(_pagedResult.CurrentPage * _pagedResult.PageSize, _pagedResult.TotalCount))</span>
123
- of <span class="font-bold text-main">@_pagedResult.TotalCount</span> members
124
- </p>
125
- <div class="flex items-center gap-2">
126
- <button @onclick="() => ChangePage(_pagedResult.CurrentPage - 1)" disabled="@(!_pagedResult.HasPreviousPage)"
127
- class="p-2 rounded-lg border border-border text-muted hover:bg-card disabled:opacity-30 disabled:cursor-not-allowed transition-all">
128
- <svg class="w-4 h-4" 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>
129
- </button>
130
 
131
- @for (int p = 1; p <= _pagedResult.TotalPages; p++)
132
- {
133
- var pageNumber = p;
134
- if (p == _pagedResult.CurrentPage)
135
- {
136
- <span class="w-8 h-8 flex items-center justify-center rounded-lg bg-main text-body text-xs font-bold">@p</span>
137
- }
138
- else if (p == 1 || p == _pagedResult.TotalPages || (p >= _pagedResult.CurrentPage - 1 && p <= _pagedResult.CurrentPage + 1))
139
  {
140
- <button @onclick="() => ChangePage(pageNumber)"
141
- class="w-8 h-8 flex items-center justify-center rounded-lg border border-border text-xs font-bold text-muted hover:text-primary hover:border-primary transition-colors">@p</button>
142
- }
143
- else if (p == _pagedResult.CurrentPage - 2 || p == _pagedResult.CurrentPage + 2)
144
- {
145
- <span class="w-8 h-8 flex items-center justify-center text-xs text-muted">...</span>
 
 
 
 
 
 
 
 
146
  }
147
- }
148
 
149
- <button @onclick="() => ChangePage(_pagedResult.CurrentPage + 1)" disabled="@(!_pagedResult.HasNextPage)"
150
- class="p-2 rounded-lg border border-border text-muted hover:bg-card disabled:opacity-30 disabled:cursor-not-allowed transition-all">
151
- <svg class="w-4 h-4" 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>
152
- </button>
153
- </div>
154
- </div>
155
- }
156
- </div>
157
- }
158
- }
159
- else if (_currentView == ViewMode.Details && _selectedUser != null)
160
- {
161
- <div class="animate-fade">
162
- <button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-muted hover:text-main mb-6 group">
163
- <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>
164
- Back to Directory
165
- </button>
166
-
167
- <div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
168
- <div class="lg:col-span-1">
169
- <div class="aspect-square w-full rounded-3xl bg-card border border-border flex flex-col items-center justify-center text-main p-6 shadow-sm relative overflow-hidden">
170
- <div class="absolute inset-0 bg-primary/5"></div>
171
- <div class="relative z-10 flex flex-col items-center">
172
- <div class="w-32 h-32 rounded-full bg-main text-body flex items-center justify-center text-5xl font-black mb-6 shadow-xl">
173
- @(_selectedUser.User.FullName?.Substring(0, Math.Min(2, _selectedUser.User.FullName.Length)).ToUpper() ?? "U")
174
- </div>
175
- <h2 class="text-2xl font-bold text-center mb-2">@_selectedUser.User.FullName</h2>
176
- <div class="inline-flex rounded-full px-4 py-1.5 text-[10px] font-black uppercase tracking-widest @(_selectedUser.User.Role == "Admin" ? "bg-amber-500/10 text-amber-500 border border-amber-500/20" : "bg-blue-500/10 text-blue-500 border border-blue-500/20")">
177
- @_selectedUser.User.Role
178
  </div>
179
  </div>
180
- </div>
181
  </div>
182
-
183
- <div class="lg:col-span-2 space-y-8">
184
- <div>
185
- <h1 class="text-4xl font-extrabold text-main tracking-tight mb-4">Member Profile</h1>
186
- <div class="flex items-center gap-3">
187
- @if (_selectedUser.User.BanStatus == true)
188
- {
189
- <span class="inline-flex items-center rounded-full bg-red-500/10 border border-red-500/20 px-4 py-1.5 text-[10px] font-black text-red-500 tracking-widest uppercase">Suspended</span>
190
- }
191
- else if (_selectedUser.User.IsActive)
192
- {
193
- <span class="inline-flex items-center rounded-full bg-green-500/10 border border-green-500/20 px-4 py-1.5 text-[10px] font-black text-green-500 tracking-widest uppercase">Active</span>
194
- }
 
 
 
 
 
 
 
 
 
 
195
  </div>
196
  </div>
197
 
198
- <div class="grid grid-cols-1 sm:grid-cols-2 gap-8 border-y border-border py-8">
199
- <div>
200
- <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Email Address</h4>
201
- <p class="text-sm font-semibold text-main">@_selectedUser.User.Email</p>
202
- </div>
203
- <div>
204
- <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Phone Number</h4>
205
- <p class="text-sm font-semibold text-main">@(string.IsNullOrEmpty(_selectedUser.User.PhoneNumber) ? "-" : _selectedUser.User.PhoneNumber)</p>
206
- </div>
207
- <div>
208
- <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Student ID</h4>
209
- <p class="text-sm font-semibold text-main">@(string.IsNullOrEmpty(_selectedUser.User.StudentId) ? "-" : _selectedUser.User.StudentId)</p>
210
- </div>
211
  <div>
212
- <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Joined Date</h4>
213
- <p class="text-sm font-semibold text-main">@_selectedUser.User.CreatedAt.ToString("MMMM dd, yyyy")</p>
 
 
 
 
 
 
 
 
 
214
  </div>
215
- <div class="sm:col-span-2">
216
- <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Physical Address</h4>
217
- <p class="text-sm font-semibold text-main">@(string.IsNullOrEmpty(_selectedUser.User.Address) ? "-" : _selectedUser.User.Address)</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  </div>
219
- </div>
220
 
221
- <div class="flex flex-wrap items-center gap-4 pt-2">
222
- <button @onclick="() => ShowEdit(_selectedUser.User.Id)" class="bg-primary text-white py-3 px-8 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Edit Details</button>
223
-
224
- <button @onclick="UpdateRole" class="bg-body border border-border text-main py-3 px-8 rounded-xl font-bold hover:bg-card transition-colors">
225
- Make @(_selectedUser.User.Role == "Admin" ? "Member" : "Admin")
226
- </button>
227
 
228
- <button @onclick="DeleteUser" class="p-3 text-red-500 hover:text-red-600 hover:bg-red-500/10 rounded-xl transition-colors ml-auto border border-transparent hover:border-red-500/20">
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">
263
- <h3 class="text-2xl font-bold text-main mb-8">Manage Subscription</h3>
264
-
265
- @if (_selectedUser.CurrentSubscription != null && _selectedUser.CurrentSubscription.IsActive)
266
  {
267
- <div class="bg-green-500/5 border border-green-500/20 rounded-2xl p-6 mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
268
- <div>
269
- <span class="text-[10px] text-green-600 font-black uppercase tracking-widest mb-1 block">Active Plan</span>
270
- <h4 class="text-2xl font-black text-green-700">@_selectedUser.CurrentSubscription.MembershipType</h4>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  </div>
272
- <div class="sm:text-right">
273
- <div class="text-[10px] text-green-600/70 font-black uppercase tracking-widest mb-1">Expires On</div>
274
- <div class="font-bold text-green-700">@_selectedUser.CurrentSubscription.ExpiryDate.ToString("MMMM dd, yyyy")</div>
275
  </div>
276
- </div>
277
  }
278
- else
279
  {
280
- <div class="bg-card border border-dashed border-border rounded-2xl p-6 mb-8 text-center">
281
- <p class="text-muted font-medium">No active subscription for this member.</p>
282
- </div>
283
- }
284
-
285
- <h4 class="text-xs font-black text-muted uppercase tracking-[0.2em] mb-6">Assign New Plan</h4>
286
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
287
- @foreach (var plan in _selectedUser.AvailableMemberships)
288
- {
289
- var isCurrent = _selectedUser.CurrentSubscription?.IsActive == true && _selectedUser.CurrentSubscription.MembershipId == plan.Id;
290
- <div class="bg-card rounded-2xl border p-6 flex flex-col transition-all relative @(isCurrent ? "border-primary shadow-lg shadow-primary/10" : "border-border hover:border-primary/50")">
291
- @if (isCurrent)
292
- {
293
- <div class="absolute -top-3 right-6 bg-primary text-white text-[9px] font-black uppercase tracking-widest px-3 py-1 rounded-full shadow-sm">Current</div>
294
- }
295
- <h5 class="font-bold text-main text-xl mb-1">@plan.Type</h5>
296
- <p class="text-muted font-medium text-sm mb-6 flex-grow">@plan.Price.ToString("N0") MMK / @plan.DurationMonths mo</p>
297
-
298
- @if (!isCurrent)
299
- {
300
- <button @onclick="() => AssignSubscription(plan.Id, plan.Type)" class="w-full bg-body border border-border text-main py-2.5 rounded-xl text-sm font-bold hover:bg-primary hover:text-white hover:border-primary transition-colors">Assign Plan</button>
301
- }
302
- else
303
- {
304
- <button disabled class="w-full bg-body border border-border text-muted py-2.5 rounded-xl text-sm font-bold opacity-50 cursor-not-allowed">Current Plan</button>
305
- }
306
  </div>
307
- }
308
- </div>
 
 
 
 
309
  </div>
310
  </div>
311
  </div>
312
- </div>
313
  }
314
- else if (_currentView == ViewMode.Create || _currentView == ViewMode.Edit)
315
  {
316
  <div class="animate-fade">
317
- <button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-muted hover:text-main mb-6 group">
318
- <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>
319
- Back to Directory
320
- </button>
321
-
322
- <div class="max-w-3xl mx-auto">
323
- <div class="mb-10 text-center">
324
- <h1 class="text-4xl font-extrabold tracking-tight text-main mb-3">@(_currentView == ViewMode.Create ? "Register New Member" : "Edit Member Details")</h1>
325
- <p class="text-lg text-muted">@(_currentView == ViewMode.Create ? "Add a new reader to the library system." : $"Update profile details for {_editUser?.FullName}.")</p>
326
- </div>
327
 
328
- <div class="bg-card border border-border p-8 sm:p-10 rounded-3xl shadow-sm">
329
- @if (_currentView == ViewMode.Create)
 
 
330
  {
331
- <EditForm Model="@_createRequest" OnValidSubmit="HandleCreate" class="space-y-6">
332
- <DataAnnotationsValidator />
333
- <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
334
- <div class="sm:col-span-2">
335
- <label class="block text-sm font-bold text-main mb-2">Full Name <span class="text-red-500">*</span></label>
336
- <InputText @bind-Value="_createRequest.FullName" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="John Doe" />
337
- <ValidationMessage For="@(() => _createRequest.FullName)" class="mt-1 text-xs text-red-500 font-medium" />
338
- </div>
339
- <div>
340
- <label class="block text-sm font-bold text-main mb-2">Email Address <span class="text-red-500">*</span></label>
341
- <InputText @bind-Value="_createRequest.Email" type="email" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="you@domain.com" />
342
- <ValidationMessage For="@(() => _createRequest.Email)" class="mt-1 text-xs text-red-500 font-medium" />
343
- </div>
344
- <div>
345
- <label class="block text-sm font-bold text-main mb-2">Password <span class="text-red-500">*</span></label>
346
- <InputText @bind-Value="_createRequest.Password" type="password" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="••••••••" />
347
- <ValidationMessage For="@(() => _createRequest.Password)" class="mt-1 text-xs text-red-500 font-medium" />
348
- </div>
349
- <div>
350
- <label class="block text-sm font-bold text-main mb-2">Phone Number</label>
351
- <InputText @bind-Value="_createRequest.PhoneNumber" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="+123456789" />
352
- </div>
353
- <div>
354
- <label class="block text-sm font-bold text-main mb-2">Student ID</label>
355
- <InputText @bind-Value="_createRequest.StudentId" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="Optional" />
356
- </div>
357
- <div class="sm:col-span-2">
358
- <label class="block text-sm font-bold text-main mb-2">Address</label>
359
- <InputTextArea @bind-Value="_createRequest.Address" rows="3" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="Full physical address" />
360
- </div>
361
- </div>
362
- <div class="pt-8 border-t border-border flex justify-end gap-4 mt-8">
363
- <button type="button" @onclick="BackToList" class="bg-body border border-border text-main px-8 py-3 rounded-xl font-bold hover:bg-card transition-colors">Cancel</button>
364
- <button type="submit" class="bg-primary text-white px-8 py-3 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Create Member</button>
365
- </div>
366
- </EditForm>
367
  }
368
- else if (_currentView == ViewMode.Edit)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  {
370
- <EditForm Model="@_updateRequest" OnValidSubmit="HandleUpdate" class="space-y-6">
371
- <DataAnnotationsValidator />
372
- <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
373
- <div class="sm:col-span-2">
374
- <label class="block text-sm font-bold text-main mb-2">Full Name <span class="text-red-500">*</span></label>
375
- <InputText @bind-Value="_updateRequest.FullName" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" />
376
- <ValidationMessage For="@(() => _updateRequest.FullName)" class="mt-1 text-xs text-red-500 font-medium" />
377
  </div>
378
  <div>
379
- <label class="block text-sm font-bold text-main mb-2">Phone Number</label>
380
- <InputText @bind-Value="_updateRequest.PhoneNumber" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" />
381
- </div>
382
- <div>
383
- <label class="block text-sm font-bold text-main mb-2">Student ID</label>
384
- <InputText @bind-Value="_updateRequest.StudentId" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" />
385
- </div>
386
- <div class="sm:col-span-2">
387
- <label class="block text-sm font-bold text-main mb-2">Address</label>
388
- <InputTextArea @bind-Value="_updateRequest.Address" rows="3" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" />
389
  </div>
390
  </div>
391
- <div class="pt-8 border-t border-border flex justify-end gap-4 mt-8">
392
- <button type="button" @onclick="BackToList" class="bg-body border border-border text-main px-8 py-3 rounded-xl font-bold hover:bg-card transition-colors">Cancel</button>
393
- <button type="submit" class="bg-primary text-white px-8 py-3 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Update Member</button>
 
 
 
 
 
394
  </div>
395
- </EditForm>
396
  }
397
  </div>
398
- </div>
399
  </div>
400
  }
401
  </div>
402
 
403
  @code {
404
  private enum ViewMode { List, Details, Create, Edit }
 
 
405
  private ViewMode _currentView = ViewMode.List;
 
406
 
407
  private IEnumerable<UserDto>? _allUsers;
408
  private PagedResult<UserDto>? _pagedResult;
 
409
  private bool _isLoading = true;
410
  private const int PageSize = 10;
411
 
@@ -418,7 +499,33 @@
418
 
419
  protected override async Task OnInitializedAsync()
420
  {
421
- await LoadUsers();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  }
423
 
424
  private async Task LoadUsers()
@@ -669,6 +776,26 @@
669
  await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
670
  }
671
  }
672
- }
673
 
 
 
 
 
 
 
 
 
 
674
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  @inject NavigationManager Navigation
10
 
11
  <div class="max-w-7xl mx-auto py-10 px-6 animate-fade">
12
+ <!-- Top Tabs Navigation -->
13
+ <div class="flex items-center gap-8 border-b border-border mb-10">
14
+ <button @onclick="() => SetTab(AdminTab.Directory)" class="pb-4 px-2 text-sm font-black uppercase tracking-widest transition-all relative @(_activeTab == AdminTab.Directory ? "text-primary" : "text-muted hover:text-main")">
15
+ Member Directory
16
+ @if (_activeTab == AdminTab.Directory) { <div class="absolute bottom-0 left-0 right-0 h-1 bg-primary rounded-t-full"></div> }
17
+ </button>
18
+ <button @onclick="() => SetTab(AdminTab.Approvals)" class="pb-4 px-2 text-sm font-black uppercase tracking-widest transition-all relative @(_activeTab == AdminTab.Approvals ? "text-primary" : "text-muted hover:text-main")">
19
+ Membership Approvals
20
+ @if (_pendingSubscriptions?.Any() == true)
21
+ {
22
+ <span class="ml-2 bg-red-500 text-white text-[10px] px-2 py-0.5 rounded-full">@_pendingSubscriptions.Count()</span>
23
+ }
24
+ @if (_activeTab == AdminTab.Approvals) { <div class="absolute bottom-0 left-0 right-0 h-1 bg-primary rounded-t-full"></div> }
25
+ </button>
26
+ </div>
 
 
27
 
28
+ @if (_activeTab == AdminTab.Directory)
29
+ {
30
+ @if (_currentView == ViewMode.List)
31
  {
32
+ <div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
33
+ <div>
34
+ <h1 class="text-4xl font-extrabold tracking-tight text-main mb-2">Members Directory</h1>
35
+ <p class="text-muted text-lg">
36
+ Manage library members and administrator accounts.
37
+ <span class="ml-2 text-xs text-muted font-bold">(@(_allUsers?.Count() ?? 0) total)</span>
38
+ </p>
39
+ </div>
40
+ <div class="flex items-center gap-3">
41
+ <button @onclick="ShowCreate" class="bg-primary text-white px-6 py-3 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all whitespace-nowrap flex items-center">
42
+ <svg class="w-5 h-5 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>
43
+ Add Member
44
+ </button>
45
+ </div>
46
  </div>
47
+
48
+ @if (_isLoading)
49
+ {
50
+ <div class="space-y-4">
51
+ @for (int i = 0; i < 5; i++)
52
+ {
53
+ <div class="animate-pulse bg-card rounded-xl border border-border h-24 w-full"></div>
54
+ }
55
+ </div>
56
+ }
57
+ else if (_pagedResult != null)
58
+ {
59
+ <div class="bg-card rounded-2xl border border-border overflow-hidden shadow-sm">
60
+ <div class="overflow-x-auto">
61
+ <table class="w-full text-left border-collapse min-w-max whitespace-nowrap">
62
+ <thead class="bg-body border-b border-border">
63
+ <tr>
64
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Member</th>
65
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Contact</th>
66
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Role</th>
67
+ <th class="px-6 py-4 text-[10px] font-bold text-muted uppercase tracking-widest">Status</th>
68
+ <th class="px-6 py-4 text-right text-[10px] font-bold text-muted uppercase tracking-widest">Actions</th>
69
+ </tr>
70
+ </thead>
71
+ <tbody class="divide-y divide-border">
72
+ @foreach (var user in _pagedResult.Items)
73
+ {
74
+ <tr class="hover:bg-body transition-colors group">
75
+ <td class="px-6 py-4">
76
+ <div class="flex items-center gap-4">
77
+ <div class="h-10 w-10 flex-shrink-0 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-sm">
78
+ @(user.FullName?.Substring(0, Math.Min(2, user.FullName.Length)).ToUpper() ?? "U")
79
+ </div>
80
+ <div>
81
+ <button @onclick="() => ShowDetails(user.Id)" class="text-sm font-bold text-main hover:text-primary transition-colors text-left">@user.FullName</button>
82
+ @if (!string.IsNullOrEmpty(user.StudentId))
83
+ {
84
+ <div class="text-xs text-muted font-medium mt-0.5">ID: @user.StudentId</div>
85
+ }
86
+ </div>
87
  </div>
88
+ </td>
89
+ <td class="px-6 py-4">
90
+ <div class="text-sm text-main font-medium">@user.Email</div>
91
+ <div class="text-xs text-muted">@(string.IsNullOrEmpty(user.PhoneNumber) ? "No phone" : user.PhoneNumber)</div>
92
+ </td>
93
+ <td class="px-6 py-4">
94
+ <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase @(user.Role == "Admin" ? "bg-amber-500/10 text-amber-500" : "bg-blue-500/10 text-blue-500")">
95
+ @user.Role
 
 
 
 
 
 
 
 
96
  </span>
97
+ </td>
98
+ <td class="px-6 py-4">
99
+ @if (user.BanStatus == true)
100
+ {
101
+ <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase bg-red-500/10 text-red-500">
102
+ Suspended
103
+ </span>
104
+ }
105
+ else if (user.IsActive)
106
+ {
107
+ <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase bg-green-500/10 text-green-500">
108
+ Active
109
+ </span>
110
+ }
111
+ else
112
+ {
113
+ <span class="inline-flex rounded-full px-3 py-1 text-[10px] font-black tracking-widest uppercase bg-body text-muted border border-border">
114
+ Inactive
115
+ </span>
116
+ }
117
+ </td>
118
+ <td class="px-6 py-4 text-right">
119
+ <div class="flex items-center justify-end gap-2">
120
+ <button @onclick="() => ShowDetails(user.Id)" class="p-2 rounded-lg border border-border text-muted hover:bg-primary hover:text-white hover:border-primary transition-all">
121
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg>
122
+ </button>
123
+ <button @onclick="() => ShowEdit(user.Id)" class="p-2 rounded-lg border border-border text-muted hover:bg-primary hover:text-white hover:border-primary transition-all">
124
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
125
+ </button>
126
+ </div>
127
+ </td>
128
+ </tr>
129
+ }
130
+ </tbody>
131
+ </table>
132
+ </div>
133
 
134
+ @if (_pagedResult.TotalPages > 1)
135
+ {
136
+ <div class="px-6 py-4 border-t border-border flex items-center justify-between bg-body">
137
+ <p class="text-xs text-muted">
138
+ Showing <span class="font-bold text-main">@((_pagedResult.CurrentPage - 1) * _pagedResult.PageSize + 1)</span>
139
+ to
140
+ <span class="font-bold text-main">@(Math.Min(_pagedResult.CurrentPage * _pagedResult.PageSize, _pagedResult.TotalCount))</span>
141
+ of <span class="font-bold text-main">@_pagedResult.TotalCount</span> members
142
+ </p>
143
+ <div class="flex items-center gap-2">
144
+ <button @onclick="() => ChangePage(_pagedResult.CurrentPage - 1)" disabled="@(!_pagedResult.HasPreviousPage)"
145
+ class="p-2 rounded-lg border border-border text-muted hover:bg-card disabled:opacity-30 disabled:cursor-not-allowed transition-all">
146
+ <svg class="w-4 h-4" 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>
147
+ </button>
148
 
149
+ @for (int p = 1; p <= _pagedResult.TotalPages; p++)
 
 
 
 
 
 
 
150
  {
151
+ var pageNumber = p;
152
+ if (p == _pagedResult.CurrentPage)
153
+ {
154
+ <span class="w-8 h-8 flex items-center justify-center rounded-lg bg-main text-body text-xs font-bold">@p</span>
155
+ }
156
+ else if (p == 1 || p == _pagedResult.TotalPages || (p >= _pagedResult.CurrentPage - 1 && p <= _pagedResult.CurrentPage + 1))
157
+ {
158
+ <button @onclick="() => ChangePage(pageNumber)"
159
+ class="w-8 h-8 flex items-center justify-center rounded-lg border border-border text-xs font-bold text-muted hover:text-primary hover:border-primary transition-colors">@p</button>
160
+ }
161
+ else if (p == _pagedResult.CurrentPage - 2 || p == _pagedResult.CurrentPage + 2)
162
+ {
163
+ <span class="w-8 h-8 flex items-center justify-center text-xs text-muted">...</span>
164
+ }
165
  }
 
166
 
167
+ <button @onclick="() => ChangePage(_pagedResult.CurrentPage + 1)" disabled="@(!_pagedResult.HasNextPage)"
168
+ class="p-2 rounded-lg border border-border text-muted hover:bg-card disabled:opacity-30 disabled:cursor-not-allowed transition-all">
169
+ <svg class="w-4 h-4" 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>
170
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  </div>
172
  </div>
173
+ }
174
  </div>
175
+ }
176
+ }
177
+ else if (_currentView == ViewMode.Details && _selectedUser != null)
178
+ {
179
+ <div class="animate-fade">
180
+ <button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-muted hover:text-main mb-6 group">
181
+ <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>
182
+ Back to Directory
183
+ </button>
184
+
185
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
186
+ <div class="lg:col-span-1">
187
+ <div class="aspect-square w-full rounded-3xl bg-card border border-border flex flex-col items-center justify-center text-main p-6 shadow-sm relative overflow-hidden">
188
+ <div class="absolute inset-0 bg-primary/5"></div>
189
+ <div class="relative z-10 flex flex-col items-center">
190
+ <div class="w-32 h-32 rounded-full bg-main text-body flex items-center justify-center text-5xl font-black mb-6 shadow-xl">
191
+ @(_selectedUser.User.FullName?.Substring(0, Math.Min(2, _selectedUser.User.FullName.Length)).ToUpper() ?? "U")
192
+ </div>
193
+ <h2 class="text-2xl font-bold text-center mb-2">@_selectedUser.User.FullName</h2>
194
+ <div class="inline-flex rounded-full px-4 py-1.5 text-[10px] font-black uppercase tracking-widest @(_selectedUser.User.Role == "Admin" ? "bg-amber-500/10 text-amber-500 border border-amber-500/20" : "bg-blue-500/10 text-blue-500 border border-blue-500/20")">
195
+ @_selectedUser.User.Role
196
+ </div>
197
+ </div>
198
  </div>
199
  </div>
200
 
201
+ <div class="lg:col-span-2 space-y-8">
 
 
 
 
 
 
 
 
 
 
 
 
202
  <div>
203
+ <h1 class="text-4xl font-extrabold text-main tracking-tight mb-4">Member Profile</h1>
204
+ <div class="flex items-center gap-3">
205
+ @if (_selectedUser.User.BanStatus == true)
206
+ {
207
+ <span class="inline-flex items-center rounded-full bg-red-500/10 border border-red-500/20 px-4 py-1.5 text-[10px] font-black text-red-500 tracking-widest uppercase">Suspended</span>
208
+ }
209
+ else if (_selectedUser.User.IsActive)
210
+ {
211
+ <span class="inline-flex items-center rounded-full bg-green-500/10 border border-green-500/20 px-4 py-1.5 text-[10px] font-black text-green-500 tracking-widest uppercase">Active</span>
212
+ }
213
+ </div>
214
  </div>
215
+
216
+ <div class="grid grid-cols-1 sm:grid-cols-2 gap-8 border-y border-border py-8">
217
+ <div>
218
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Email Address</h4>
219
+ <p class="text-sm font-semibold text-main">@_selectedUser.User.Email</p>
220
+ </div>
221
+ <div>
222
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Phone Number</h4>
223
+ <p class="text-sm font-semibold text-main">@(string.IsNullOrEmpty(_selectedUser.User.PhoneNumber) ? "-" : _selectedUser.User.PhoneNumber)</p>
224
+ </div>
225
+ <div>
226
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Student ID</h4>
227
+ <p class="text-sm font-semibold text-main">@(string.IsNullOrEmpty(_selectedUser.User.StudentId) ? "-" : _selectedUser.User.StudentId)</p>
228
+ </div>
229
+ <div>
230
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Joined Date</h4>
231
+ <p class="text-sm font-semibold text-main">@_selectedUser.User.CreatedAt.ToString("MMMM dd, yyyy")</p>
232
+ </div>
233
+ <div class="sm:col-span-2">
234
+ <h4 class="text-[10px] font-bold text-muted uppercase tracking-widest mb-2">Physical Address</h4>
235
+ <p class="text-sm font-semibold text-main">@(string.IsNullOrEmpty(_selectedUser.User.Address) ? "-" : _selectedUser.User.Address)</p>
236
+ </div>
237
  </div>
 
238
 
239
+ <div class="flex flex-wrap items-center gap-4 pt-2">
240
+ <button @onclick="() => ShowEdit(_selectedUser.User.Id)" class="bg-primary text-white py-3 px-8 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Edit Details</button>
241
+
242
+ <button @onclick="UpdateRole" class="bg-body border border-border text-main py-3 px-8 rounded-xl font-bold hover:bg-card transition-colors">
243
+ Make @(_selectedUser.User.Role == "Admin" ? "Member" : "Admin")
244
+ </button>
245
 
246
+ <button @onclick="DeleteUser" class="p-3 text-red-500 hover:text-red-600 hover:bg-red-500/10 rounded-xl transition-colors ml-auto border border-transparent hover:border-red-500/20">
247
+ <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>
248
+ </button>
249
+ </div>
250
 
251
+ <!-- Wallet Management Section -->
252
+ <div class="mt-12 pt-12 border-t border-border">
253
+ <div class="flex items-center justify-between mb-8">
254
+ <h3 class="text-2xl font-bold text-main">Library Wallet</h3>
255
+ <div class="bg-primary/5 border border-primary/20 px-6 py-3 rounded-2xl">
256
+ <span class="text-[10px] font-black text-primary uppercase tracking-widest block mb-1">Current Balance</span>
257
+ <span class="text-2xl font-black text-main">@_selectedUser.User.Balance.ToString("N0") MMK</span>
258
+ </div>
259
+ </div>
260
+
261
+ <div class="bg-card border border-border rounded-3xl p-8 shadow-sm">
262
+ <h4 class="text-xs font-black text-muted uppercase tracking-[0.2em] mb-6">Add Credits (Cash Deposit)</h4>
263
+ <div class="flex flex-col sm:flex-row gap-4">
264
+ <div class="flex-1 relative">
265
+ <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
266
+ <span class="text-muted text-xs font-black">MMK</span>
267
+ </div>
268
+ <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" />
269
+ </div>
270
+ <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)">
271
+ Confirm Deposit
272
+ </button>
273
+ </div>
274
+ <p class="mt-4 text-xs text-muted font-medium italic">Credits will be added instantly for automated subscription purchases.</p>
275
  </div>
276
  </div>
277
 
278
+
279
+ <!-- Subscription Management -->
280
+ <div class="mt-12 pt-12 border-t border-border">
281
+ <h3 class="text-2xl font-bold text-main mb-8">Manage Subscription</h3>
282
+
283
+ @if (_selectedUser.CurrentSubscription != null && _selectedUser.CurrentSubscription.IsActive)
284
+ {
285
+ <div class="bg-green-500/5 border border-green-500/20 rounded-2xl p-6 mb-8 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
286
+ <div>
287
+ <span class="text-[10px] text-green-600 font-black uppercase tracking-widest mb-1 block">Active Plan</span>
288
+ <h4 class="text-2xl font-black text-green-700">@_selectedUser.CurrentSubscription.MembershipType</h4>
289
+ </div>
290
+ <div class="sm:text-right">
291
+ <div class="text-[10px] text-green-600/70 font-black uppercase tracking-widest mb-1">Expires On</div>
292
+ <div class="font-bold text-green-700">@_selectedUser.CurrentSubscription.ExpiryDate.ToString("MMMM dd, yyyy")</div>
293
  </div>
 
294
  </div>
295
+ }
296
+ else
297
+ {
298
+ <div class="bg-card border border-dashed border-border rounded-2xl p-6 mb-8 text-center">
299
+ <p class="text-muted font-medium">No active subscription for this member.</p>
300
+ </div>
301
+ }
302
+
303
+ <h4 class="text-xs font-black text-muted uppercase tracking-[0.2em] mb-6">Assign New Plan</h4>
304
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
305
+ @foreach (var plan in _selectedUser.AvailableMemberships)
306
+ {
307
+ var isCurrent = _selectedUser.CurrentSubscription?.IsActive == true && _selectedUser.CurrentSubscription.MembershipId == plan.Id;
308
+ <div class="bg-card rounded-2xl border p-6 flex flex-col transition-all relative @(isCurrent ? "border-primary shadow-lg shadow-primary/10" : "border-border hover:border-primary/50")">
309
+ @if (isCurrent)
310
+ {
311
+ <div class="absolute -top-3 right-6 bg-primary text-white text-[9px] font-black uppercase tracking-widest px-3 py-1 rounded-full shadow-sm">Current</div>
312
+ }
313
+ <h5 class="font-bold text-main text-xl mb-1">@plan.Type</h5>
314
+ <p class="text-muted font-medium text-sm mb-6 flex-grow">@plan.Price.ToString("N0") MMK / @plan.DurationMonths mo</p>
315
+
316
+ @if (!isCurrent)
317
+ {
318
+ <button @onclick="() => AssignSubscription(plan.Id, plan.Type)" class="w-full bg-body border border-border text-main py-2.5 rounded-xl text-sm font-bold hover:bg-primary hover:text-white hover:border-primary transition-colors">Assign Plan</button>
319
+ }
320
+ else
321
+ {
322
+ <button disabled class="w-full bg-body border border-border text-muted py-2.5 rounded-xl text-sm font-bold opacity-50 cursor-not-allowed">Current Plan</button>
323
+ }
324
+ </div>
325
+ }
326
  </div>
 
327
  </div>
328
  </div>
329
+ </div>
330
+ </div>
331
+ }
332
+ else if (_currentView == ViewMode.Create || _currentView == ViewMode.Edit)
333
+ {
334
+ <div class="animate-fade">
335
+ <button @onclick="BackToList" class="inline-flex items-center text-sm font-medium text-muted hover:text-main mb-6 group">
336
+ <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>
337
+ Back to Directory
338
+ </button>
339
+
340
+ <div class="max-w-3xl mx-auto">
341
+ <div class="mb-10 text-center">
342
+ <h1 class="text-4xl font-extrabold tracking-tight text-main mb-3">@(_currentView == ViewMode.Create ? "Register New Member" : "Edit Member Details")</h1>
343
+ <p class="text-lg text-muted">@(_currentView == ViewMode.Create ? "Add a new reader to the library system." : $"Update profile details for {_editUser?.FullName}.")</p>
344
+ </div>
345
 
346
+ <div class="bg-card border border-border p-8 sm:p-10 rounded-3xl shadow-sm">
347
+ @if (_currentView == ViewMode.Create)
 
 
 
 
348
  {
349
+ <EditForm Model="@_createRequest" OnValidSubmit="HandleCreate" class="space-y-6">
350
+ <DataAnnotationsValidator />
351
+ <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
352
+ <div class="sm:col-span-2">
353
+ <label class="block text-sm font-bold text-main mb-2">Full Name <span class="text-red-500">*</span></label>
354
+ <InputText @bind-Value="_createRequest.FullName" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="John Doe" />
355
+ <ValidationMessage For="@(() => _createRequest.FullName)" class="mt-1 text-xs text-red-500 font-medium" />
356
+ </div>
357
+ <div>
358
+ <label class="block text-sm font-bold text-main mb-2">Email Address <span class="text-red-500">*</span></label>
359
+ <InputText @bind-Value="_createRequest.Email" type="email" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="you@domain.com" />
360
+ <ValidationMessage For="@(() => _createRequest.Email)" class="mt-1 text-xs text-red-500 font-medium" />
361
+ </div>
362
+ <div>
363
+ <label class="block text-sm font-bold text-main mb-2">Password <span class="text-red-500">*</span></label>
364
+ <InputText @bind-Value="_createRequest.Password" type="password" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="••••••••" />
365
+ <ValidationMessage For="@(() => _createRequest.Password)" class="mt-1 text-xs text-red-500 font-medium" />
366
+ </div>
367
+ <div>
368
+ <label class="block text-sm font-bold text-main mb-2">Phone Number</label>
369
+ <InputText @bind-Value="_createRequest.PhoneNumber" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="+123456789" />
370
+ </div>
371
+ <div>
372
+ <label class="block text-sm font-bold text-main mb-2">Student ID</label>
373
+ <InputText @bind-Value="_createRequest.StudentId" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="Optional" />
374
+ </div>
375
+ <div class="sm:col-span-2">
376
+ <label class="block text-sm font-bold text-main mb-2">Address</label>
377
+ <InputTextArea @bind-Value="_createRequest.Address" rows="3" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" placeholder="Full physical address" />
378
+ </div>
379
  </div>
380
+ <div class="pt-8 border-t border-border flex justify-end gap-4 mt-8">
381
+ <button type="button" @onclick="BackToList" class="bg-body border border-border text-main px-8 py-3 rounded-xl font-bold hover:bg-card transition-colors">Cancel</button>
382
+ <button type="submit" class="bg-primary text-white px-8 py-3 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Create Member</button>
383
  </div>
384
+ </EditForm>
385
  }
386
+ else if (_currentView == ViewMode.Edit)
387
  {
388
+ <EditForm Model="@_updateRequest" OnValidSubmit="HandleUpdate" class="space-y-6">
389
+ <DataAnnotationsValidator />
390
+ <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
391
+ <div class="sm:col-span-2">
392
+ <label class="block text-sm font-bold text-main mb-2">Full Name <span class="text-red-500">*</span></label>
393
+ <InputText @bind-Value="_updateRequest.FullName" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" />
394
+ <ValidationMessage For="@(() => _updateRequest.FullName)" class="mt-1 text-xs text-red-500 font-medium" />
395
+ </div>
396
+ <div>
397
+ <label class="block text-sm font-bold text-main mb-2">Phone Number</label>
398
+ <InputText @bind-Value="_updateRequest.PhoneNumber" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" />
399
+ </div>
400
+ <div>
401
+ <label class="block text-sm font-bold text-main mb-2">Student ID</label>
402
+ <InputText @bind-Value="_updateRequest.StudentId" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" />
403
+ </div>
404
+ <div class="sm:col-span-2">
405
+ <label class="block text-sm font-bold text-main mb-2">Address</label>
406
+ <InputTextArea @bind-Value="_updateRequest.Address" rows="3" class="w-full bg-body border border-border rounded-xl px-4 py-3 focus:ring-2 focus:ring-primary focus:border-transparent outline-none text-main transition-all" />
407
+ </div>
 
 
 
 
 
 
408
  </div>
409
+ <div class="pt-8 border-t border-border flex justify-end gap-4 mt-8">
410
+ <button type="button" @onclick="BackToList" class="bg-body border border-border text-main px-8 py-3 rounded-xl font-bold hover:bg-card transition-colors">Cancel</button>
411
+ <button type="submit" class="bg-primary text-white px-8 py-3 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">Update Member</button>
412
+ </div>
413
+ </EditForm>
414
+ }
415
  </div>
416
  </div>
417
  </div>
418
+ }
419
  }
420
+ else if (_activeTab == AdminTab.Approvals)
421
  {
422
  <div class="animate-fade">
423
+ <h1 class="text-4xl font-extrabold tracking-tight text-main mb-2">Membership Approvals</h1>
424
+ <p class="text-muted text-lg mb-8">Review and approve pending "Pay Cash at Library" subscription requests.</p>
 
 
 
 
 
 
 
 
425
 
426
+ @if (_isLoading)
427
+ {
428
+ <div class="space-y-4">
429
+ @for (int i = 0; i < 3; i++)
430
  {
431
+ <div class="animate-pulse bg-card rounded-xl border border-border h-24 w-full"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  }
433
+ </div>
434
+ }
435
+ else if (_pendingSubscriptions == null || !_pendingSubscriptions.Any())
436
+ {
437
+ <div class="bg-card rounded-3xl border border-dashed border-border p-20 text-center">
438
+ <div class="w-20 h-20 bg-primary/5 rounded-full flex items-center justify-center mx-auto mb-6 text-primary">
439
+ <svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
440
+ </div>
441
+ <h3 class="text-xl font-bold text-main mb-2">All Caught Up!</h3>
442
+ <p class="text-muted">No pending membership requests to approve.</p>
443
+ </div>
444
+ }
445
+ else
446
+ {
447
+ <div class="grid grid-cols-1 gap-6">
448
+ @foreach (var sub in _pendingSubscriptions)
449
  {
450
+ <div class="bg-card border border-border rounded-3xl p-6 sm:p-8 flex flex-col md:flex-row md:items-center justify-between gap-6 shadow-sm hover:shadow-md transition-shadow">
451
+ <div class="flex items-center gap-6">
452
+ <div class="w-16 h-16 rounded-2xl bg-primary/10 flex items-center justify-center text-primary font-black text-xl">
453
+ @(sub.UserName?.Substring(0, Math.Min(2, sub.UserName.Length)).ToUpper() ?? "U")
 
 
 
454
  </div>
455
  <div>
456
+ <h4 class="text-xl font-bold text-main mb-1">@sub.UserName</h4>
457
+ <p class="text-sm text-muted mb-2">@sub.UserEmail</p>
458
+ <div class="inline-flex items-center rounded-full bg-blue-500/10 px-3 py-1 text-[10px] font-black text-blue-500 tracking-widest uppercase border border-blue-500/20">
459
+ @sub.MembershipType
460
+ </div>
 
 
 
 
 
461
  </div>
462
  </div>
463
+
464
+ <div class="flex items-center gap-4 ml-auto sm:ml-0">
465
+ <button @onclick="() => HandleApproval(sub.Id, false)" class="bg-body border border-border text-red-500 hover:bg-red-500 hover:text-white hover:border-red-500 px-6 py-3 rounded-xl font-bold transition-all">
466
+ Reject
467
+ </button>
468
+ <button @onclick="() => HandleApproval(sub.Id, true)" class="bg-primary text-white px-8 py-3 rounded-xl font-bold hover:bg-primary-hover shadow-lg shadow-primary/20 transition-all">
469
+ Approve
470
+ </button>
471
  </div>
472
+ </div>
473
  }
474
  </div>
475
+ }
476
  </div>
477
  }
478
  </div>
479
 
480
  @code {
481
  private enum ViewMode { List, Details, Create, Edit }
482
+ private enum AdminTab { Directory, Approvals }
483
+
484
  private ViewMode _currentView = ViewMode.List;
485
+ private AdminTab _activeTab = AdminTab.Directory;
486
 
487
  private IEnumerable<UserDto>? _allUsers;
488
  private PagedResult<UserDto>? _pagedResult;
489
+ private IEnumerable<SubscriptionDto>? _pendingSubscriptions;
490
  private bool _isLoading = true;
491
  private const int PageSize = 10;
492
 
 
499
 
500
  protected override async Task OnInitializedAsync()
501
  {
502
+ await LoadData();
503
+ }
504
+
505
+ private async Task LoadData()
506
+ {
507
+ _isLoading = true;
508
+ try
509
+ {
510
+ _allUsers = await ApiClient.GetUsersAsync();
511
+ _pendingSubscriptions = await ApiClient.GetPendingSubscriptionsAsync();
512
+ ApplyPagination(1);
513
+ }
514
+ catch (Exception ex)
515
+ {
516
+ await JS.InvokeVoidAsync("alert", "Error loading data: " + ex.Message);
517
+ }
518
+ finally
519
+ {
520
+ _isLoading = false;
521
+ }
522
+ }
523
+
524
+ private async Task SetTab(AdminTab tab)
525
+ {
526
+ _activeTab = tab;
527
+ _currentView = ViewMode.List;
528
+ await LoadData();
529
  }
530
 
531
  private async Task LoadUsers()
 
776
  await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
777
  }
778
  }
 
779
 
780
+ private async Task HandleApproval(Guid subscriptionId, bool approve)
781
+ {
782
+ try
783
+ {
784
+ var success = await ApiClient.ApproveSubscriptionAsync(new ApproveSubscriptionRequest
785
+ {
786
+ SubscriptionId = subscriptionId,
787
+ Approve = approve
788
+ });
789
 
790
+ if (success)
791
+ {
792
+ await JS.InvokeVoidAsync("alert", approve ? "Subscription approved!" : "Subscription rejected.");
793
+ await LoadData(); // Refresh list
794
+ }
795
+ }
796
+ catch (Exception ex)
797
+ {
798
+ await JS.InvokeVoidAsync("alert", "Error: " + ex.Message);
799
+ }
800
+ }
801
+ }
BlazorWebAssembly/Pages/Membership.razor CHANGED
@@ -26,9 +26,10 @@
26
  }
27
  else
28
  {
29
- <!-- Current Subscription Banner (Only for Auth) -->
30
  <AuthorizeView>
31
  <Authorized>
 
32
  @if (_currentSub != null)
33
  {
34
  <div class="bg-primary/5 border border-primary/20 rounded-[2.5rem] p-10 mb-12 flex flex-col md:flex-row items-center justify-between gap-8 shadow-sm relative overflow-hidden group">
@@ -51,34 +52,33 @@
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
  {
@@ -307,4 +307,3 @@
307
  }
308
  }
309
  }
310
-
 
26
  }
27
  else
28
  {
29
+ <!-- Auth Section -->
30
  <AuthorizeView>
31
  <Authorized>
32
+ <!-- Current Subscription Banner -->
33
  @if (_currentSub != null)
34
  {
35
  <div class="bg-primary/5 border border-primary/20 rounded-[2.5rem] p-10 mb-12 flex flex-col md:flex-row items-center justify-between gap-8 shadow-sm relative overflow-hidden group">
 
52
  </div>
53
  </div>
54
  </div>
55
+ }
56
 
57
+ <!-- Wallet & Loyalty Summary (Always visible for logged in) -->
58
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-12">
59
+ <div class="bg-card border border-border rounded-3xl p-6 flex items-center justify-between shadow-sm">
60
+ <div class="flex items-center gap-4">
61
+ <div class="w-12 h-12 bg-primary/10 rounded-2xl flex items-center justify-center text-primary">
62
+ <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>
63
+ </div>
64
+ <div>
65
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest">Loyalty Points</p>
66
+ <p class="text-xl font-black text-main">@(_loyaltyAccount?.CurrentBalance.ToString("N0") ?? "0")</p>
 
67
  </div>
68
  </div>
69
+ </div>
70
+ <div class="bg-card border border-border rounded-3xl p-6 flex items-center justify-between shadow-sm">
71
+ <div class="flex items-center gap-4">
72
+ <div class="w-12 h-12 bg-emerald-500/10 rounded-2xl flex items-center justify-center text-emerald-500">
73
+ <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>
74
+ </div>
75
+ <div>
76
+ <p class="text-[10px] font-black text-muted uppercase tracking-widest">Wallet Balance</p>
77
+ <p class="text-xl font-black text-main">@_walletBalance.ToString("N0") <span class="text-xs font-bold text-muted">MMK</span></p>
78
  </div>
79
  </div>
80
  </div>
81
+ </div>
 
82
 
83
  @if (_queuedRewards.Any())
84
  {
 
307
  }
308
  }
309
  }
 
BlazorWebAssembly/Services/LibraryApiClient.cs CHANGED
@@ -182,9 +182,24 @@ namespace BlazorWebAssembly.Services
182
 
183
  public async Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync()
184
  {
 
185
  return await _httpClient.GetFromJsonAsync<IEnumerable<SubscriptionDto>>("api/subscriptions/all") ?? Enumerable.Empty<SubscriptionDto>();
186
  }
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  public async Task<SubscriptionUpgradePreviewDto?> GetUpgradePreviewAsync(int membershipId)
189
  {
190
  try
 
182
 
183
  public async Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync()
184
  {
185
+ await EnsureAuthHeader();
186
  return await _httpClient.GetFromJsonAsync<IEnumerable<SubscriptionDto>>("api/subscriptions/all") ?? Enumerable.Empty<SubscriptionDto>();
187
  }
188
 
189
+ public async Task<IEnumerable<SubscriptionDto>> GetPendingSubscriptionsAsync()
190
+ {
191
+ await EnsureAuthHeader();
192
+ return await _httpClient.GetFromJsonAsync<IEnumerable<SubscriptionDto>>("api/subscriptions/pending") ?? Enumerable.Empty<SubscriptionDto>();
193
+ }
194
+
195
+ public async Task<bool> ApproveSubscriptionAsync(ApproveSubscriptionRequest request)
196
+ {
197
+ await EnsureAuthHeader();
198
+ var response = await _httpClient.PostAsJsonAsync("api/subscriptions/approve", request);
199
+ return response.IsSuccessStatusCode;
200
+ }
201
+
202
+
203
  public async Task<SubscriptionUpgradePreviewDto?> GetUpgradePreviewAsync(int membershipId)
204
  {
205
  try
DbConnect/Entities/UserSubscription.cs CHANGED
@@ -1,4 +1,4 @@
1
- using System;
2
  using System.Collections.Generic;
3
 
4
  namespace DbConnect.Entities;
@@ -20,6 +20,8 @@ public partial class UserSubscription
20
  public string Status { get; set; } = null!;
21
 
22
  public string? ExternalRedemptionId { get; set; }
 
 
23
 
24
  public virtual Membership Membership { get; set; } = null!;
25
 
 
1
+ using System;
2
  using System.Collections.Generic;
3
 
4
  namespace DbConnect.Entities;
 
20
  public string Status { get; set; } = null!;
21
 
22
  public string? ExternalRedemptionId { get; set; }
23
+ public string? PaymentMethod { get; set; }
24
+
25
 
26
  public virtual Membership Membership { get; set; } = null!;
27
 
LibraryManagement.Shared/Models/LibraryDtos.cs CHANGED
@@ -129,6 +129,8 @@ namespace LibraryManagement.Shared.Models
129
  public DateTime StartDate { get; set; }
130
  public DateTime ExpiryDate { get; set; }
131
  public bool IsActive { get; set; }
 
 
132
  public string? ExternalRedemptionId { get; set; }
133
  public bool IsExpired => DateTime.UtcNow > ExpiryDate;
134
  }
@@ -144,6 +146,12 @@ namespace LibraryManagement.Shared.Models
144
  public int MembershipId { get; set; }
145
  }
146
 
 
 
 
 
 
 
147
  public class SubscriptionUpgradePreviewDto
148
  {
149
  public decimal OriginalPrice { get; set; }
 
129
  public DateTime StartDate { get; set; }
130
  public DateTime ExpiryDate { get; set; }
131
  public bool IsActive { get; set; }
132
+ public string Status { get; set; } = "Active";
133
+ public string? PaymentMethod { get; set; }
134
  public string? ExternalRedemptionId { get; set; }
135
  public bool IsExpired => DateTime.UtcNow > ExpiryDate;
136
  }
 
146
  public int MembershipId { get; set; }
147
  }
148
 
149
+ public class ApproveSubscriptionRequest
150
+ {
151
+ public Guid SubscriptionId { get; set; }
152
+ public bool Approve { get; set; }
153
+ }
154
+
155
  public class SubscriptionUpgradePreviewDto
156
  {
157
  public decimal OriginalPrice { get; set; }