Yuyuqt commited on
Commit
b8cc5e6
·
1 Parent(s): 25f925e

add: Queued membership

Browse files
Backend/Features/Loyalty/LoyaltyController.cs CHANGED
@@ -103,13 +103,13 @@ namespace Backend.Features.Loyalty
103
  bool membershipGranted = false;
104
  if (int.TryParse(redemption.ExternalUserId?.Trim(), out int userId))
105
  {
106
- System.Diagnostics.Debug.WriteLine($"Fulfilling redemption {id} for user {userId}. RewardId: '{redemption.RewardId}'");
107
- membershipGranted = await _subscriptionService.HandleLoyaltyRedemptionAsync(userId, redemption.RewardId, redemption.Id);
108
  }
109
 
110
  if (!membershipGranted)
111
  {
112
- var errorMsg = $"Redemption fulfilled in loyalty system, but failed to grant library membership. The Reward ID '{redemption.RewardId}' (Redemption ID: {id}) may not be correctly mapped in the library system, or the user already has researchers this membership.";
113
  System.Diagnostics.Debug.WriteLine(errorMsg);
114
  return BadRequest(new { message = errorMsg });
115
  }
 
103
  bool membershipGranted = false;
104
  if (int.TryParse(redemption.ExternalUserId?.Trim(), out int userId))
105
  {
106
+ System.Diagnostics.Debug.WriteLine($"Fulfilling redemption {id} for user {userId}. RewardId: '{redemption.RewardId}', RewardName: '{redemption.RewardName}'");
107
+ membershipGranted = await _subscriptionService.HandleLoyaltyRedemptionAsync(userId, redemption.RewardId, redemption.RewardName, redemption.Id);
108
  }
109
 
110
  if (!membershipGranted)
111
  {
112
+ var errorMsg = $"Redemption fulfilled in loyalty system, but failed to grant library membership. RewardName='{redemption.RewardName}', RewardId='{redemption.RewardId}' (Redemption ID: {id}).";
113
  System.Diagnostics.Debug.WriteLine(errorMsg);
114
  return BadRequest(new { message = errorMsg });
115
  }
Backend/Features/Loyalty/LoyaltyModels.cs CHANGED
@@ -8,39 +8,26 @@ namespace Backend.Features.Loyalty
8
  [JsonPropertyName("id")]
9
  public string Id { get; set; } = string.Empty;
10
 
 
 
 
 
 
 
11
  [JsonPropertyName("rewardId")]
12
  public string RewardId { get; set; } = string.Empty;
13
 
14
- // Fallback for snake_case from external API
15
- [JsonPropertyName("reward_id")]
16
- public string? RewardIdSnakeCase { get => RewardId; set => RewardId = string.IsNullOrEmpty(value) ? RewardId : value; }
17
-
18
  [JsonPropertyName("rewardName")]
19
  public string RewardName { get; set; } = string.Empty;
20
-
21
- [JsonPropertyName("reward_name")]
22
- public string? RewardNameSnakeCase { get => RewardName; set => RewardName = string.IsNullOrEmpty(value) ? RewardName : value; }
23
-
24
- [JsonPropertyName("pointsSpent")]
25
- public double PointsSpent { get; set; }
26
 
27
- [JsonPropertyName("points_spent")]
28
- public double? PointsSpentSnakeCase { set => PointsSpent = value ?? PointsSpent; }
29
 
30
  [JsonPropertyName("status")]
31
  public string Status { get; set; } = string.Empty;
32
 
33
- [JsonPropertyName("externalUserId")]
34
- public string ExternalUserId { get; set; } = string.Empty;
35
-
36
- [JsonPropertyName("external_user_id")]
37
- public string? ExternalUserIdSnakeCase { get => ExternalUserId; set => ExternalUserId = string.IsNullOrEmpty(value) ? ExternalUserId : value; }
38
-
39
- [JsonPropertyName("createdAt")]
40
- public DateTime CreatedAt { get; set; }
41
-
42
- [JsonPropertyName("created_at")]
43
- public DateTime? CreatedAtSnakeCase { set => CreatedAt = value ?? CreatedAt; }
44
  }
