diff --git a/Backend/Features/Loyalty/LoyaltyController.cs b/Backend/Features/Loyalty/LoyaltyController.cs index fd3223b52d4c050788b4a90c487038107e341e4b..4bf143c9375acfea5bc8e8d90d80846a6286f0de 100644 --- a/Backend/Features/Loyalty/LoyaltyController.cs +++ b/Backend/Features/Loyalty/LoyaltyController.cs @@ -24,6 +24,13 @@ namespace Backend.Features.Loyalty _context = context; } + [HttpGet("rewards")] + public async Task GetRewards() + { + var rewards = await _loyaltyService.GetActiveRewardsAsync(); + return Ok(rewards); + } + // ── Member endpoints ────────────────────────────────────────────────── [HttpGet("my-account")] @@ -32,8 +39,31 @@ namespace Backend.Features.Loyalty var userIdStr = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userIdStr)) return Unauthorized(); + // 2. Get user from DB to get Email/Mobile for potential registration + var user = await _context.Users.FindAsync(Guid.Parse(userIdStr)); + if (user == null) return NotFound("User not found."); + + // 3. Get Loyalty Account var account = await _loyaltyService.GetUserAccountAsync(userIdStr); - if (account == null) return NotFound("Loyalty account not found."); + + if (account == null) + { + // Auto-register if account not found + var registered = await _loyaltyService.RegisterUserAsync(userIdStr, user.Email, user.PhoneNumber ?? "0000000000"); + if (registered) + { + // Process a small signup bonus if it's their first time being linked + await _loyaltyService.ProcessEventAsync(userIdStr, "SIGNUP", 20, $"LINK-{user.Id}", "Account Auto-Linked Bonus", user.Email, user.PhoneNumber); + + // Try lookup again + account = await _loyaltyService.GetUserAccountAsync(userIdStr); + } + } + + if (account == null) + { + return Ok(null); // Will show "Account Not Linked" in UI + } return Ok(account); } @@ -55,7 +85,7 @@ namespace Backend.Features.Loyalty if (string.IsNullOrEmpty(userIdStr)) return Unauthorized(); var redemptions = await _loyaltyService.GetUserRedemptionsAsync(userIdStr); - var pending = redemptions.Where(r => r.Status.Equals("Pending", StringComparison.OrdinalIgnoreCase)); + var pending = redemptions.Where(r => string.Equals(r.Status, "Pending", StringComparison.OrdinalIgnoreCase)); return Ok(pending); } diff --git a/Backend/Features/Loyalty/LoyaltyService.cs b/Backend/Features/Loyalty/LoyaltyService.cs index 3eea4bf30a8121ef5663e1c5c945e98a67d6cb31..d80eb9b7f6ba32483bb88ba4215495124be7527b 100644 --- a/Backend/Features/Loyalty/LoyaltyService.cs +++ b/Backend/Features/Loyalty/LoyaltyService.cs @@ -14,15 +14,15 @@ namespace Backend.Features.Loyalty public interface ILoyaltyService { Task RegisterUserAsync(string externalUserId, string email, string mobile); - Task ProcessEventAsync(string externalUserId, string eventKey, double eventValue, string referenceId, string description, string email, string mobile); - Task GetUserAccountAsync(string externalUserId); + Task ProcessEventAsync(string externalUserId, string eventKey, double eventValue, string? referenceId = null, string? description = null, string? email = null, string? mobile = null); + Task GetUserAccountAsync(string externalUserId); Task<(bool Success, string Message)> ClaimRewardAsync(string externalUserId, string rewardId, string notes); Task> GetPendingRedemptionsAsync(); Task> GetUserRedemptionsAsync(string externalUserId); Task> GetRedemptionsHistoryAsync(); Task UpdateRedemptionStatusAsync(string redemptionId, string status); Task> GetPointsHistoryAsync(string accountId); - Task> GetAllMembersPointsHistoryAsync(); + Task> GetActiveRewardsAsync(); } public class LoyaltyService : ILoyaltyService @@ -45,23 +45,19 @@ namespace Backend.Features.Loyalty { try { + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId); + var payload = new { - systemId = SystemId, externalUserId = externalUserId, email = email, mobile = mobile, - tier = "Member" // Default + systemId = SystemId }; var response = await _httpClient.PostAsJsonAsync("/api/v1/accounts", payload); - if (!response.IsSuccessStatusCode) - { - var error = await response.Content.ReadAsStringAsync(); - _logger.LogWarning($"Failed to register user in loyalty system: {error}"); - return false; - } - return true; + return response.IsSuccessStatusCode; } catch (Exception ex) { @@ -70,12 +66,16 @@ namespace Backend.Features.Loyalty } } - public async Task ProcessEventAsync(string externalUserId, string eventKey, double eventValue, string referenceId, string description, string email, string mobile) + public async Task ProcessEventAsync(string externalUserId, string eventKey, double eventValue, string? referenceId = null, string? description = null, string? email = null, string? mobile = null) { try { + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId); + var payload = new { + systemId = SystemId, externalUserId = externalUserId, eventKey = eventKey, eventValue = eventValue, @@ -85,14 +85,8 @@ namespace Backend.Features.Loyalty mobile = mobile }; - var response = await _httpClient.PostAsJsonAsync("/api/v1/events/process", payload); - if (!response.IsSuccessStatusCode) - { - var error = await response.Content.ReadAsStringAsync(); - _logger.LogWarning($"Failed to process loyalty event {eventKey} for user {externalUserId}: {error}"); - return false; - } - return true; + var response = await _httpClient.PostAsJsonAsync($"/api/v1/events/process?systemId={SystemId}", payload); + return response.IsSuccessStatusCode; } catch (Exception ex) { @@ -101,26 +95,32 @@ namespace Backend.Features.Loyalty } } - public async Task GetUserAccountAsync(string externalUserId) + public async Task GetUserAccountAsync(string externalUserId) { try { - var response = await _httpClient.GetAsync($"/api/v1/accounts/lookup/{SystemId}/{externalUserId}"); + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId); + + var response = await _httpClient.GetAsync($"/api/v1/accounts/lookup/{externalUserId}?systemId={SystemId}"); + if (response.IsSuccessStatusCode) { - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var account = await response.Content.ReadFromJsonAsync(options); + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; + var account = await response.Content.ReadFromJsonAsync(options); + if (account != null && string.IsNullOrWhiteSpace(account.Id) && !string.IsNullOrWhiteSpace(account.AccountId)) { account.Id = account.AccountId; } return account; } + return null; } catch (Exception ex) { - _logger.LogError(ex, $"Error fetching loyalty account for user {externalUserId}."); + _logger.LogError(ex, $"Error looking up loyalty account for {externalUserId}."); return null; } } @@ -131,36 +131,22 @@ namespace Backend.Features.Loyalty { var payload = new { + systemId = SystemId, externalUserId = externalUserId, rewardId = rewardId, notes = notes }; - var response = await _httpClient.PostAsJsonAsync("/api/v1/redemption/claim", payload); - if (response.IsSuccessStatusCode) - { - return (true, "Reward claimed successfully!"); - } + var response = await _httpClient.PostAsJsonAsync($"/api/v1/redemption/claim?systemId={SystemId}", payload); + if (response.IsSuccessStatusCode) return (true, "Reward claimed successfully!"); var error = await response.Content.ReadAsStringAsync(); - _logger.LogWarning($"Failed to claim reward {rewardId} for user {externalUserId}: {error}"); - - try - { - var errorDoc = JsonDocument.Parse(error); - if (errorDoc.RootElement.TryGetProperty("message", out var msgElement)) - { - return (false, msgElement.GetString() ?? "Failed to claim reward."); - } - } - catch { } - - return (false, string.IsNullOrEmpty(error) ? "Failed to claim reward." : error); + return (false, error); } catch (Exception ex) { _logger.LogError(ex, "Error while claiming reward."); - return (false, "An error occurred while processing your request."); + return (false, "An error occurred."); } } @@ -168,58 +154,56 @@ namespace Backend.Features.Loyalty { try { - var response = await _httpClient.GetAsync($"/api/v1/redemption/history/{SystemId}/{externalUserId}"); + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId); + + var response = await _httpClient.GetAsync($"/api/v1/redemption/history/{externalUserId}?systemId={SystemId}"); if (response.IsSuccessStatusCode) { - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; return await response.Content.ReadFromJsonAsync>(options) ?? Enumerable.Empty(); } return Enumerable.Empty(); } - catch (Exception ex) - { - _logger.LogError(ex, $"Error fetching redemptions for user {externalUserId}."); - return Enumerable.Empty(); - } + catch { return Enumerable.Empty(); } } public async Task> GetPendingRedemptionsAsync() { try { - var response = await _httpClient.GetAsync("/api/v1/admin/redemptions/pending"); + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId); + + var response = await _httpClient.GetAsync($"/api/v1/admin/redemptions/pending?systemId={SystemId}"); if (response.IsSuccessStatusCode) { - var rawJson = await response.Content.ReadAsStringAsync(); - _logger.LogInformation($"Raw Pending Redemptions JSON: {rawJson}"); - - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - return JsonSerializer.Deserialize>(rawJson, options) ?? Enumerable.Empty(); + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; + return await response.Content.ReadFromJsonAsync>(options) ?? Enumerable.Empty(); } return Enumerable.Empty(); } - catch (Exception ex) - { - _logger.LogError(ex, "Error fetching pending redemptions."); - return Enumerable.Empty(); - } + catch { return Enumerable.Empty(); } } public async Task> GetRedemptionsHistoryAsync() { try { - var response = await _httpClient.GetAsync("/api/v1/admin/redemptions/history"); + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId); + + var response = await _httpClient.GetAsync($"/api/v1/admin/redemptions/history?systemId={SystemId}"); if (response.IsSuccessStatusCode) { - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; return await response.Content.ReadFromJsonAsync>(options) ?? Enumerable.Empty(); } return Enumerable.Empty(); } catch (Exception ex) { - _logger.LogError(ex, "Error fetching redemptions history."); + _logger.LogError(ex, "Error fetching redemption history."); return Enumerable.Empty(); } } @@ -228,107 +212,58 @@ namespace Backend.Features.Loyalty { try { - var response = await _httpClient.PutAsJsonAsync($"/api/v1/admin/redemptions/{redemptionId}/status", new { status }); + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId); + + var response = await _httpClient.PutAsJsonAsync($"/api/v1/admin/redemptions/{redemptionId}/status?systemId={SystemId}", new { systemId = SystemId, status }); return response.IsSuccessStatusCode; } - catch (Exception ex) - { - _logger.LogError(ex, $"Error updating redemption status to {status} for {redemptionId}."); - return false; - } + catch { return false; } } public async Task> GetPointsHistoryAsync(string accountId) { try { - var response = await _httpClient.GetAsync($"/api/v1/accounts/{accountId}/history"); + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId); + + var response = await _httpClient.GetAsync($"/api/v1/accounts/{accountId}/history?systemId={SystemId}"); if (response.IsSuccessStatusCode) { - var rawJson = await response.Content.ReadAsStringAsync(); - _logger.LogInformation("Raw Points History JSON for account {AccountId}: {Json}", accountId, rawJson); - - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - return JsonSerializer.Deserialize>(rawJson, options) ?? Enumerable.Empty(); + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; + return await response.Content.ReadFromJsonAsync>(options) ?? Enumerable.Empty(); } - - var error = await response.Content.ReadAsStringAsync(); - _logger.LogWarning("Points history request failed for account {AccountId}. Status={Status}. Body={Body}", accountId, response.StatusCode, error); return Enumerable.Empty(); } catch (Exception ex) { - _logger.LogError(ex, $"Error fetching points history for account {accountId}."); + _logger.LogError(ex, $"Error fetching points history for {accountId}."); return Enumerable.Empty(); } } - public async Task> GetAllMembersPointsHistoryAsync() + public async Task> GetActiveRewardsAsync() { - return Enumerable.Empty(); - } - } - - public class PointHistoryEntryDto - { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("accountId")] - public string AccountId { get; set; } = string.Empty; - - [JsonPropertyName("externalUserId")] - public string? ExternalUserId { get; set; } - - [JsonPropertyName("pointDelta")] - public double PointDelta { get; set; } - - [JsonPropertyName("eventKey")] - public string EventKey { get; set; } = string.Empty; - - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - - [JsonPropertyName("referenceId")] - public string? ReferenceId { get; set; } - - [JsonPropertyName("createdAt")] - public DateTime CreatedAt { get; set; } - - // Optional redemption-related fields (present for REDEEM events) - [JsonPropertyName("rewardId")] - public string? RewardId { get; set; } - - [JsonPropertyName("rewardName")] - public string? RewardName { get; set; } - - [JsonPropertyName("redemptionStatus")] - public string? RedemptionStatus { get; set; } - - [JsonPropertyName("redeemedAt")] - public DateTime? RedeemedAt { get; set; } - } - - public class AccountLookupResponse - { - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - // Some loyalty API versions return `accountId` instead of `id`. - [JsonPropertyName("accountId")] - public string? AccountId { get; set; } - - [JsonPropertyName("externalUserId")] - public string ExternalUserId { get; set; } = string.Empty; - - [JsonPropertyName("currentBalance")] - public double CurrentBalance { get; set; } - - [JsonPropertyName("tier")] - public string Tier { get; set; } = string.Empty; + try + { + _httpClient.DefaultRequestHeaders.Clear(); + _httpClient.DefaultRequestHeaders.Add("x-system-id", SystemId); - [JsonPropertyName("lifetimePoints")] - public double LifetimePoints { get; set; } + var response = await _httpClient.GetAsync($"/api/v1/rewards/active?systemId={SystemId}"); + + if (response.IsSuccessStatusCode) + { + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; + return await response.Content.ReadFromJsonAsync>(options) ?? Enumerable.Empty(); + } + return Enumerable.Empty(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching active rewards."); + return Enumerable.Empty(); + } + } } } - diff --git a/Backend/Program.cs b/Backend/Program.cs index d06ebb0b7c7708294bcee43399c97620ea210f74..a0f1f10b01c6d207814f9deb37d35f02653f1a8a 100644 --- a/Backend/Program.cs +++ b/Backend/Program.cs @@ -25,6 +25,14 @@ var builder = WebApplication.CreateBuilder(args); // Credential = GoogleCredential.FromFile("LibraryFirebase.json") //}); +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowWasm", + policy => policy.WithOrigins("https://localhost:7058", "http://localhost:5158") + .AllowAnyMethod() + .AllowAnyHeader()); +}); + builder.Services.AddControllers(); builder.Services.AddAuthorization(); @@ -104,6 +112,9 @@ var app = builder.Build(); app.UseSwagger(); app.UseSwaggerUI(); + +app.UseCors("AllowWasm"); + app.UseAuthentication(); app.UseAuthorization(); diff --git a/BlazorFrontend/Services/LibraryApiClient.cs b/BlazorFrontend/Services/LibraryApiClient.cs index 2d09d7eee874e58913f83cbca2810b7d8737c002..8fdfffffe57a98ea22ebef92cbcc44e2229b9ec0 100644 --- a/BlazorFrontend/Services/LibraryApiClient.cs +++ b/BlazorFrontend/Services/LibraryApiClient.cs @@ -1,3 +1,4 @@ +using System.Text.Json.Serialization; using System.Net.Http.Json; using System.Text.Json; using LibraryManagement.Shared.Models; @@ -182,7 +183,7 @@ namespace BlazorFrontend.Services { // JsonPropertyName attributes in LoyaltyAccountDto will handle the mapping // and correctly populate CurrentBalance from the backend's currentBalance - return await response.Content.ReadFromJsonAsync(); + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; return await response.Content.ReadFromJsonAsync(options); } else { @@ -221,11 +222,8 @@ namespace BlazorFrontend.Services { try { - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var response = await _httpClient.GetAsync($"api/v1/accounts/{accountId}/history"); - if (response.IsSuccessStatusCode) - return await response.Content.ReadFromJsonAsync>(options) ?? Enumerable.Empty(); - return Enumerable.Empty(); + // Call the Backend's proxy endpoint instead of trying to hit the external API directly + return await _httpClient.GetFromJsonAsync>("api/Loyalty/my-points-history") ?? Enumerable.Empty(); } catch { return Enumerable.Empty(); } } @@ -233,7 +231,7 @@ namespace BlazorFrontend.Services public async Task> GetAllMembersPointsHistoryAsync() { try { - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; var response = await _httpClient.GetAsync("api/Loyalty/admin/all-points-history"); var raw = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) @@ -293,9 +291,8 @@ namespace BlazorFrontend.Services { try { - - var client = new HttpClient(); - return await client.GetFromJsonAsync>("http://150.95.88.91:4100/api/v1/rewards/active/THS-LMS") ?? Enumerable.Empty(); + // Call our Backend proxy which handles the external API and SystemId + return await _httpClient.GetFromJsonAsync>("api/Loyalty/rewards") ?? Enumerable.Empty(); } catch { return Enumerable.Empty(); } } @@ -342,7 +339,7 @@ namespace BlazorFrontend.Services var response = await _httpClient.GetAsync("api/Loyalty/admin/redemptions/pending"); if (response.IsSuccessStatusCode) { - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; return await response.Content.ReadFromJsonAsync>(options) ?? Enumerable.Empty(); } return Enumerable.Empty(); @@ -413,3 +410,6 @@ namespace BlazorFrontend.Services #endregion } } + + + diff --git a/BlazorWebAssembly/App.razor b/BlazorWebAssembly/App.razor index 6fd3ed1b5a3b04db302df62b1cbbe91cb1edbecb..98229ab95493c6ccea4d0f4b9b6d47a8567fd34f 100644 --- a/BlazorWebAssembly/App.razor +++ b/BlazorWebAssembly/App.razor @@ -1,12 +1,30 @@ - - - - - - - Not found - -

