Spaces:
Sleeping
Sleeping
Yuyuqt commited on
Commit ·
b8cc5e6
1
Parent(s): 25f925e
add: Queued membership
Browse files- Backend/Features/Loyalty/LoyaltyController.cs +3 -3
- Backend/Features/Loyalty/LoyaltyModels.cs +10 -23
- Backend/Features/Loyalty/LoyaltyService.cs +1 -0
- Backend/Features/Subscriptions/SubscriptionController.cs +12 -0
- Backend/Features/Subscriptions/SubscriptionService.cs +43 -8
- Frontend/Controllers/MembershipController.cs +14 -12
- Frontend/Models/Dtos/LoyaltyRedemptionDto.cs +10 -7
- Frontend/Models/MembershipViewModel.cs +1 -1
- Frontend/Services/LibraryApiClient.cs +7 -0
- Frontend/Views/Membership/Index.cshtml +8 -6
- Frontend/Views/Subscriptions/LoyaltyClaims.cshtml +4 -4
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.
|
| 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("
|
| 28 |
-
public double
|
| 29 |
|
| 30 |
[JsonPropertyName("status")]
|
| 31 |
public string Status { get; set; } = string.Empty;
|
| 32 |
|
| 33 |
-
[JsonPropertyName("
|
| 34 |
-
public
|
| 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<
|
|
|
|
| 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<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
{
|
| 57 |
try
|
| 58 |
{
|
| 59 |
-
//
|
| 60 |
-
var
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
if (membership == null)
|
| 64 |
{
|
| 65 |
-
|
| 66 |
return false;
|
| 67 |
}
|
| 68 |
|
| 69 |
-
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
.
|
| 35 |
-
.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
| 39 |
.ToList();
|
| 40 |
|
| 41 |
var viewModel = new MembershipViewModel
|
| 42 |
{
|
| 43 |
-
CurrentSubscription =
|
| 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("
|
| 21 |
-
public
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
[JsonPropertyName("status")]
|
| 24 |
public string Status { get; set; } = string.Empty;
|
| 25 |
|
| 26 |
-
[JsonPropertyName("
|
| 27 |
-
public DateTime
|
| 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<
|
| 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
|
| 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"><
|
| 102 |
</div>
|
| 103 |
<div>
|
| 104 |
-
<div class="font-bold text-slate-900">@queued.
|
| 105 |
-
<div class="text-sm text-slate-500">
|
|
|
|
|
|
|
| 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">
|
| 110 |
-
<div class="text-xs text-slate-400 mt-1">
|
| 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") • 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">
|
| 41 |
-
<th class="px-6 py-4 text-xs font-bold text-slate-500 uppercase tracking-wider">
|
| 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.
|
| 72 |
-
<td class="px-6 py-4 text-slate-500 text-sm">@claim.
|
| 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" />
|