45
 
46
  public class ClaimRewardRequestDto
 
8
  [JsonPropertyName("id")]
9
  public string Id { get; set; } = string.Empty;
10
 
11
+ [JsonPropertyName("systemId")]
12
+ public string SystemId { get; set; } = string.Empty;
13
+
14
+ [JsonPropertyName("externalUserId")]
15
+ public string ExternalUserId { get; set; } = string.Empty;
16
+
17
  [JsonPropertyName("rewardId")]
18
  public string RewardId { get; set; } = string.Empty;
19
 
 
 
 
 
20
  [JsonPropertyName("rewardName")]
21
  public string RewardName { get; set; } = string.Empty;
 
 
 
 
 
 
22
 
23
+ [JsonPropertyName("pointCost")]
24
+ public double PointCost { get; set; }
25
 
26
  [JsonPropertyName("status")]
27
  public string Status { get; set; } = string.Empty;
28
 
29
+ [JsonPropertyName("redeemedAt")]
30
+ public DateTime RedeemedAt { get; set; }
 
 
 
 
 
 
 
 
 
31
  }
32
 
33
  public class ClaimRewardRequestDto
Backend/Features/Loyalty/LoyaltyService.cs CHANGED
@@ -17,6 +17,7 @@ namespace Backend.Features.Loyalty
17
  Task<AccountLookupResponse?> GetUserAccountAsync(string externalUserId);
18
  Task<(bool Success, string Message)> ClaimRewardAsync(string externalUserId, string rewardId, string notes);
19
  Task<IEnumerable<LoyaltyRedemptionDto>> GetPendingRedemptionsAsync();
 
20
  Task<IEnumerable<LoyaltyRedemptionDto>> GetRedemptionsHistoryAsync();
21
  Task<bool> UpdateRedemptionStatusAsync(string redemptionId, string status);
22
  }
 
17
  Task<AccountLookupResponse?> GetUserAccountAsync(string externalUserId);
18
  Task<(bool Success, string Message)> ClaimRewardAsync(string externalUserId, string rewardId, string notes);
19
  Task<IEnumerable<LoyaltyRedemptionDto>> GetPendingRedemptionsAsync();
20
+ Task<IEnumerable<LoyaltyRedemptionDto>> GetUserRedemptionsAsync(string externalUserId);
21
  Task<IEnumerable<LoyaltyRedemptionDto>> GetRedemptionsHistoryAsync();
22
  Task<bool> UpdateRedemptionStatusAsync(string redemptionId, string status);
23
  }
Backend/Features/Subscriptions/SubscriptionController.cs CHANGED
@@ -37,6 +37,18 @@ namespace Backend.Features.Subscriptions
37
  return Ok(subscription);
38
  }
39
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  [HttpPost("subscriptions/subscribe")]
41
  [Authorize]
42
  public async Task<ActionResult<SubscriptionDto>> Subscribe([FromBody] SubscribeRequest request)
 
37
  return Ok(subscription);
38
  }
39
 
40
+ [HttpGet("subscriptions/me/all")]
41
+ [Authorize]
42
+ public async Task<ActionResult<IEnumerable<SubscriptionDto>>> GetMyAllSubscriptions()
43
+ {
44
+ var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
45
+ if (string.IsNullOrEmpty(userIdStr)) return Unauthorized();
46
+
47
+ var userId = int.Parse(userIdStr);
48
+ var subscriptions = await _subscriptionService.GetUserAllSubscriptionsAsync(userId);
49
+ return Ok(subscriptions);
50
+ }
51
+
52
  [HttpPost("subscriptions/subscribe")]
53
  [Authorize]
54
  public async Task<ActionResult<SubscriptionDto>> Subscribe([FromBody] SubscribeRequest request)
Backend/Features/Subscriptions/SubscriptionService.cs CHANGED
@@ -8,7 +8,8 @@ namespace Backend.Features.Subscriptions
8
  {
9
  Task<IEnumerable<MembershipDto>> GetMembershipsAsync();
10
  Task<SubscriptionDto?> GetUserSubscriptionAsync(int userId);
11
- Task<bool> HandleLoyaltyRedemptionAsync(int userId, string rewardId, string id);
 
12
  Task<SubscriptionDto> SubscribeUserAsync(int userId, int membershipId);
13
  Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync();
14
  }