Sorry, there's nothing at this address.

-
-
-
+ + + + + +
+

Access Denied

+

You don't have permission to view this page. Please log in.

+ Go to Login +
+
+ +
+
+ Loading... +
+

Verifying credentials...

+
+
+
+ +
+ + Not found + +

Sorry, there's nothing at this address.

+
+
+
+
diff --git a/BlazorWebAssembly/BlazorWebAssembly.csproj b/BlazorWebAssembly/BlazorWebAssembly.csproj index d2e605e9ed75283b5015bfc740db5c58a6f4ebed..6e99dc8757aa24dd8eb2b8900946d30303dbb21d 100644 --- a/BlazorWebAssembly/BlazorWebAssembly.csproj +++ b/BlazorWebAssembly/BlazorWebAssembly.csproj @@ -7,6 +7,8 @@ + + diff --git a/BlazorWebAssembly/Layout/MainLayout.razor b/BlazorWebAssembly/Layout/MainLayout.razor index 76eb72528390cf4c8dcc2044cab588ece0835d39..91c1ead51062dbddcea7f7f310f400f21140ac72 100644 --- a/BlazorWebAssembly/Layout/MainLayout.razor +++ b/BlazorWebAssembly/Layout/MainLayout.razor @@ -1,16 +1,100 @@ -@inherits LayoutComponentBase -
- +@inherits LayoutComponentBase +@using BlazorWebAssembly.Services +@using Microsoft.AspNetCore.Components.Authorization +@inject ThemeService ThemeService +@inject AuthenticationStateProvider AuthStateProvider + +
+ + + + +
+ +
+
+
+ + +
+
-
- @Body -
- +
+ + + + Logout + + + Sign In + + +
+
+ + +
+
+ @Body +
+
+
+ +@code { + protected override async Task OnInitializedAsync() + { + await ThemeService.InitializeAsync(); + } +} diff --git a/BlazorWebAssembly/Layout/NavMenu.razor b/BlazorWebAssembly/Layout/NavMenu.razor index 1109911162658f095580d318ad9218409d26bb36..61e42a90a6cd445c05e0d9fa4557578bbe4a31e6 100644 --- a/BlazorWebAssembly/Layout/NavMenu.razor +++ b/BlazorWebAssembly/Layout/NavMenu.razor @@ -1,39 +1,71 @@ - + - private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; + diff --git a/BlazorWebAssembly/Layout/ThemeSwitcher.razor b/BlazorWebAssembly/Layout/ThemeSwitcher.razor new file mode 100644 index 0000000000000000000000000000000000000000..d6984dc7fad119ab0ed3ba8d810315607968807c --- /dev/null +++ b/BlazorWebAssembly/Layout/ThemeSwitcher.razor @@ -0,0 +1,29 @@ +@using BlazorWebAssembly.Services +@inject ThemeService ThemeService + +
+ + + +
+ +@code { + private string CurrentTheme => ThemeService.CurrentTheme; + + private async Task SetTheme(string theme) + { + await ThemeService.SetThemeAsync(theme); + } +} diff --git a/BlazorWebAssembly/Pages/Books.razor b/BlazorWebAssembly/Pages/Books.razor new file mode 100644 index 0000000000000000000000000000000000000000..ae05b11d3aadd9e74c3b48709393367c25f54c31 --- /dev/null +++ b/BlazorWebAssembly/Pages/Books.razor @@ -0,0 +1,143 @@ +@page "/books" +@using LibraryManagement.Shared.Models +@using BlazorWebAssembly.Services +@inject LibraryApiClient ApiClient +@inject NavigationManager Navigation + +
+ +
+
+

Book Collection

+

Discover your next favorite story among our curated selection.

+
+
+ +
+
+ + +
+ + @foreach (var cat in _categories) + { + + } +
+ + + @if (_isLoading) + { +
+ @for (int i = 0; i < 8; i++) + { +
+ } +
+ } + else if (!FilteredBooks.Any()) + { +
+

No results found

+

Try adjusting your search or filters.

+
+ } + else + { + +
+ @foreach (var book in FilteredBooks) + { +
+ +
+ @if (!string.IsNullOrEmpty(book.CoverUrl)) + { + @book.Title + } + else + { +
+ No Cover +
+ } + + @if (book.AvailableCopies <= 0) + { +
+ Unavailable +
+ } +
+ + +
+
+ @foreach (var cat in book.Categories.Take(2)) + { + @cat.Name + } +
+

@book.Title

+

@book.Author

+ +
+ @book.Status + + @book.AvailableCopies / @book.TotalCopies + +
+
+
+ } +
+ } +
+ +@code { + private bool _isLoading = true; + private string _searchQuery = ""; + private int? _selectedCategoryId; + private List _books = new(); + private List _categories = new(); + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + _isLoading = true; + try + { + _books = (await ApiClient.GetBooksAsync()).ToList(); + _categories = (await ApiClient.GetCategoriesAsync()).ToList(); + } + catch { } + finally { _isLoading = false; } + } + + private IEnumerable FilteredBooks => _books + .Where(b => (string.IsNullOrEmpty(_searchQuery) || b.Title.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase) || b.Author.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase)) + && (!_selectedCategoryId.HasValue || b.Categories.Any(c => c.Id == _selectedCategoryId))); + + private void FilterByCategory(int? id) + { + _selectedCategoryId = id; + } + + private void ShowDetails(int id) + { + // Navigation.NavigateTo($"books/{id}"); + } +} diff --git a/BlazorWebAssembly/Pages/Borrowings.razor b/BlazorWebAssembly/Pages/Borrowings.razor new file mode 100644 index 0000000000000000000000000000000000000000..047b9c86e68a2a48ef831bd2e1abbca3d8ddb720 --- /dev/null +++ b/BlazorWebAssembly/Pages/Borrowings.razor @@ -0,0 +1,122 @@ +@page "/borrowings" +@using LibraryManagement.Shared.Models +@using BlazorWebAssembly.Services +@inject LibraryApiClient ApiClient +@inject NavigationManager Navigation + +
+ +
+
+

My Loans

+

+ Keep track of your reading journey and manage your active borrowings. +

+
+ +
+ + @if (_isLoading) + { +
+ @for (int i = 0; i < 3; i++) + { +
+ } +
+ } + else if (!_borrowings.Any()) + { +
+

No active loans

+

You haven't borrowed any books yet. Explore our collection to get started.

+ +
+ } + else + { +
+ @foreach (var loan in _borrowings) + { +
+ +
+ +
+ + +
+

@loan.BookTitle

+

ID: @loan.BookId

+
+ + + + + +
+ + @loan.Status + + @if (loan.Status == "Borrowed") + { + + } +
+
+ } +
+ } +
+ +@code { + private bool _isLoading = true; + private List _borrowings = new(); + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + _isLoading = true; + try + { + _borrowings = (await ApiClient.GetMyBorrowingsAsync()).ToList(); + } + catch { } + finally { _isLoading = false; } + } + + private async Task ReturnRequestAsync(Guid id) + { + var success = await ApiClient.RequestReturnAsync(id); + if (success) await LoadData(); + } + + private string GetStatusColor(string status) => status switch + { + "Borrowed" => "bg-blue-500/10 text-blue-500", + "Returned" => "bg-green-500/10 text-green-500", + "PendingReturn" => "bg-orange-500/10 text-orange-500", + _ => "bg-body text-muted" + }; +} diff --git a/BlazorWebAssembly/Pages/Counter.razor b/BlazorWebAssembly/Pages/Counter.razor deleted file mode 100644 index ef23cb31607f8888025536ef3a18f653ebb225db..0000000000000000000000000000000000000000 --- a/BlazorWebAssembly/Pages/Counter.razor +++ /dev/null @@ -1,18 +0,0 @@ -@page "/counter" - -Counter - -

Counter

- -

Current count: @currentCount

- - - -@code { - private int currentCount = 0; - - private void IncrementCount() - { - currentCount++; - } -} diff --git a/BlazorWebAssembly/Pages/Home.razor b/BlazorWebAssembly/Pages/Home.razor index 9001e0bd2727c4e5aac3632d326cbf1c4415264b..d0995196845b8d35637e450f965398e5fdc6d167 100644 --- a/BlazorWebAssembly/Pages/Home.razor +++ b/BlazorWebAssembly/Pages/Home.razor @@ -1,7 +1,149 @@ -@page "/" +@page "/" +@using LibraryManagement.Shared.Models +@using BlazorWebAssembly.Services +@using Microsoft.AspNetCore.Components.Authorization +@inject LibraryApiClient ApiClient +@inject NavigationManager Navigation +@inject AuthenticationStateProvider AuthStateProvider -Home + + + +
+
+

+ Welcome back, @context.User.Identity?.Name +

+

Here is what's happening with your library account today.

+
-

Hello, world!

+
+ +
+

Loyalty Balance

+
+

@(_loyaltyAccount?.CurrentBalance ?? 0)

+ POINTS +
+
+
+
+
-Welcome to your new app. + +
+

Current Loans

+

2

+
+ Healthy +
+
+ + +
+

Membership

+

Premium Gold

+

Active Status

+ +
+
+ +
+

Recommended for You

+
+ @for (int i = 0; i < 5; i++) + { +
+ } +
+
+
+
+ + + +
+ +
+

+ Where Every Page
Finds a Purpose. +

+

+ Join over 5,000 readers in a digital-first library experience. Access a global collection of literature, earn rewards for reading, and connect with fellow book lovers. +

+
+ + +
+
+ + +
+
+

12,000+

+

Verified Books

+
+
+

5.2k

+

Active Readers

+
+
+

150+

+

Daily Borrowings

+
+
+ + +
+
+

Experience Library
Management redefined.

+
+
+
+ +
+
+

Loyalty Reward System

+

Earn points for every book you return on time and exchange them for membership perks.

+
+
+
+
+ +
+
+

Instant Notifications

+

Get real-time updates when your favorite books are back in stock or when your loan is due.

+
+
+
+
+
+
+ Preview of Interface +
+
+
+
+
+ +@code { + private LoyaltyAccountDto? _loyaltyAccount; + + protected override async Task OnInitializedAsync() + { + try + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + if (authState.User.Identity?.IsAuthenticated == true) + { + _loyaltyAccount = await ApiClient.GetMyLoyaltyAccountAsync(); + } + } + catch { } + } +} diff --git a/BlazorWebAssembly/Pages/Login.razor b/BlazorWebAssembly/Pages/Login.razor new file mode 100644 index 0000000000000000000000000000000000000000..1db520c968ce3c69d20e7ae2b584f6fd42155691 --- /dev/null +++ b/BlazorWebAssembly/Pages/Login.razor @@ -0,0 +1,97 @@ +@page "/login" +@using BlazorWebAssembly.Services +@using LibraryManagement.Shared.Models +@inject AuthService AuthService +@inject NavigationManager Navigation + +
+
+ +
+
+ +
+

Welcome Back

+

Enter your credentials to access your library account.

+
+ + +
+ + + +
+ + +
+ +
+ + +
+ + @if (!string.IsNullOrEmpty(_errorMessage)) + { +
+ @_errorMessage +
+ } + + +
+
+ + +

+ Don't have an account? + +

+
+
+ +@code { + private LoginRequest _loginModel = new(); + private bool _isLoading = false; + private string? _errorMessage; + + private async Task HandleLogin() + { + _isLoading = true; + _errorMessage = null; + try + { + var success = await AuthService.LoginAsync(_loginModel); + if (success) + { + Navigation.NavigateTo("/"); + } + else + { + _errorMessage = "Invalid email or password. Please try again."; + } + } + catch (Exception ex) + { + _errorMessage = "A connection error occurred. Please check your network."; + } + finally + { + _isLoading = false; + } + } +} diff --git a/BlazorWebAssembly/Pages/Membership.razor b/BlazorWebAssembly/Pages/Membership.razor new file mode 100644 index 0000000000000000000000000000000000000000..bb1c5155b41b794b2e78c4dd2c784b76a56aab24 --- /dev/null +++ b/BlazorWebAssembly/Pages/Membership.razor @@ -0,0 +1,130 @@ +@page "/membership" +@using LibraryManagement.Shared.Models +@using BlazorWebAssembly.Services +@inject LibraryApiClient ApiClient +@inject NavigationManager Navigation + +
+ +
+

Choose Your Journey

+

+ From casual readers to devoted bookworms, we have a plan that perfectly matches your literary appetite. +

+
+ + @if (_isLoading) + { +
+ @for (int i = 0; i < 3; i++) + { +
+ } +
+ } + else + { + + @if (_currentSub != null) + { +
+
+
+ P +
+
+

@_currentSub.MembershipType Plan

+

Active until @_currentSub.ExpiryDate.ToString("MMMM dd, yyyy")

+
+
+
+ Active +
+
+ } + + +
+ @foreach (var plan in _memberships) + { +
+ @if (IsCurrentPlan(plan.Id)) + { +
+ Current Plan +
+ } + +
+

@plan.Type

+
+ $@plan.Price + /@plan.DurationMonths mo +
+
+ +
+
+
+ +
+ Borrow up to @plan.MaxBooks books +
+
+
+ +
+ Keep for @plan.BorrowingDays days +
+
+
+ +
+ Digital access included +
+
+ + +
+ } +
+ } +
+ +@code { + private bool _isLoading = true; + private List _memberships = new(); + private SubscriptionDto? _currentSub; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + _isLoading = true; + try + { + _memberships = (await ApiClient.GetMembershipsAsync()).ToList(); + _currentSub = await ApiClient.GetMySubscriptionAsync(); + } + catch { } + finally { _isLoading = false; } + } + + private bool IsCurrentPlan(int id) => _currentSub?.MembershipId == id; + + private async Task SubscribeAsync(MembershipDto plan) + { + var result = await ApiClient.SubscribeAsync(new SubscribeRequest { MembershipId = plan.Id }); + if (result != null) + { + await LoadData(); + } + } +} diff --git a/BlazorWebAssembly/Pages/Register.razor b/BlazorWebAssembly/Pages/Register.razor new file mode 100644 index 0000000000000000000000000000000000000000..e049f3a38089d33695ec6854f94a27b1fe1b6ca9 --- /dev/null +++ b/BlazorWebAssembly/Pages/Register.razor @@ -0,0 +1,110 @@ +@page "/register" +@using BlazorWebAssembly.Services +@using LibraryManagement.Shared.Models +@inject AuthService AuthService +@inject NavigationManager Navigation + +
+
+ +
+

Create Account

+

Join our community and start your reading journey today.

+
+ + +
+ + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + @if (!string.IsNullOrEmpty(_errorMessage)) + { +
+ @_errorMessage +
+ } + + +
+
+ + +

+ Already have an account? + +

+
+
+ +@code { + private RegisterRequest _registerModel = new(); + private bool _isLoading = false; + private string? _errorMessage; + + private async Task HandleRegister() + { + _isLoading = true; + _errorMessage = null; + try + { + var success = await AuthService.RegisterAsync(_registerModel); + if (success) + { + Navigation.NavigateTo("/"); + } + else + { + _errorMessage = "Registration failed. This email may already be in use."; + } + } + catch (Exception ex) + { + _errorMessage = "An unexpected error occurred. Please try again later."; + } + finally + { + _isLoading = false; + } + } +} diff --git a/BlazorWebAssembly/Pages/Rewards.razor b/BlazorWebAssembly/Pages/Rewards.razor new file mode 100644 index 0000000000000000000000000000000000000000..3561c3246af4007d3aa45c8f36a6d5e8912dfe66 --- /dev/null +++ b/BlazorWebAssembly/Pages/Rewards.razor @@ -0,0 +1,100 @@ +@page "/rewards" +@using LibraryManagement.Shared.Models +@using BlazorWebAssembly.Services +@inject LibraryApiClient ApiClient +@inject NavigationManager Navigation + +
+ +
+