@@ -52,27 +53,61 @@ namespace Backend.Features.Subscriptions
52
  return MapToDto(subscription);
53
  }
54
 
55
- public async Task<bool> HandleLoyaltyRedemptionAsync(int userId, string rewardId, string id)
 
 
 
 
 
 
 
 
 
 
 
 
56
  {
57
  try
58
  {
59
- // 1. Find the local membership that corresponds to this loyalty reward
60
- var membership = await _context.Memberships
61
- .FirstOrDefaultAsync(m => m.RewardId == rewardId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  if (membership == null)
64
  {
65
- // If no membership matches this rewardId, we can't fulfill it automatically
66
  return false;
67
  }
68
 
69
- // 2. Grant the membership to the user (will be queued if they already have one)
 
 
70
  await SubscribeUserAsync(userId, membership.Id);
71
 
72
  return true;
73
  }
74
- catch
75
  {
 
76
  return false;
77
  }
78
  }
 
8
  {
9
  Task<IEnumerable<MembershipDto>> GetMembershipsAsync();
10
  Task<SubscriptionDto?> GetUserSubscriptionAsync(int userId);
11
+ Task<IEnumerable<SubscriptionDto>> GetUserAllSubscriptionsAsync(int userId);
12
+ Task<bool> HandleLoyaltyRedemptionAsync(int userId, string rewardId, string rewardName, string redemptionId);
13
  Task<SubscriptionDto> SubscribeUserAsync(int userId, int membershipId);
14
  Task<IEnumerable<SubscriptionDto>> GetAllSubscriptionsAsync();
15
  }
 
53
  return MapToDto(subscription);
54
  }
55
 
56
+ public async Task<IEnumerable<SubscriptionDto>> GetUserAllSubscriptionsAsync(int userId)
57
+ {
58
+ var subscriptions = await _context.UserSubscriptions
59
+ .Include(s => s.Membership)
60
+ .Include(s => s.User)
61
+ .Where(s => s.UserId == userId && s.IsActive)
62
+ .OrderBy(s => s.StartDate)
63
+ .ToListAsync();
64
+
65
+ return subscriptions.Select(MapToDto);
66
+ }
67
+
68
+ public async Task<bool> HandleLoyaltyRedemptionAsync(int userId, string rewardId, string rewardName, string redemptionId)
69
  {
70
  try
71
  {
72
+ // Load all memberships into memory (small collection)
73
+ var allMemberships = await _context.Memberships.ToListAsync();
74
+
75
+ Membership? membership = null;
76
+
77
+ // 1. First try exact match by RewardId (most precise)
78
+ if (!string.IsNullOrEmpty(rewardId))
79
+ {
80
+ membership = allMemberships.FirstOrDefault(m => m.RewardId == rewardId);
81
+ }
82
+
83
+ // 2. Fallback: in-memory string matching on rewardName vs membership Type
84
+ // e.g. "Free Monthly Basic Membership" contains words "Basic" and "Monthly"
85
+ if (membership == null && !string.IsNullOrEmpty(rewardName))
86
+ {
87
+ var normalizedRewardName = rewardName.ToLowerInvariant();
88
+ membership = allMemberships.FirstOrDefault(m =>
89
+ {
90
+ var typeWords = m.Type.ToLowerInvariant().Split(' ', StringSplitOptions.RemoveEmptyEntries);
91
+ return typeWords.All(w => normalizedRewardName.Contains(w));
92
+ });
93
+ }
94
 
95
  if (membership == null)
96
  {
97
+ System.Diagnostics.Debug.WriteLine($"No membership matched for rewardId='{rewardId}', rewardName='{rewardName}'");
98
  return false;
99
  }
100
 
101
+ System.Diagnostics.Debug.WriteLine($"Matched membership '{membership.Type}' (Id={membership.Id}) for rewardName='{rewardName}'");
102
+
103
+ // 3. Grant the membership (will be queued if they already have one)
104
  await SubscribeUserAsync(userId, membership.Id);
105
 
106
  return true;
107
  }
108
+ catch (Exception ex)
109
  {
110
+ System.Diagnostics.Debug.WriteLine($"HandleLoyaltyRedemptionAsync error: {ex.Message}");
111
  return false;
112
  }
113
  }