Loyalty Rewards

+

+ Your points are your power. Exchange them for exclusive memberships and reading perks. +

+
+ + @if (_isLoading) + { +
+
+
+ } + else + { + +
+
+

Your Balance

+
+

@(_account?.CurrentBalance ?? 0)

+ POINTS +
+
+ +
+

Member Tier

+

@(_account?.Tier ?? "Member")

+
+ +
+ + +
+ @foreach (var reward in _rewards) + { +
+
+
+ +
+ @reward.PointCost pts +
+ +

@reward.Name

+

@reward.Description

+ + +
+ } +
+ } +
+ +@code { + private bool _isLoading = true; + private LoyaltyAccountDto? _account; + private List _rewards = new(); + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + _isLoading = true; + try + { + _account = await ApiClient.GetMyLoyaltyAccountAsync(); + _rewards = (await ApiClient.GetActiveRewardsAsync()).ToList(); + } + catch { } + finally { _isLoading = false; } + } + + private async Task ClaimAsync(LoyaltyRewardDto reward) + { + var (success, msg) = await ApiClient.ClaimRewardAsync(reward.Id); + if (success) + { + await LoadData(); + // Show success message + } + } +} diff --git a/BlazorWebAssembly/Pages/UserInsights.razor b/BlazorWebAssembly/Pages/UserInsights.razor new file mode 100644 index 0000000000000000000000000000000000000000..63e7395603964b2d359e4e4cbb0f0a271291ab3c --- /dev/null +++ b/BlazorWebAssembly/Pages/UserInsights.razor @@ -0,0 +1,100 @@ +@page "/user-insights" +@using LibraryManagement.Shared.Models +@using BlazorWebAssembly.Services +@inject LibraryApiClient ApiClient +@inject NavigationManager Navigation +@using Microsoft.AspNetCore.Authorization +@attribute [Authorize(Roles = "Librarian")] + +
+ +
+
+

User Insights

+

+ Analytics and data-driven visibility into library membership and reading patterns. +

+
+
+
+ Total Members + 1,284 +
+
+ Active Now + 42 +
+
+
+ + +
+ +
+

Top Readers

+
+ @for (int i = 1; i <= 5; i++) + { +
+ @i +
+

Member User @i

+

@(20 - i) Books Borrowed

+
+
+
+
+
+ } +
+
+ + +
+

Point Leaders

+
+ @for (int i = 1; i <= 5; i++) + { +
+
+ L +
+
+

User Alpha @i

+

@(5000 - (i * 500)) Points

+
+ TIER @(6-i) +
+ } +
+
+ + +
+

Growth Patterns

+
+
+
+
+
+
+
+
+
+ Mon + Tue + Wed + Thu + Fri +
+
+
+
+
+ +@code { + protected override async Task OnInitializedAsync() + { + // Data loading logic here + } +} diff --git a/BlazorWebAssembly/Pages/Weather.razor b/BlazorWebAssembly/Pages/Weather.razor deleted file mode 100644 index 8a083fc20b7978b634447f4551863389e807064d..0000000000000000000000000000000000000000 --- a/BlazorWebAssembly/Pages/Weather.razor +++ /dev/null @@ -1,57 +0,0 @@ -@page "/weather" -@inject HttpClient Http - -Weather - -

Weather

- -

This component demonstrates fetching data from the server.

- -@if (forecasts == null) -{ -

Loading...

-} -else -{ - - - - - - - - - - - @foreach (var forecast in forecasts) - { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-} - -@code { - private WeatherForecast[]? forecasts; - - protected override async Task OnInitializedAsync() - { - forecasts = await Http.GetFromJsonAsync("sample-data/weather.json"); - } - - public class WeatherForecast - { - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public string? Summary { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - } -} diff --git a/BlazorWebAssembly/Pages/Wishlist.razor b/BlazorWebAssembly/Pages/Wishlist.razor new file mode 100644 index 0000000000000000000000000000000000000000..ccd3d30099f05d0e23fe4dc963aaad3734f2ce54 --- /dev/null +++ b/BlazorWebAssembly/Pages/Wishlist.razor @@ -0,0 +1,79 @@ +@page "/wishlist" +@using LibraryManagement.Shared.Models +@using BlazorWebAssembly.Services +@inject LibraryApiClient ApiClient +@inject NavigationManager Navigation + +
+ +
+

My Wishlist

+

+ A curated collection of books you're waiting for. We'll notify you as soon as they're back on the shelf. +

+
+ + @if (_isLoading) + { +
+ @for (int i = 0; i < 3; i++) + { +
+ } +
+ } + else if (!_wishlistItems.Any()) + { +
+

Your wishlist is empty

+

Found a book that's currently borrowed? Add it to your wishlist!

+ +
+ } + else + { +
+ @foreach (var book in _wishlistItems) + { +
+
+ @if (!string.IsNullOrEmpty(book.CoverUrl)) + { + @book.Title + } + else + { + + } +
+
+

@book.Title

+

@book.Author

+ +
+ + @(book.AvailableCopies > 0 ? "Available" : "Waiting") + + +
+
+
+ } +
+ } +
+ +@code { + private bool _isLoading = false; + private List _wishlistItems = new(); + + protected override async Task OnInitializedAsync() + { + // Mocking for now, would use a real service + _isLoading = true; + await Task.Delay(500); + _isLoading = false; + } +} diff --git a/BlazorWebAssembly/Program.cs b/BlazorWebAssembly/Program.cs index 5adaf8796edbc7f3a36341c252a5b2acf59eec2b..aaf279ecd5302e48ac7f85bdc912903c3c4f06d7 100644 --- a/BlazorWebAssembly/Program.cs +++ b/BlazorWebAssembly/Program.cs @@ -1,4 +1,8 @@ using BlazorWebAssembly; +using BlazorWebAssembly.Providers; +using BlazorWebAssembly.Services; +using Blazored.LocalStorage; +using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; @@ -6,6 +10,26 @@ var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); -builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +// Add HttpClient with Backend address +builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://localhost:7028/") }); + +// Add Blazored LocalStorage +builder.Services.AddBlazoredLocalStorage(); + +// Add Authorization Core +builder.Services.AddAuthorizationCore(); + +// Register Custom AuthStateProvider +builder.Services.AddScoped(); +builder.Services.AddScoped(sp => sp.GetRequiredService()); + +// Register AuthService +builder.Services.AddScoped(); + +// Register ThemeService +builder.Services.AddScoped(); + +// Register LibraryApiClient +builder.Services.AddScoped(); await builder.Build().RunAsync(); diff --git a/BlazorWebAssembly/Providers/JwtAuthenticationStateProvider.cs b/BlazorWebAssembly/Providers/JwtAuthenticationStateProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..8ca058d7aca63ff2535be36fa74aec9709147bcc --- /dev/null +++ b/BlazorWebAssembly/Providers/JwtAuthenticationStateProvider.cs @@ -0,0 +1,87 @@ +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Json; +using Blazored.LocalStorage; +using Microsoft.AspNetCore.Components.Authorization; + +namespace BlazorWebAssembly.Providers +{ + public class JwtAuthenticationStateProvider : AuthenticationStateProvider + { + private readonly ILocalStorageService _localStorage; + private readonly HttpClient _httpClient; + private readonly AuthenticationState _anonymous; + + public JwtAuthenticationStateProvider(ILocalStorageService localStorage, HttpClient httpClient) + { + _localStorage = localStorage; + _httpClient = httpClient; + _anonymous = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); + } + + public override async Task GetAuthenticationStateAsync() + { + var token = await _localStorage.GetItemAsync("authToken"); + + if (string.IsNullOrWhiteSpace(token)) + return _anonymous; + + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); + + return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt"))); + } + + public void NotifyUserAuthentication(string token) + { + var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt")); + var authState = Task.FromResult(new AuthenticationState(authenticatedUser)); + NotifyAuthenticationStateChanged(authState); + } + + public void NotifyUserLogout() + { + var authState = Task.FromResult(_anonymous); + NotifyAuthenticationStateChanged(authState); + } + + private IEnumerable ParseClaimsFromJwt(string jwt) + { + var claims = new List(); + var payload = jwt.Split('.')[1]; + + var jsonBytes = ParseBase64WithoutPadding(payload); + + var keyValuePairs = JsonSerializer.Deserialize>(jsonBytes); + + if (keyValuePairs != null) + { + foreach (var kvp in keyValuePairs) + { + if (kvp.Value is JsonElement element && element.ValueKind == JsonValueKind.Array) + { + foreach (var item in element.EnumerateArray()) + { + claims.Add(new Claim(kvp.Key, item.ToString())); + } + } + else + { + claims.Add(new Claim(kvp.Key, kvp.Value.ToString() ?? "")); + } + } + } + + return claims; + } + + private byte[] ParseBase64WithoutPadding(string base64) + { + switch (base64.Length % 4) + { + case 2: base64 += "=="; break; + case 3: base64 += "="; break; + } + return Convert.FromBase64String(base64); + } + } +} diff --git a/BlazorWebAssembly/Services/AuthService.cs b/BlazorWebAssembly/Services/AuthService.cs new file mode 100644 index 0000000000000000000000000000000000000000..86a3d9b46737179a0f72dec32e64ca986d7de1e8 --- /dev/null +++ b/BlazorWebAssembly/Services/AuthService.cs @@ -0,0 +1,69 @@ +using System.Net.Http.Json; +using Blazored.LocalStorage; +using LibraryManagement.Shared.Models; +using BlazorWebAssembly.Providers; +using Microsoft.AspNetCore.Components.Authorization; + +namespace BlazorWebAssembly.Services +{ + public class AuthService + { + private readonly HttpClient _httpClient; + private readonly ILocalStorageService _localStorage; + private readonly AuthenticationStateProvider _authStateProvider; + + public AuthService(HttpClient httpClient, ILocalStorageService localStorage, AuthenticationStateProvider authStateProvider) + { + _httpClient = httpClient; + _localStorage = localStorage; + _authStateProvider = authStateProvider; + } + + public async Task LoginAsync(LoginRequest request) + { + var response = await _httpClient.PostAsJsonAsync("api/auth/login", request); + + if (!response.IsSuccessStatusCode) + return false; + + var authResponse = await response.Content.ReadFromJsonAsync(); + + if (authResponse != null) + { + await _localStorage.SetItemAsync("authToken", authResponse.Token); + ((JwtAuthenticationStateProvider)_authStateProvider).NotifyUserAuthentication(authResponse.Token); + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", authResponse.Token); + return true; + } + + return false; + } + + public async Task RegisterAsync(RegisterRequest request) + { + var response = await _httpClient.PostAsJsonAsync("api/auth/register", request); + + if (!response.IsSuccessStatusCode) + return false; + + var authResponse = await response.Content.ReadFromJsonAsync(); + if (authResponse != null) + { + // Auto login after registration + await _localStorage.SetItemAsync("authToken", authResponse.Token); + ((JwtAuthenticationStateProvider)_authStateProvider).NotifyUserAuthentication(authResponse.Token); + _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", authResponse.Token); + return true; + } + + return false; + } + + public async Task Logout() + { + await _localStorage.RemoveItemAsync("authToken"); + ((JwtAuthenticationStateProvider)_authStateProvider).NotifyUserLogout(); + _httpClient.DefaultRequestHeaders.Authorization = null; + } + } +} diff --git a/BlazorWebAssembly/Services/LibraryApiClient.cs b/BlazorWebAssembly/Services/LibraryApiClient.cs new file mode 100644 index 0000000000000000000000000000000000000000..d58fe4bbddb6a2c955debcb046aa67c89cebef4a --- /dev/null +++ b/BlazorWebAssembly/Services/LibraryApiClient.cs @@ -0,0 +1,131 @@ +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; +using LibraryManagement.Shared.Models; + +namespace BlazorWebAssembly.Services +{ + public class LibraryApiClient + { + private readonly HttpClient _httpClient; + + public LibraryApiClient(HttpClient httpClient) + { + _httpClient = httpClient; + } + + #region Books + public async Task> GetBooksAsync(int? categoryId = null) + { + var url = categoryId.HasValue ? $"api/books?categoryId={categoryId}" : "api/books"; + return await _httpClient.GetFromJsonAsync>(url) ?? Enumerable.Empty(); + } + + public async Task GetBookAsync(int id) + { + return await _httpClient.GetFromJsonAsync($"api/books/{id}"); + } + + public async Task CreateBookAsync(BookCreateRequest request) + { + var response = await _httpClient.PostAsJsonAsync("api/books", request); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; + } + + public async Task UpdateBookAsync(int id, BookUpdateRequest request) + { + var response = await _httpClient.PutAsJsonAsync($"api/books/{id}", request); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; + } + + public async Task DeleteBookAsync(int id) + { + var response = await _httpClient.DeleteAsync($"api/books/{id}"); + return response.IsSuccessStatusCode; + } + #endregion + + #region Borrowings + public async Task BorrowBookAsync(BorrowRequest request) + { + var response = await _httpClient.PostAsJsonAsync("api/borrowings/borrow", request); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; + } + + public async Task ReturnBookAsync(Guid id) + { + var response = await _httpClient.PostAsync($"api/borrowings/return/{id}", null); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; + } + + public async Task> GetMyBorrowingsAsync() + { + return await _httpClient.GetFromJsonAsync>("api/borrowings/me") ?? Enumerable.Empty(); + } + + public async Task> GetAllBorrowingsAsync() + { + return await _httpClient.GetFromJsonAsync>("api/borrowings") ?? Enumerable.Empty(); + } + + public async Task RequestReturnAsync(Guid borrowingId) + { + var response = await _httpClient.PostAsync($"api/borrowings/return-request/{borrowingId}", null); + return response.IsSuccessStatusCode; + } + #endregion + + #region Categories + public async Task> GetCategoriesAsync() + { + return await _httpClient.GetFromJsonAsync>("api/categories") ?? Enumerable.Empty(); + } + #endregion + + #region Subscriptions + public async Task> GetMembershipsAsync() + { + return await _httpClient.GetFromJsonAsync>("api/memberships") ?? Enumerable.Empty(); + } + + public async Task GetMySubscriptionAsync() + { + try { return await _httpClient.GetFromJsonAsync("api/subscriptions/me"); } + catch { return null; } + } + + public async Task SubscribeAsync(SubscribeRequest request) + { + var response = await _httpClient.PostAsJsonAsync("api/subscriptions/subscribe", request); + return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync() : null; + } + #endregion + + #region Loyalty + public async Task GetMyLoyaltyAccountAsync() + { + try + { + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; + return await _httpClient.GetFromJsonAsync("api/Loyalty/my-account", options); + } + catch { return null; } + } + + public async Task> GetActiveRewardsAsync() + { + return await _httpClient.GetFromJsonAsync>("api/Loyalty/rewards") ?? Enumerable.Empty(); + } + + public async Task<(bool Success, string Message)> ClaimRewardAsync(string rewardId, string? notes = null) + { + var request = new ClaimRewardRequestDto { RewardId = rewardId, Notes = notes }; + var response = await _httpClient.PostAsJsonAsync("api/Loyalty/claim", request); + + if (response.IsSuccessStatusCode) return (true, "Claimed successfully!"); + var error = await response.Content.ReadAsStringAsync(); + return (false, error); + } + #endregion + } +} diff --git a/BlazorWebAssembly/Services/ThemeService.cs b/BlazorWebAssembly/Services/ThemeService.cs new file mode 100644 index 0000000000000000000000000000000000000000..463c0cbc432bb0ec4590dbdd5bf27916b0e3502f --- /dev/null +++ b/BlazorWebAssembly/Services/ThemeService.cs @@ -0,0 +1,40 @@ +using Microsoft.JSInterop; + +namespace BlazorWebAssembly.Services +{ + public class ThemeService + { + private readonly IJSRuntime _jsRuntime; + private string _currentTheme = "light"; + + public event Action? OnThemeChanged; + + public ThemeService(IJSRuntime jsRuntime) + { + _jsRuntime = jsRuntime; + } + + public string CurrentTheme => _currentTheme; + + public async Task InitializeAsync() + { + var savedTheme = await _jsRuntime.InvokeAsync("localStorage.getItem", "app-theme"); + if (!string.IsNullOrEmpty(savedTheme)) + { + await SetThemeAsync(savedTheme); + } + else + { + await SetThemeAsync("light"); + } + } + + public async Task SetThemeAsync(string theme) + { + _currentTheme = theme; + await _jsRuntime.InvokeVoidAsync("localStorage.setItem", "app-theme", theme); + await _jsRuntime.InvokeVoidAsync("document.documentElement.setAttribute", "data-theme", theme); + OnThemeChanged?.Invoke(theme); + } + } +} diff --git a/BlazorWebAssembly/_Imports.razor b/BlazorWebAssembly/_Imports.razor index 7aabff8546a96b4a99ad370a7332e6ce1e572db2..8b217586e59741befacfc74cb63c9edb9481d2f6 100644 --- a/BlazorWebAssembly/_Imports.razor +++ b/BlazorWebAssembly/_Imports.razor @@ -1,4 +1,4 @@ -@using System.Net.Http +@using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @@ -8,3 +8,7 @@ @using Microsoft.JSInterop @using BlazorWebAssembly @using BlazorWebAssembly.Layout +@using Microsoft.AspNetCore.Components.Authorization +@using BlazorWebAssembly.Providers +@using BlazorWebAssembly.Services +@using LibraryManagement.Shared.Models diff --git a/BlazorWebAssembly/wwwroot/css/app.css b/BlazorWebAssembly/wwwroot/css/app.css index 54a8aa38c33ef9079348041e6e2ea9b35196f540..d55dbe23b38ec8320fe34136cef959e7822b4762 100644 --- a/BlazorWebAssembly/wwwroot/css/app.css +++ b/BlazorWebAssembly/wwwroot/css/app.css @@ -1,103 +1,77 @@ -html, body { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -h1:focus { - outline: none; -} - -a, .btn-link { - color: #0071c1; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { - box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; -} - -.content { - padding-top: 1.1rem; +/* Design System Tokens */ +:root { + --transition-speed: 0.3s; + --border-radius: 16px; + --font-main: 'Outfit', 'Inter', system-ui, sans-serif; } -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; +/* --- LIGHT THEME (Olive + Gold) --- */ +[data-theme='light'] { + --bg-body: #f7f5ef; + --bg-card: #ffffff; + --bg-nav: #f7f5ef; + --text-main: #3a3d2e; + --text-muted: #6b705c; + --primary: #a68a64; + --primary-hover: #8e7555; + --accent: #6b705c; + --border: #d6c7a1; + --shadow: 0 4px 6px -1px rgb(58 61 46 / 0.1); + --glass-border: rgba(214, 199, 161, 0.5); + --text-on-primary: #ffffff; } -.invalid { - outline: 1px solid red; +/* --- DARK THEME (Mute Purple Stone) --- */ +[data-theme='dark'] { + --bg-body: #2d2838; + --bg-card: #373245; + --bg-nav: #1e1b26; + --text-main: #f8f7fc; + --text-muted: #b8a9d9; + --primary: #b8a9d9; + --primary-hover: #e9e4f5; + --accent: #7c6a9b; + --border: #4a4458; + --shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5); + --glass-border: rgba(184, 169, 217, 0.1); + --text-on-primary: #2d2838; } -.validation-message { - color: red; +/* --- EMERALD THEME (Slate + Mint) --- */ +[data-theme='emerald'] { + --bg-body: #f5f7f8; + --bg-card: #ffffff; + --bg-nav: #2b3a42; + --text-main: #2b3a42; + --text-muted: #5c8374; + --primary: #8fb9a8; + --primary-hover: #7aa594; + --accent: #5c8374; + --border: #dce3e5; + --shadow: 0 4px 6px -1px rgb(43 58 66 / 0.1); + --glass-border: rgba(143, 185, 168, 0.2); + --text-on-primary: #2b3a42; } -#blazor-error-ui { - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; -} - - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } - -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - padding: 1rem 1rem 1rem 3.7rem; - color: white; -} - - .blazor-error-boundary::after { - content: "An error has occurred." - } - -.loading-progress { - position: relative; - display: block; - width: 8rem; - height: 8rem; - margin: 20vh auto 1rem auto; -} - - .loading-progress circle { - fill: none; - stroke: #e0e0e0; - stroke-width: 0.6rem; - transform-origin: 50% 50%; - transform: rotate(-90deg); - } - - .loading-progress circle:last-child { - stroke: #1b6ec2; - stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; - transition: stroke-dasharray 0.05s ease-in-out; - } - -.loading-progress-text { - position: absolute; - text-align: center; - font-weight: bold; - inset: calc(20vh + 3.25rem) 0 auto 0.2rem; +/* Global resets */ +html, body { + margin: 0; + padding: 0; + font-family: var(--font-main); + background-color: var(--bg-body); + color: var(--text-main); + transition: background-color var(--transition-speed), color var(--transition-speed); } - .loading-progress-text:after { - content: var(--blazor-load-percentage-text, "Loading"); - } +/* Scrollbar styling */ +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; } +::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } -code { - color: #c02d76; +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } } +.animate-fade { animation: fadeIn 0.4s ease-out forwards; } \ No newline at end of file diff --git a/BlazorWebAssembly/wwwroot/index.html b/BlazorWebAssembly/wwwroot/index.html index 5e772ff91207ce8c3be3658fc815550c9fb44dbc..95897fdb49ec8a0857f892ba87a953cce1a85c8d 100644 --- a/BlazorWebAssembly/wwwroot/index.html +++ b/BlazorWebAssembly/wwwroot/index.html @@ -4,21 +4,56 @@ - BlazorWebAssembly + LibraryLuxe | Management System - + + + + + + + + + + + +
- - - - -
+
+
+

Entering LibraryLuxe...

+
@@ -26,7 +61,33 @@ Reload 🗙
+ + + diff --git a/Frontend/Controllers/AuthController.cs b/Frontend/Controllers/AuthController.cs index a8506eccdf36591ff59bbb739e36c1a968e60baa..338624f03457daa54413532da7087633c208cd1a 100644 --- a/Frontend/Controllers/AuthController.cs +++ b/Frontend/Controllers/AuthController.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using System.Security.Claims; -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; using Frontend.Services; namespace Frontend.Controllers @@ -108,3 +108,4 @@ namespace Frontend.Controllers } } } + diff --git a/Frontend/Controllers/BooksController.cs b/Frontend/Controllers/BooksController.cs index 9947340919098ddf2f9e93d09d8332c99941d3a7..403940437767aa776e128f2b79cbc7a6a873a719 100644 --- a/Frontend/Controllers/BooksController.cs +++ b/Frontend/Controllers/BooksController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Frontend.Services; -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; namespace Frontend.Controllers { @@ -125,3 +125,4 @@ namespace Frontend.Controllers } } } + diff --git a/Frontend/Controllers/BorrowingsController.cs b/Frontend/Controllers/BorrowingsController.cs index 3ce86488ae10040cc976b57cd8fcfee962031b60..50ff3b066e0a3b647d2f9b91e1568b3faa54a104 100644 --- a/Frontend/Controllers/BorrowingsController.cs +++ b/Frontend/Controllers/BorrowingsController.cs @@ -1,5 +1,5 @@ using Frontend.Models; -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; using Frontend.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -107,3 +107,4 @@ namespace Frontend.Controllers } } } + diff --git a/Frontend/Controllers/HomeController.cs b/Frontend/Controllers/HomeController.cs index fbc897fdc5a2b19f6b55e945246cb4f16d498c15..66da6c07f1d7d049ef1b910c3edc32a85e8044e9 100644 --- a/Frontend/Controllers/HomeController.cs +++ b/Frontend/Controllers/HomeController.cs @@ -25,7 +25,7 @@ namespace Frontend.Controllers ViewBag.TotalMembers = 0; ViewBag.LoyaltyPoints = 0; ViewBag.LoyaltyStatus = "Not Authenticated"; - ViewBag.RecentBooks = Enumerable.Empty(); + ViewBag.RecentBooks = Enumerable.Empty(); try { @@ -90,3 +90,4 @@ namespace Frontend.Controllers } } } + diff --git a/Frontend/Controllers/MembersController.cs b/Frontend/Controllers/MembersController.cs index 3bb29432ebf69e3495edf911ce2d600c7f704785..b61a60bf703c5e5609b5564c93246d32b3d5da19 100644 --- a/Frontend/Controllers/MembersController.cs +++ b/Frontend/Controllers/MembersController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Frontend.Services; -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; using Microsoft.AspNetCore.Authorization; namespace Frontend.Controllers @@ -18,12 +18,12 @@ namespace Frontend.Controllers public async Task Index(int page = 1) { const int pageSize = 10; - var allUsers = await _apiClient.GetUsersAsync() ?? Enumerable.Empty(); + var allUsers = await _apiClient.GetUsersAsync() ?? Enumerable.Empty(); var totalCount = allUsers.Count(); var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize); page = Math.Max(1, Math.Min(page, Math.Max(1, totalPages))); - var pagedResult = new Frontend.Models.PagedResult + var pagedResult = new Frontend.Models.PagedResult { Items = allUsers.Skip((page - 1) * pageSize).Take(pageSize), CurrentPage = page, @@ -153,3 +153,4 @@ namespace Frontend.Controllers } } } + diff --git a/Frontend/Controllers/MembershipController.cs b/Frontend/Controllers/MembershipController.cs index 78bac9e74d87089148206710fee4118f18fc6802..575a4f94306590b08d8181374ba14cf9708ce3a2 100644 --- a/Frontend/Controllers/MembershipController.cs +++ b/Frontend/Controllers/MembershipController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Frontend.Services; -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; using Frontend.Models; using Microsoft.AspNetCore.Authorization; using System.Threading.Tasks; @@ -66,3 +66,4 @@ namespace Frontend.Controllers } } } + diff --git a/Frontend/Controllers/RewardsController.cs b/Frontend/Controllers/RewardsController.cs index de99ec968ae768cd5f612aa584d37ae32b8b1599..20bada1b6781741f1d0ab5b6ef0bbff2a11cf7e3 100644 --- a/Frontend/Controllers/RewardsController.cs +++ b/Frontend/Controllers/RewardsController.cs @@ -1,5 +1,5 @@ using Frontend.Models; -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; using Frontend.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -90,3 +90,4 @@ namespace Frontend.Controllers } } } + diff --git a/Frontend/Controllers/SubscriptionsController.cs b/Frontend/Controllers/SubscriptionsController.cs index 216e2f90f8dbf224b0d343e53f86fa170c722891..772a97e820575e84f55614cb5e06178eb85c471b 100644 --- a/Frontend/Controllers/SubscriptionsController.cs +++ b/Frontend/Controllers/SubscriptionsController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Frontend.Services; -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; using Frontend.Models; using Microsoft.AspNetCore.Authorization; using System.Threading.Tasks; @@ -53,3 +53,4 @@ namespace Frontend.Controllers } } } + diff --git a/Frontend/Frontend.csproj b/Frontend/Frontend.csproj index cdbe16c0ac17fd0cd27c6ddeccab1936bd033be7..2a215335adeb9677ba5c06efb892cdc03d82ec4e 100644 --- a/Frontend/Frontend.csproj +++ b/Frontend/Frontend.csproj @@ -1,5 +1,9 @@ + + + + net8.0 enable diff --git a/Frontend/Models/Dtos/LibraryDtos.cs b/Frontend/Models/Dtos/LibraryDtos.cs deleted file mode 100644 index 9efa6832c64802f23a062fe23e197c68a328cb84..0000000000000000000000000000000000000000 --- a/Frontend/Models/Dtos/LibraryDtos.cs +++ /dev/null @@ -1,285 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Frontend.Models.Dtos -{ - // Auth DTOs - public class RegisterRequest - { - public string FullName { get; set; } = string.Empty; - public string Email { get; set; } = string.Empty; - public string Password { get; set; } = string.Empty; - public string? PhoneNumber { get; set; } - public string? Address { get; set; } - public string? StudentId { get; set; } - } - - public class LoginRequest - { - public string Email { get; set; } = string.Empty; - public string Password { get; set; } = string.Empty; - } - - public class AuthResponse - { - public string Token { get; set; } = string.Empty; - public string FullName { get; set; } = string.Empty; - public string Role { get; set; } = string.Empty; - public DateTime Expiry { get; set; } - } - - // Book DTOs - public class BookDto - { - public int Id { get; set; } - public string Title { get; set; } = string.Empty; - public string Isbn { get; set; } = string.Empty; - public string Author { get; set; } = string.Empty; - public string Status { get; set; } = string.Empty; - public bool IsActive { get; set; } - public string? Description { get; set; } - public int TotalCopies { get; set; } - public int AvailableCopies { get; set; } - public DateTime CreatedAt { get; set; } - public DateTime? UpdatedAt { get; set; } - public string? CoverUrl { get; set; } - public List Categories { get; set; } = new(); - } - - public class BookCategoryDto - { - public int Id { get; set; } - public string Name { get; set; } = string.Empty; - } - - public class BookCreateRequest - { - public string Title { get; set; } = string.Empty; - public string Isbn { get; set; } = string.Empty; - public string Author { get; set; } = string.Empty; - public string? Description { get; set; } - public int TotalCopies { get; set; } - public List? CategoryIds { get; set; } - } - - public class BookUpdateRequest - { - public string Title { get; set; } = string.Empty; - public string Author { get; set; } = string.Empty; - public string? Description { get; set; } - public int TotalCopies { get; set; } - public List? CategoryIds { get; set; } - } - - // Borrowing DTOs - public class BorrowingDto - { - public Guid Id { get; set; } - public Guid UserId { get; set; } - public string UserEmail { get; set; } = string.Empty; - public int BookId { get; set; } - public string BookTitle { get; set; } = string.Empty; - public DateTime BorrowDate { get; set; } - public DateTime DueDate { get; set; } - public DateTime? ReturnDate { get; set; } // This is ActualReturnDate in some contexts - public string Status { get; set; } = string.Empty; - public decimal FineAmount { get; set; } - public bool IsFinePaid { get; set; } - } - - public class BorrowRequest - { - public int BookId { get; set; } - } - - // Category DTOs - public class CategoryDto - { - public int Id { get; set; } - public string Name { get; set; } = string.Empty; - } - - public class CategoryCreateRequest - { - public string Name { get; set; } = string.Empty; - } - - // Subscription & Membership DTOs - public class MembershipDto - { - public int Id { get; set; } - public string Type { get; set; } = string.Empty; - public int MaxBooks { get; set; } - public int BorrowingDays { get; set; } - public decimal Price { get; set; } - public int DurationMonths { get; set; } - public string? RewardId { get; set; } - } - - public class SubscriptionDto - { - public Guid Id { get; set; } - public Guid UserId { get; set; } - public int MembershipId { get; set; } - public string MembershipType { get; set; } = string.Empty; - public string UserName { get; set; } = string.Empty; - public string UserEmail { get; set; } = string.Empty; - public DateTime StartDate { get; set; } - public DateTime ExpiryDate { get; set; } - public bool IsActive { get; set; } - public bool IsExpired => DateTime.UtcNow > ExpiryDate; - } - - public class SubscribeRequest - { - public int MembershipId { get; set; } - } - - public class AdminSubscribeRequest - { - public Guid UserId { get; set; } - public int MembershipId { get; set; } - } - - // Loyalty DTOs - public class LoyaltyAccountDto - { - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - // Some responses use `accountId` instead of `id`. - [JsonPropertyName("accountId")] - public string? AccountId { get; set; } - - [JsonPropertyName("externalUserId")] - public string ExternalUserId { get; set; } = string.Empty; - - [JsonPropertyName("currentBalance")] - public double CurrentBalance { get; set; } - - [JsonPropertyName("tier")] - public string Tier { get; set; } = string.Empty; - - [JsonPropertyName("lifetimePoints")] - public double LifetimePoints { get; set; } - } - - public class LoyaltyRewardDto - { - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - - [JsonPropertyName("pointCost")] - public double PointCost { get; set; } - - [JsonPropertyName("stockQuantity")] - public int StockQuantity { get; set; } - - [JsonPropertyName("isActive")] - public bool IsActive { get; set; } - } - - public class ClaimRewardRequestDto - { - public string RewardId { get; set; } = string.Empty; - public string? Notes { get; set; } - } - - // User DTOs - public class UserDto - { - public Guid Id { get; set; } - public string FullName { get; set; } = string.Empty; - public string Email { get; set; } = string.Empty; - public string? PhoneNumber { get; set; } - public string Role { get; set; } = string.Empty; - public bool IsActive { get; set; } - public string? StudentId { get; set; } - public string? Address { get; set; } - public DateTime CreatedAt { get; set; } - public DateTime? UpdatedAt { get; set; } - public bool? BanStatus { get; set; } - public DateTime? SuspensionEndDate { get; set; } - } - - public class UserCreateRequest - { - public string FullName { get; set; } = string.Empty; - public string Email { get; set; } = string.Empty; - public string Password { get; set; } = string.Empty; - public string? PhoneNumber { get; set; } - public string? StudentId { get; set; } - public string? Address { get; set; } - } - - public class UserUpdateRequest - { - public string FullName { get; set; } = string.Empty; - public string? PhoneNumber { get; set; } - public string? StudentId { get; set; } - public string? Address { get; set; } - } - - public class UserRoleUpdateRequest - { - public string Role { get; set; } = string.Empty; - } - - // Points History DTOs - public class PointHistoryEntryDto - { - [JsonPropertyName("id")] - public int Id { get; set; } - [JsonPropertyName("accountId")] - public string AccountId { get; set; } = string.Empty; - [JsonPropertyName("externalUserId")] - public string? ExternalUserId { get; set; } - [JsonPropertyName("pointDelta")] - public double PointDelta { get; set; } - [JsonPropertyName("eventKey")] - public string EventKey { get; set; } = string.Empty; - [JsonPropertyName("description")] - public string Description { get; set; } = string.Empty; - [JsonPropertyName("referenceId")] - public string? ReferenceId { get; set; } - [JsonPropertyName("createdAt")] - public DateTime CreatedAt { get; set; } - - [JsonPropertyName("rewardId")] - public string? RewardId { get; set; } - - [JsonPropertyName("rewardName")] - public string? RewardName { get; set; } - - [JsonPropertyName("redemptionStatus")] - public string? RedemptionStatus { get; set; } - - [JsonPropertyName("redeemedAt")] - public DateTime? RedeemedAt { get; set; } - } - - public class UserPointsHistoryDto - { - [JsonPropertyName("userId")] - public Guid UserId { get; set; } - [JsonPropertyName("userName")] - public string UserName { get; set; } = string.Empty; - [JsonPropertyName("userEmail")] - public string UserEmail { get; set; } = string.Empty; - [JsonPropertyName("accountId")] - public string AccountId { get; set; } = string.Empty; - [JsonPropertyName("currentBalance")] - public double CurrentBalance { get; set; } - [JsonPropertyName("tier")] - public string Tier { get; set; } = string.Empty; - [JsonPropertyName("history")] - public IEnumerable History { get; set; } = Enumerable.Empty(); - [JsonPropertyName("redemptions")] - public IEnumerable Redemptions { get; set; } = Enumerable.Empty(); - } -} diff --git a/Frontend/Models/Dtos/LoyaltyRedemptionDto.cs b/Frontend/Models/Dtos/LoyaltyRedemptionDto.cs deleted file mode 100644 index 81b0ceee127b20b466179fba0b2487858e5ea2ce..0000000000000000000000000000000000000000 --- a/Frontend/Models/Dtos/LoyaltyRedemptionDto.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Text.Json.Serialization; - -namespace Frontend.Models.Dtos -{ - public class LoyaltyRedemptionDto - { - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - [JsonPropertyName("systemId")] - public string SystemId { get; set; } = string.Empty; - - [JsonPropertyName("externalUserId")] - public string ExternalUserId { get; set; } = string.Empty; - - [JsonPropertyName("rewardId")] - public string? RewardId { get; set; } - - [JsonPropertyName("rewardName")] - public string RewardName { get; set; } = string.Empty; - - [JsonPropertyName("pointCost")] - public double PointCost { get; set; } - - [JsonPropertyName("status")] - public string Status { get; set; } = string.Empty; - - [JsonPropertyName("redeemedAt")] - public DateTime RedeemedAt { get; set; } - } -} \ No newline at end of file diff --git a/Frontend/Models/MemberDetailsViewModel.cs b/Frontend/Models/MemberDetailsViewModel.cs index 9e5a663585e6681043dcd87bd9ad2676524568e6..5e8212dceb9edb2b21ae8ec6a1c074805aa8fa1b 100644 --- a/Frontend/Models/MemberDetailsViewModel.cs +++ b/Frontend/Models/MemberDetailsViewModel.cs @@ -1,4 +1,4 @@ -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; using System.Collections.Generic; namespace Frontend.Models @@ -10,3 +10,4 @@ namespace Frontend.Models public List AvailableMemberships { get; set; } = new(); } } + diff --git a/Frontend/Models/MembershipViewModel.cs b/Frontend/Models/MembershipViewModel.cs index 5955aff2e184589a979159e99e5ce8c6d7581bce..0665bbcf0e19fe3fdb0b6446a97ee9676578f9dc 100644 --- a/Frontend/Models/MembershipViewModel.cs +++ b/Frontend/Models/MembershipViewModel.cs @@ -1,4 +1,4 @@ -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; using System.Collections.Generic; namespace Frontend.Models @@ -12,3 +12,4 @@ namespace Frontend.Models public Dictionary RewardPointCosts { get; set; } = new Dictionary(); } } + diff --git a/Frontend/Models/RewardsViewModel.cs b/Frontend/Models/RewardsViewModel.cs index fcbccf5ff7810ef7c1792d50e7567b38569ce3ff..c1655b032dfc4f43f3d6e95de04e24ff611d4bf5 100644 --- a/Frontend/Models/RewardsViewModel.cs +++ b/Frontend/Models/RewardsViewModel.cs @@ -1,4 +1,4 @@ -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; namespace Frontend.Models { @@ -19,3 +19,4 @@ namespace Frontend.Models public IEnumerable AllMembersHistory { get; set; } = Enumerable.Empty(); } } + diff --git a/Frontend/Models/SubscriptionsViewModel.cs b/Frontend/Models/SubscriptionsViewModel.cs index 9af0432f3f02da947b5bdae7d983a8b54c39c11b..2fe63f7550da5f0bee0a2db60f1f1bf3b134d6fd 100644 --- a/Frontend/Models/SubscriptionsViewModel.cs +++ b/Frontend/Models/SubscriptionsViewModel.cs @@ -1,4 +1,4 @@ -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; namespace Frontend.Models { @@ -8,3 +8,4 @@ namespace Frontend.Models public List ActiveSubscriptions { get; set; } = new(); } } + diff --git a/Frontend/Services/LibraryApiClient.cs b/Frontend/Services/LibraryApiClient.cs index 124325983b8bb8ce49ca4304b719b506d1d63da1..0a52340982fdfc0c0b5d6217b3b825ad27817981 100644 --- a/Frontend/Services/LibraryApiClient.cs +++ b/Frontend/Services/LibraryApiClient.cs @@ -1,11 +1,21 @@ +using System.Text.Json.Serialization; using System.Net.Http.Json; using System.Text.Json; -using Frontend.Models.Dtos; +using LibraryManagement.Shared.Models; // Changed from LibraryManagement.Shared.Models namespace Frontend.Services { public class LibraryApiClient { + public async Task> GetActiveRewardsAsync() + { + try + { + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; + return await _httpClient.GetFromJsonAsync>("api/Loyalty/rewards", options) ?? Enumerable.Empty(); + } + catch { return Enumerable.Empty(); } + } private readonly HttpClient _httpClient; private readonly IHttpContextAccessor _httpContextAccessor; @@ -232,11 +242,8 @@ namespace Frontend.Services { try { - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; - var response = await _httpClient.GetAsync($"api/v1/accounts/{accountId}/history"); - if (response.IsSuccessStatusCode) - return await response.Content.ReadFromJsonAsync>(options) ?? Enumerable.Empty(); - return Enumerable.Empty(); + // Call the Backend's proxy endpoint instead of trying to hit the external API directly + return await _httpClient.GetFromJsonAsync>("api/Loyalty/my-points-history") ?? Enumerable.Empty(); } catch { return Enumerable.Empty(); } } @@ -244,7 +251,7 @@ namespace Frontend.Services public async Task> GetAllMembersPointsHistoryAsync() { try { - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; var response = await _httpClient.GetAsync("api/Loyalty/admin/all-points-history"); var raw = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) @@ -300,22 +307,6 @@ namespace Frontend.Services } } - public async Task> GetActiveRewardsAsync() - { - try - { - // Fetch from the external loyalty API directly or via our backend proxy - // The user specified the URL: http://150.95.88.91:4100/api/v1/rewards/active/THS-LMS - // We'll use a new HttpClient or just the existing one if configured correctly. - // However, our backend doesn't have a proxy for this yet. - // For simplicity, let's assume we call it directly or via a backend endpoint we'll add. - // Better: Let's add a backend proxy in LoyaltyController to avoid CORS issues if this was a browser app. - // In ASP.NET Core MVC, the server-side HttpClient can call it directly. - var client = new HttpClient(); - return await client.GetFromJsonAsync>("http://150.95.88.91:4100/api/v1/rewards/active/THS-LMS") ?? Enumerable.Empty(); - } - catch { return Enumerable.Empty(); } - } #region Users public async Task> GetUsersAsync() @@ -359,7 +350,7 @@ namespace Frontend.Services var response = await _httpClient.GetAsync("api/Loyalty/admin/redemptions/pending"); if (response.IsSuccessStatusCode) { - var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString }; return await response.Content.ReadFromJsonAsync>(options) ?? Enumerable.Empty(); } return Enumerable.Empty(); @@ -397,3 +388,7 @@ namespace Frontend.Services } } } + + + + diff --git a/Frontend/Views/Auth/Login.cshtml b/Frontend/Views/Auth/Login.cshtml index 37960f37802f36256cc84e04905b42d3642e4109..6f4efb47bd34e6711b71edbe3c87c53aa12bce39 100644 --- a/Frontend/Views/Auth/Login.cshtml +++ b/Frontend/Views/Auth/Login.cshtml @@ -1,4 +1,4 @@ -@model Frontend.Models.Dtos.LoginRequest +@model LibraryManagement.Shared.Models.LoginRequest @{ ViewData["Title"] = "Sign In"; Layout = "_Layout"; @@ -47,3 +47,4 @@
+ diff --git a/Frontend/Views/Auth/Register.cshtml b/Frontend/Views/Auth/Register.cshtml index fad4302dc70ba67f955de86030d461a1c5236f1d..d9d20e28c0aec385dca8ef4bfe64f9fa9a517f69 100644 --- a/Frontend/Views/Auth/Register.cshtml +++ b/Frontend/Views/Auth/Register.cshtml @@ -1,4 +1,4 @@ -@model Frontend.Models.Dtos.RegisterRequest +@model LibraryManagement.Shared.Models.RegisterRequest @{ ViewData["Title"] = "Create Account"; Layout = "_Layout"; @@ -54,3 +54,4 @@ + diff --git a/Frontend/Views/Books/Create.cshtml b/Frontend/Views/Books/Create.cshtml index 02994ce0a7abca016b1ebc90625c5f44e2ac7a81..fb3b772a0546fe00cbcc5a83745adb0b330a820f 100644 --- a/Frontend/Views/Books/Create.cshtml +++ b/Frontend/Views/Books/Create.cshtml @@ -1,4 +1,4 @@ -@model Frontend.Models.Dtos.BookCreateRequest +@model LibraryManagement.Shared.Models.BookCreateRequest @{ ViewData["Title"] = "Add New Book"; } @@ -72,7 +72,7 @@
@{ - var allCategories = ViewBag.Categories as IEnumerable; + var allCategories = ViewBag.Categories as IEnumerable; } @if (allCategories != null && allCategories.Any()) { @@ -106,3 +106,4 @@ @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} } + diff --git a/Frontend/Views/Books/Details.cshtml b/Frontend/Views/Books/Details.cshtml index 9cc704a6313aedb8bca4d9151c6c74720535d77f..622065b09c878aeb1e28496015b7d0c307245249 100644 --- a/Frontend/Views/Books/Details.cshtml +++ b/Frontend/Views/Books/Details.cshtml @@ -1,4 +1,4 @@ -@model Frontend.Models.Dtos.BookDto +@model LibraryManagement.Shared.Models.BookDto @{ ViewData["Title"] = Model.Title; } @@ -75,3 +75,4 @@
+ diff --git a/Frontend/Views/Books/Edit.cshtml b/Frontend/Views/Books/Edit.cshtml index 717c728f478a6f27abfc5bbee16b31cb29f8d469..b52ebb247a095e377cd8a5d3ccd6c99cca939306 100644 --- a/Frontend/Views/Books/Edit.cshtml +++ b/Frontend/Views/Books/Edit.cshtml @@ -1,4 +1,4 @@ -@model Frontend.Models.Dtos.BookUpdateRequest +@model LibraryManagement.Shared.Models.BookUpdateRequest @{ ViewData["Title"] = "Edit Book"; } @@ -63,7 +63,7 @@
@{ - var allCategories = ViewBag.Categories as IEnumerable; + var allCategories = ViewBag.Categories as IEnumerable; var selectedIds = Model.CategoryIds ?? new List(); } @if (allCategories != null && allCategories.Any()) @@ -99,3 +99,4 @@ @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} } + diff --git a/Frontend/Views/Books/Index.cshtml b/Frontend/Views/Books/Index.cshtml index 53dfd55ec5a688515f9da2a4677c83c278ec50de..3ad0cc0c3232faada7d848f5aa5eeb4ee3207e73 100644 --- a/Frontend/Views/Books/Index.cshtml +++ b/Frontend/Views/Books/Index.cshtml @@ -1,4 +1,4 @@ -@model Frontend.Models.PagedResult +@model Frontend.Models.PagedResult @{ ViewData["Title"] = "Books Catalog"; } @@ -37,7 +37,7 @@ @* Category Filter Pills *@ @{ - var categories = ViewBag.Categories as IEnumerable ?? Enumerable.Empty(); + var categories = ViewBag.Categories as IEnumerable ?? Enumerable.Empty(); int? selectedCategoryId = ViewBag.SelectedCategoryId; } @if (categories.Any()) @@ -191,3 +191,4 @@
} + diff --git a/Frontend/Views/Borrowings/Index.cshtml b/Frontend/Views/Borrowings/Index.cshtml index e13b7f3921536f4ad2c599fdd9b47bcf354bd8f0..7198ad3aa543bb5732d78db679ae60d3be99d731 100644 --- a/Frontend/Views/Borrowings/Index.cshtml +++ b/Frontend/Views/Borrowings/Index.cshtml @@ -1,4 +1,4 @@ -@model IEnumerable +@model IEnumerable @{ ViewData["Title"] = "My Loans"; } @@ -109,3 +109,4 @@ + diff --git a/Frontend/Views/Borrowings/Manage.cshtml b/Frontend/Views/Borrowings/Manage.cshtml index f0086fdfed28b3e2f39355caf0a5b0328c4341fc..e0f67b0170712a52f29f538ff04e8db9022e003a 100644 --- a/Frontend/Views/Borrowings/Manage.cshtml +++ b/Frontend/Views/Borrowings/Manage.cshtml @@ -1,4 +1,4 @@ -@model IEnumerable +@model IEnumerable @{ ViewData["Title"] = "Manage Borrowings"; } @@ -147,3 +147,4 @@ + diff --git a/Frontend/Views/Home/Index.cshtml b/Frontend/Views/Home/Index.cshtml index b322eb81b007e16f493c4b31f6149a002deb48ee..955900f99dfdd7d71e6b8c54d6cc4bae5028f139 100644 --- a/Frontend/Views/Home/Index.cshtml +++ b/Frontend/Views/Home/Index.cshtml @@ -103,7 +103,7 @@
- @foreach (var book in (IEnumerable)ViewBag.RecentBooks) + @foreach (var book in (IEnumerable)ViewBag.RecentBooks) {
@@ -151,3 +151,4 @@
+ diff --git a/Frontend/Views/Members/Create.cshtml b/Frontend/Views/Members/Create.cshtml index 81c2b30c7ba3c228a3434f4dd82073dc94f348f7..7e27ae0e526abd7a9d6be4e1c3b4db28ed17d9c9 100644 --- a/Frontend/Views/Members/Create.cshtml +++ b/Frontend/Views/Members/Create.cshtml @@ -1,4 +1,4 @@ -@model Frontend.Models.Dtos.UserCreateRequest +@model LibraryManagement.Shared.Models.UserCreateRequest @{ ViewData["Title"] = "Add New Member"; } @@ -88,3 +88,4 @@ @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} } + diff --git a/Frontend/Views/Members/Edit.cshtml b/Frontend/Views/Members/Edit.cshtml index 014f9e2b994dbc9bbe1d5ca02b8f493a18f59c8c..2d2bc849a6a7a4d428f63bce8f901c0ec19a6388 100644 --- a/Frontend/Views/Members/Edit.cshtml +++ b/Frontend/Views/Members/Edit.cshtml @@ -1,4 +1,4 @@ -@model Frontend.Models.Dtos.UserUpdateRequest +@model LibraryManagement.Shared.Models.UserUpdateRequest @{ ViewData["Title"] = "Edit Member"; } @@ -70,3 +70,4 @@ @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} } + diff --git a/Frontend/Views/Members/Index.cshtml b/Frontend/Views/Members/Index.cshtml index 6e942767e1f943f85eb28be16a6fd0c402779e9f..017ff57824f7b23b59f62fe9c987a540f647d783 100644 --- a/Frontend/Views/Members/Index.cshtml +++ b/Frontend/Views/Members/Index.cshtml @@ -1,4 +1,4 @@ -@model Frontend.Models.PagedResult +@model Frontend.Models.PagedResult @{ ViewData["Title"] = "Members Directory"; } @@ -166,3 +166,4 @@ } + diff --git a/Frontend/Views/Subscriptions/LoyaltyClaims.cshtml b/Frontend/Views/Subscriptions/LoyaltyClaims.cshtml index 732402e4021961901c751ee169f9ea66f9ad5234..2baec3a4fd6fdfb7e6d64a249dbc4e6fcc983b91 100644 --- a/Frontend/Views/Subscriptions/LoyaltyClaims.cshtml +++ b/Frontend/Views/Subscriptions/LoyaltyClaims.cshtml @@ -1,4 +1,4 @@ -@model List +@model List @{ ViewData["Title"] = "Loyalty Reward Claims"; } @@ -84,3 +84,4 @@ + diff --git a/LibraryManagement.slnx b/LibraryManagement.slnx index bd08fb7f2418d3e1ff89b5143d622a7489c96a39..e8b1c566d315427b4d12fdf1b34e7c4e1a380431 100644 --- a/LibraryManagement.slnx +++ b/LibraryManagement.slnx @@ -4,4 +4,5 @@ + diff --git a/scratch/event.json b/scratch/event.json new file mode 100644 index 0000000000000000000000000000000000000000..9386b256822e4be627b4cee65bb93bed5c911208 --- /dev/null +++ b/scratch/event.json @@ -0,0 +1,6 @@ +{ + "systemId": "THS-LMS", + "externalUserId": "test", + "eventKey": "SIGNUP", + "eventValue": 10 +} diff --git a/scratch/test_loyalty_api.cs b/scratch/test_loyalty_api.cs new file mode 100644 index 0000000000000000000000000000000000000000..b4601a01d46a392cc69627f8d8fe1c925b8ef929 --- /dev/null +++ b/scratch/test_loyalty_api.cs @@ -0,0 +1,53 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using System.Collections.Generic; + +class Program +{ + static async Task Main() + { + var client = new HttpClient(); + client.BaseAddress = new Uri("http://150.95.88.91:4100"); + client.DefaultRequestHeaders.Add("x-system-id", "THS-LMS"); + + Console.WriteLine("Testing Loyalty API Connectivity..."); + + try + { + Console.WriteLine("\n1. Testing Active Rewards..."); + var rewardsResponse = await client.GetAsync("/api/v1/rewards/active/THS-LMS"); + Console.WriteLine($"Status: {rewardsResponse.StatusCode}"); + if (rewardsResponse.IsSuccessStatusCode) + { + var content = await rewardsResponse.Content.ReadAsStringAsync(); + Console.WriteLine($"Content: {content.Substring(0, Math.Min(100, content.Length))}..."); + } + else + { + var error = await rewardsResponse.Content.ReadAsStringAsync(); + Console.WriteLine($"Error: {error}"); + } + + Console.WriteLine("\n2. Testing Account Lookup (Test User)..."); + // Using a dummy ID to see if we get a 404 or a connection error + var lookupResponse = await client.GetAsync("/api/v1/accounts/lookup/THS-LMS/test-user-id"); + Console.WriteLine($"Status: {lookupResponse.StatusCode}"); + if (lookupResponse.IsSuccessStatusCode) + { + var content = await lookupResponse.Content.ReadAsStringAsync(); + Console.WriteLine($"Content: {content}"); + } + else + { + var error = await lookupResponse.Content.ReadAsStringAsync(); + Console.WriteLine($"Error: {error}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"\nCRITICAL CONNECTION ERROR: {ex.Message}"); + } + } +} diff --git a/scratch/test_loyalty_conn.cs b/scratch/test_loyalty_conn.cs new file mode 100644 index 0000000000000000000000000000000000000000..2fa96a47ac3baa2877a8e81b6a967b0bf9c31c9b --- /dev/null +++ b/scratch/test_loyalty_conn.cs @@ -0,0 +1,18 @@ +using System.Net.Http.Json; + +var client = new HttpClient(); +client.BaseAddress = new Uri("http://150.95.88.91:4100"); +client.DefaultRequestHeaders.Add("x-system-id", "THS-LMS"); + +try { + Console.WriteLine("Testing connection to Loyalty API..."); + var response = await client.GetAsync("/api/v1/rewards/active/THS-LMS"); + Console.WriteLine($"Status: {response.StatusCode}"); + var content = await response.Content.ReadAsStringAsync(); + Console.WriteLine($"Content: {content.Substring(0, Math.Min(content.Length, 100))}..."); +} catch (Exception ex) { + Console.WriteLine($"Connection failed: {ex.Message}"); + if (ex.InnerException != null) { + Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); + } +}