Frontend/Controllers/MembershipController.cs CHANGED
@@ -21,26 +21,28 @@ namespace Frontend.Controllers
21
 
22
  public async Task<IActionResult> Index()
23
  {
24
- var subscription = await _apiClient.GetMySubscriptionAsync();
25
  var memberships = await _apiClient.GetMembershipsAsync();
26
- var redemptions = await _apiClient.GetMyRedemptionsAsync();
27
  var loyaltyAccount = await _apiClient.GetMyLoyaltyAccountAsync();
28
  var activeRewards = await _apiClient.GetActiveRewardsAsync();
29
 
30
- // Filter redemptions to only show memberships that are Pending (Queued)
31
- // We use LoyaltyRewardId to identify which redemptions are memberships
32
- var membershipRewardIds = memberships
33
- .Where(m => !string.IsNullOrEmpty(m.RewardId))
34
- .Select(m => m.RewardId)
35
- .ToList();
36
-
37
- var queuedMemberships = redemptions
38
- .Where(r => (r.Status == "Pending" || r.Status == "PENDING" || r.Status == "Fulfilled" || r.Status == "FULFILLED") && membershipRewardIds.Contains(r.RewardId))
 
 
 
39
  .ToList();
40
 
41
  var viewModel = new MembershipViewModel
42
  {
43
- CurrentSubscription = subscription,
44
  AvailableMemberships = memberships.ToList(),
45
  QueuedMemberships = queuedMemberships,
46
  LoyaltyAccount = loyaltyAccount,
 
21
 
22
  public async Task<IActionResult> Index()
23
  {
24
+ var allSubscriptions = await _apiClient.GetMyAllSubscriptionsAsync();
25
  var memberships = await _apiClient.GetMembershipsAsync();
 
26
  var loyaltyAccount = await _apiClient.GetMyLoyaltyAccountAsync();
27
  var activeRewards = await _apiClient.GetActiveRewardsAsync();
28
 
29
+ var now = DateTime.UtcNow;
30
+
31
+ // The currently active subscription is the one whose time range includes now
32
+ var currentSubscription = allSubscriptions
33
+ .Where(s => s.StartDate <= now && s.ExpiryDate > now)
34
+ .OrderByDescending(s => s.StartDate)
35
+ .FirstOrDefault();
36
+
37
+ // Queued = subscriptions that haven't started yet (StartDate in the future)
38
+ var queuedMemberships = allSubscriptions
39
+ .Where(s => s.StartDate > now)
40
+ .OrderBy(s => s.StartDate)
41
  .ToList();
42
 
43
  var viewModel = new MembershipViewModel
44
  {
45
+ CurrentSubscription = currentSubscription,
46
  AvailableMemberships = memberships.ToList(),
47
  QueuedMemberships = queuedMemberships,
48
  LoyaltyAccount = loyaltyAccount,
Frontend/Models/Dtos/LoyaltyRedemptionDto.cs CHANGED
@@ -8,22 +8,25 @@ namespace Frontend.Models.Dtos
8
  [JsonPropertyName("id")]
9
  public string Id { get; set; } = string.Empty;
10
 
 
 
 
11
  [JsonPropertyName("externalUserId")]
12
  public string ExternalUserId { get; set; } = string.Empty;
13
 
14
- [JsonPropertyName("rewardName")]
15
- public string RewardName { get; set; } = string.Empty;
16
-
17
  [JsonPropertyName("rewardId")]
18
  public string? RewardId { get; set; }
19
 
20
- [JsonPropertyName("pointsSpent")]
21
- public double PointsSpent { get; set; }
 
 
 
22
 
23
  [JsonPropertyName("status")]
24
  public string Status { get; set; } = string.Empty;
25
 
26
- [JsonPropertyName("createdAt")]
27
- public DateTime CreatedAt { get; set; }
28
  }
29
  }
 
8
  [JsonPropertyName("id")]
9
  public string Id { get; set; } = string.Empty;
10
 
11
+ [JsonPropertyName("systemId")]
12
+ public string SystemId { get; set; } = string.Empty;
13
+
14
  [JsonPropertyName("externalUserId")]
15
  public string ExternalUserId { get; set; } = string.Empty;
16
 
 
 
 
17
  [JsonPropertyName("rewardId")]
18
  public string? RewardId { get; set; }
19
 
20
+ [JsonPropertyName("rewardName")]
21
+ public string RewardName { get; set; } = string.Empty;
22
+
23
+ [JsonPropertyName("pointCost")]
24
+ public double PointCost { get; set; }
25
 
26
  [JsonPropertyName("status")]
27
  public string Status { get; set; } = string.Empty;
28
 
29
+ [JsonPropertyName("redeemedAt")]
30
+ public DateTime RedeemedAt { get; set; }
31
  }
32
  }
Frontend/Models/MembershipViewModel.cs CHANGED
@@ -7,7 +7,7 @@ namespace Frontend.Models
7
  {
8
  public SubscriptionDto? CurrentSubscription { get; set; }
9
  public List<MembershipDto> AvailableMemberships { get; set; } = new List<MembershipDto>();
10
- public List<LoyaltyRedemptionDto> QueuedMemberships { get; set; } = new List<LoyaltyRedemptionDto>();
11
  public LoyaltyAccountDto? LoyaltyAccount { get; set; }
12
  public Dictionary<string, double> RewardPointCosts { get; set; } = new Dictionary<string, double>();
13
  }
 
7
  {
8
  public SubscriptionDto? CurrentSubscription { get; set; }
9
  public List<MembershipDto> AvailableMemberships { get; set; } = new List<MembershipDto>();
10
+ public List<SubscriptionDto> QueuedMemberships { get; set; } = new List<SubscriptionDto>();
11
  public LoyaltyAccountDto? LoyaltyAccount { get; set; }
12
  public Dictionary<string, double> RewardPointCosts { get; set; } = new Dictionary<string, double>();
13
  }
Frontend/Services/LibraryApiClient.cs CHANGED
@@ -139,6 +139,13 @@ namespace Frontend.Services
139
  } catch { return null; }
140
  }
141
 
 
 
 
 
 
 
 
142
  public async Task<SubscriptionDto?> SubscribeAsync(SubscribeRequest request)
143
  {
144
  var response = await _httpClient.PostAsJsonAsync("api/subscriptions/subscribe", request);
 
139
  } catch { return null; }
140
  }
141
 
142
+ public async Task<IEnumerable<SubscriptionDto>> GetMyAllSubscriptionsAsync()
143
+ {
144
+ try {
145
+ return await _httpClient.GetFromJsonAsync<IEnumerable<SubscriptionDto>>("api/subscriptions/me/all") ?? Enumerable.Empty<SubscriptionDto>();
146
+ } catch { return Enumerable.Empty<SubscriptionDto>(); }
147
+ }
148
+
149
  public async Task<SubscriptionDto?> SubscribeAsync(SubscribeRequest request)
150
  {
151
  var response = await _httpClient.PostAsJsonAsync("api/subscriptions/subscribe", request);
Frontend/Views/Membership/Index.cshtml CHANGED
@@ -90,7 +90,7 @@
90
  <section>
91
  <h2 class="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2">
92
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-amber-500"><path d="M10 10 2.5 2.5"/><path d="M21.5 2.5 14 10"/><path d="m21.5 21.5-7.5-7.5"/><path d="M10 14 2.5 21.5"/><circle cx="12" cy="12" r="3"/></svg>
93
- Queued Membership
94
  </h2>
95
  <div class="space-y-3">
96
  @foreach (var queued in Model.QueuedMemberships)
@@ -98,16 +98,18 @@
98
  <div class="flex items-center justify-between bg-amber-50 border border-amber-100 p-5 rounded-2xl shadow-sm">
99
  <div class="flex items-center gap-4">
100
  <div class="h-12 w-12 bg-white rounded-xl flex items-center justify-center text-amber-600 shadow-sm border border-amber-100">
101
- <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
102
  </div>
103
  <div>
104
- <div class="font-bold text-slate-900">@queued.RewardName</div>
105
- <div class="text-sm text-slate-500">Redeemed on @queued.CreatedAt.ToString("MMM dd, yyyy")</div>
 
 
106
  </div>
107
  </div>
108
  <div class="text-right">
109
- <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700 border border-amber-200 uppercase tracking-wider">Pending Approval</span>
110
- <div class="text-xs text-slate-400 mt-1">Librarian will review soon</div>
111
  </div>
112
  </div>
113
  }
 
90
  <section>
91
  <h2 class="text-xl font-bold text-slate-900 mb-4 flex items-center gap-2">
92
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-amber-500"><path d="M10 10 2.5 2.5"/><path d="M21.5 2.5 14 10"/><path d="m21.5 21.5-7.5-7.5"/><path d="M10 14 2.5 21.5"/><circle cx="12" cy="12" r="3"/></svg>
93
+ Queued Memberships (@Model.QueuedMemberships.Count)
94
  </h2>
95
  <div class="space-y-3">
96
  @foreach (var queued in Model.QueuedMemberships)
 
98
  <div class="flex items-center justify-between bg-amber-50 border border-amber-100 p-5 rounded-2xl shadow-sm">
99
  <div class="flex items-center gap-4">
100
  <div class="h-12 w-12 bg-white rounded-xl flex items-center justify-center text-amber-600 shadow-sm border border-amber-100">
101
+ <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
102
  </div>
103
  <div>
104
+ <div class="font-bold text-slate-900">@queued.MembershipType Membership</div>
105
+ <div class="text-sm text-slate-500">
106
+ Activates on @queued.StartDate.ToString("MMM dd, yyyy") &bull; Expires @queued.ExpiryDate.ToString("MMM dd, yyyy")
107
+ </div>
108
  </div>
109
  </div>
110
  <div class="text-right">
111
+ <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-amber-100 text-amber-700 border border-amber-200 uppercase tracking-wider">Queued</span>
112
+ <div class="text-xs text-slate-400 mt-1">Will activate automatically</div>
113
  </div>
114
  </div>
115
  }
Frontend/Views/Subscriptions/LoyaltyClaims.cshtml CHANGED
@@ -37,8 +37,8 @@
37
  <tr class="bg-slate-50 border-b border-slate-200">
38
  <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Member ID</th>
39
  <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Reward</th>
40
- <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Points Spent</th>
41
- <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Date</th>
42
  <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider text-right">Actions</th>
43
  </tr>
44
  </thead>
@@ -68,8 +68,8 @@
68
  <td class="px-6 py-4">
69
  <span class="font-semibold text-slate-900">@claim.RewardName</span>
70
  </td>
71
- <td class="px-6 py-4 text-slate-600">@claim.PointsSpent.ToString("N0") pts</td>
72
- <td class="px-6 py-4 text-slate-500 text-sm">@claim.CreatedAt.ToString("MMM dd, yyyy")</td>
73
  <td class="px-6 py-4 text-right">
74
  <form asp-action="FulfillRedemption" method="post" class="inline">
75
  <input type="hidden" name="id" value="@claim.Id" />
 
37
  <tr class="bg-slate-50 border-b border-slate-200">
38
  <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Member ID</th>
39
  <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Reward</th>
40
+ <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Point Cost</th>
41
+ <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">Redeemed At</th>
42
  <th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider text-right">Actions</th>
43
  </tr>
44
  </thead>
 
68
  <td class="px-6 py-4">
69
  <span class="font-semibold text-slate-900">@claim.RewardName</span>
70
  </td>
71
+ <td class="px-6 py-4 text-slate-600">@claim.PointCost.ToString("N0") pts</td>
72
+ <td class="px-6 py-4 text-slate-500 text-sm">@claim.RedeemedAt.ToString("MMM dd, yyyy")</td>
73
  <td class="px-6 py-4 text-right">
74
  <form asp-action="FulfillRedemption" method="post" class="inline">
75
  <input type="hidden" name="id" value="@claim.Id" />