Yuyuqt commited on
Commit
c312181
·
1 Parent(s): 1a7e978

add: subscription rewards, register rewards, fix some features

Browse files
Backend/Features/Auth/AuthService.cs CHANGED
@@ -6,6 +6,7 @@ using System.Security.Claims;
6
  using System.Text;
7
 
8
  using Backend.Features.Subscriptions;
 
9
 
10
  namespace Backend.Features.Auth
11
  {
@@ -20,12 +21,14 @@ namespace Backend.Features.Auth
20
  private readonly LibraryManagementContext _context;
21
  private readonly IConfiguration _configuration;
22
  private readonly ISubscriptionService _subscriptionService;
 
23
 
24
- public AuthService(LibraryManagementContext context, IConfiguration configuration, ISubscriptionService subscriptionService)
25
  {
26
  _context = context;
27
  _configuration = configuration;
28
  _subscriptionService = subscriptionService;
 
29
  }
30
 
31
  public async Task<AuthResponse> Register(RegisterRequest request)
@@ -56,6 +59,20 @@ namespace Backend.Features.Auth
56
  await _subscriptionService.SubscribeUserAsync(user.Id, 3); // 3 is "Basic Yearly"
57
  }
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  return await AuthenticateUser(user);
60
  }
61
 
 
6
  using System.Text;
7
 
8
  using Backend.Features.Subscriptions;
9
+ using Backend.Features.Loyalty;
10
 
11
  namespace Backend.Features.Auth
12
  {
 
21
  private readonly LibraryManagementContext _context;
22
  private readonly IConfiguration _configuration;
23
  private readonly ISubscriptionService _subscriptionService;
24
+ private readonly ILoyaltyService _loyaltyService;
25
 
26
+ public AuthService(LibraryManagementContext context, IConfiguration configuration, ISubscriptionService subscriptionService, ILoyaltyService loyaltyService)
27
  {
28
  _context = context;
29
  _configuration = configuration;
30
  _subscriptionService = subscriptionService;
31
+ _loyaltyService = loyaltyService;
32
  }
33
 
34
  public async Task<AuthResponse> Register(RegisterRequest request)
 
59
  await _subscriptionService.SubscribeUserAsync(user.Id, 3); // 3 is "Basic Yearly"
60
  }
61
 
62
+ // Loyalty Integration: Register the new user and process SIGNUP event
63
+ string externalUserId = user.Id.ToString();
64
+ string userMobile = user.PhoneNumber ?? "0000000000";
65
+ await _loyaltyService.RegisterUserAsync(externalUserId, user.Email, userMobile);
66
+ await _loyaltyService.ProcessEventAsync(
67
+ externalUserId: externalUserId,
68
+ eventKey: "SIGNUP",
69
+ eventValue: 20,
70
+ referenceId: $"USR-{user.Id}",
71
+ description: "New User Registration",
72
+ email: user.Email,
73
+ mobile: userMobile
74
+ );
75
+
76
  return await AuthenticateUser(user);
77
  }
78
 
Backend/Features/Books/BookController.cs CHANGED
@@ -15,7 +15,7 @@ namespace Backend.Features.Books
15
  }
16
 
17
  [HttpGet]
18
- //[Authorize]
19
  public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks()
20
  {
21
  var books = await _bookService.GetAllBooksAsync();
@@ -23,7 +23,7 @@ namespace Backend.Features.Books
23
  }
24
 
25
  [HttpGet("{id}")]
26
- //[Authorize]
27
  public async Task<ActionResult<BookDto>> GetBook(int id)
28
  {
29
  var book = await _bookService.GetBookByIdAsync(id);
@@ -32,7 +32,7 @@ namespace Backend.Features.Books
32
  }
33
 
34
  [HttpPost]
35
- //[Authorize(Roles = "Librarian")]
36
  public async Task<ActionResult<BookDto>> CreateBook([FromBody] BookCreateRequest request)
37
  {
38
  try
@@ -47,7 +47,7 @@ namespace Backend.Features.Books
47
  }
48
 
49
  [HttpPut("{id}")]
50
- //[Authorize(Roles = "Librarian")]
51
  public async Task<ActionResult<BookDto>> UpdateBook(int id, [FromBody] BookUpdateRequest request)
52
  {
53
  var updatedBook = await _bookService.UpdateBookAsync(id, request);
@@ -56,7 +56,7 @@ namespace Backend.Features.Books
56
  }
57
 
58
  [HttpDelete("{id}")]
59
- //[Authorize(Roles = "Librarian")]
60
  public async Task<IActionResult> DeleteBook(int id)
61
  {
62
  var success = await _bookService.DeleteBookAsync(id);
 
15
  }
16
 
17
  [HttpGet]
18
+ [Authorize]
19
  public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks()
20
  {
21
  var books = await _bookService.GetAllBooksAsync();
 
23
  }
24
 
25
  [HttpGet("{id}")]
26
+ [Authorize]
27
  public async Task<ActionResult<BookDto>> GetBook(int id)
28
  {
29
  var book = await _bookService.GetBookByIdAsync(id);
 
32
  }
33
 
34
  [HttpPost]
35
+ [Authorize(Roles = "Librarian")]
36
  public async Task<ActionResult<BookDto>> CreateBook([FromBody] BookCreateRequest request)
37
  {
38
  try
 
47
  }
48
 
49
  [HttpPut("{id}")]
50
+ [Authorize(Roles = "Librarian")]
51
  public async Task<ActionResult<BookDto>> UpdateBook(int id, [FromBody] BookUpdateRequest request)
52
  {
53
  var updatedBook = await _bookService.UpdateBookAsync(id, request);
 
56
  }
57
 
58
  [HttpDelete("{id}")]
59
+ [Authorize(Roles = "Librarian")]
60
  public async Task<IActionResult> DeleteBook(int id)
61
  {
62
  var success = await _bookService.DeleteBookAsync(id);
Backend/Features/Borrowing/BorrowingController.cs CHANGED
@@ -16,7 +16,7 @@ namespace Backend.Features.Borrowings
16
  }
17
 
18
  [HttpPost("borrow")]
19
- //[Authorize]
20
  public async Task<ActionResult<BorrowingDto>> BorrowBook([FromBody] BorrowRequest request)
21
  {
22
  try
@@ -35,7 +35,7 @@ namespace Backend.Features.Borrowings
35
  }
36
 
37
  [HttpPost("return/{id}")]
38
- //[Authorize(Roles = "Librarian")]
39
  public async Task<ActionResult<BorrowingDto>> ReturnBook(int id)
40
  {
41
  try
@@ -50,7 +50,7 @@ namespace Backend.Features.Borrowings
50
  }
51
 
52
  [HttpGet("me")]
53
- //[Authorize]
54
  public async Task<ActionResult<IEnumerable<BorrowingDto>>> GetMyBorrowings()
55
  {
56
  var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
@@ -62,7 +62,7 @@ namespace Backend.Features.Borrowings
62
  }
63
 
64
  [HttpGet]
65
- //[Authorize(Roles = "Librarian")]
66
  public async Task<ActionResult<IEnumerable<BorrowingDto>>> GetAllBorrowings()
67
  {
68
  var borrowings = await _borrowingService.GetAllBorrowingsAsync();
 
16
  }
17
 
18
  [HttpPost("borrow")]
19
+ [Authorize]
20
  public async Task<ActionResult<BorrowingDto>> BorrowBook([FromBody] BorrowRequest request)
21
  {
22
  try
 
35
  }
36
 
37
  [HttpPost("return/{id}")]
38
+ [Authorize(Roles = "Librarian")]
39
  public async Task<ActionResult<BorrowingDto>> ReturnBook(int id)
40
  {
41
  try
 
50
  }
51
 
52
  [HttpGet("me")]
53
+ [Authorize]
54
  public async Task<ActionResult<IEnumerable<BorrowingDto>>> GetMyBorrowings()
55
  {
56
  var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
 
62
  }
63
 
64
  [HttpGet]
65
+ [Authorize(Roles = "Librarian")]
66
  public async Task<ActionResult<IEnumerable<BorrowingDto>>> GetAllBorrowings()
67
  {
68
  var borrowings = await _borrowingService.GetAllBorrowingsAsync();
Backend/Features/Loyalty/LoyaltyController.cs CHANGED
@@ -32,7 +32,11 @@ namespace Backend.Features.Loyalty
32
  return NotFound("Loyalty account not found.");
33
  }
34
 
35
- return Ok(account);
 
 
 
 
36
  }
37
  }
38
  }
 
32
  return NotFound("Loyalty account not found.");
33
  }
34
 
35
+ return Ok(new
36
+ {
37
+ currentBalance = account.CurrentBalance,
38
+ tier = account.Tier
39
+ });
40
  }
41
  }
42
  }
Backend/Features/Loyalty/LoyaltyService.cs CHANGED
@@ -1,6 +1,8 @@
1
  using System;
2
  using System.Net.Http;
3
  using System.Net.Http.Json;
 
 
4
  using System.Threading.Tasks;
5
  using Microsoft.Extensions.Logging;
6
 
@@ -93,7 +95,8 @@ namespace Backend.Features.Loyalty
93
  var response = await _httpClient.GetAsync($"/api/v1/accounts/lookup/{SystemId}/{externalUserId}");
94
  if (response.IsSuccessStatusCode)
95
  {
96
- return await response.Content.ReadFromJsonAsync<AccountLookupResponse>();
 
97
  }
98
  return null;
99
  }
@@ -107,10 +110,19 @@ namespace Backend.Features.Loyalty
107
 
108
  public class AccountLookupResponse
109
  {
 
110
  public string Id { get; set; } = string.Empty;
 
 
111
  public string ExternalUserId { get; set; } = string.Empty;
 
 
112
  public double CurrentBalance { get; set; }
 
 
113
  public string Tier { get; set; } = string.Empty;
 
 
114
  public double LifetimePoints { get; set; }
115
  }
116
  }
 
1
  using System;
2
  using System.Net.Http;
3
  using System.Net.Http.Json;
4
+ using System.Text.Json;
5
+ using System.Text.Json.Serialization;
6
  using System.Threading.Tasks;
7
  using Microsoft.Extensions.Logging;
8
 
 
95
  var response = await _httpClient.GetAsync($"/api/v1/accounts/lookup/{SystemId}/{externalUserId}");
96
  if (response.IsSuccessStatusCode)
97
  {
98
+ var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
99
+ return await response.Content.ReadFromJsonAsync<AccountLookupResponse>(options);
100
  }
101
  return null;
102
  }
 
110
 
111
  public class AccountLookupResponse
112
  {
113
+ [JsonPropertyName("id")]
114
  public string Id { get; set; } = string.Empty;
115
+
116
+ [JsonPropertyName("externalUserId")]
117
  public string ExternalUserId { get; set; } = string.Empty;
118
+
119
+ [JsonPropertyName("currentBalance")]
120
  public double CurrentBalance { get; set; }
121
+
122
+ [JsonPropertyName("tier")]
123
  public string Tier { get; set; } = string.Empty;
124
+
125
+ [JsonPropertyName("lifetimePoints")]
126
  public double LifetimePoints { get; set; }
127
  }
128
  }
Backend/Features/Subscriptions/SubscriptionController.cs CHANGED
@@ -57,7 +57,7 @@ namespace Backend.Features.Subscriptions
57
  }
58
 
59
  [HttpGet("subscriptions/user/{userId}")]
60
- [Authorize(Roles = "Admin")]
61
  public async Task<ActionResult<SubscriptionDto>> GetUserSubscription(int userId)
62
  {
63
  var subscription = await _subscriptionService.GetUserSubscriptionAsync(userId);
@@ -67,7 +67,7 @@ namespace Backend.Features.Subscriptions
67
  }
68
 
69
  [HttpPost("subscriptions/admin-subscribe")]
70
- [Authorize(Roles = "Admin")]
71
  public async Task<ActionResult<SubscriptionDto>> AdminSubscribe([FromBody] AdminSubscribeRequest request)
72
  {
73
  try
 
57
  }
58
 
59
  [HttpGet("subscriptions/user/{userId}")]
60
+ [Authorize(Roles = "Librarian")]
61
  public async Task<ActionResult<SubscriptionDto>> GetUserSubscription(int userId)
62
  {
63
  var subscription = await _subscriptionService.GetUserSubscriptionAsync(userId);
 
67
  }
68
 
69
  [HttpPost("subscriptions/admin-subscribe")]
70
+ [Authorize(Roles = "Librarian")]
71
  public async Task<ActionResult<SubscriptionDto>> AdminSubscribe([FromBody] AdminSubscribeRequest request)
72
  {
73
  try
Backend/Features/Users/UserController.cs CHANGED
@@ -15,7 +15,7 @@ namespace Backend.Features.Users
15
  }
16
 
17
  [HttpGet]
18
- //[Authorize(Roles = "Librarian")]
19
  public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers()
20
  {
21
  var users = await _userService.GetAllUsersAsync();
@@ -23,7 +23,7 @@ namespace Backend.Features.Users
23
  }
24
 
25
  [HttpGet("{id}")]
26
- //[Authorize(Roles = "Librarian")]
27
  public async Task<ActionResult<UserDto>> GetUser(int id)
28
  {
29
  var user = await _userService.GetUserByIdAsync(id);
@@ -47,7 +47,7 @@ namespace Backend.Features.Users
47
  }
48
 
49
  [HttpPut("{id}")]
50
- //[Authorize(Roles = "Librarian")]
51
  public async Task<ActionResult<UserDto>> UpdateUser(int id, [FromBody] UserUpdateRequest request)
52
  {
53
  var updatedUser = await _userService.UpdateUserAsync(id, request);
@@ -56,7 +56,7 @@ namespace Backend.Features.Users
56
  }
57
 
58
  [HttpPatch("{id}/role")]
59
- //[Authorize(Roles = "Librarian")]
60
  public async Task<IActionResult> UpdateUserRole(int id, [FromBody] UserRoleUpdateRequest request)
61
  {
62
  var success = await _userService.UpdateUserRoleAsync(id, request.Role);
@@ -65,7 +65,7 @@ namespace Backend.Features.Users
65
  }
66
 
67
  [HttpDelete("{id}")]
68
- //[Authorize(Roles = "Librarian")]
69
  public async Task<IActionResult> DeleteUser(int id)
70
  {
71
  var success = await _userService.DeleteUserAsync(id);
 
15
  }
16
 
17
  [HttpGet]
18
+ [Authorize(Roles = "Librarian")]
19
  public async Task<ActionResult<IEnumerable<UserDto>>> GetUsers()
20
  {
21
  var users = await _userService.GetAllUsersAsync();
 
23
  }
24
 
25
  [HttpGet("{id}")]
26
+ [Authorize(Roles = "Librarian")]
27
  public async Task<ActionResult<UserDto>> GetUser(int id)
28
  {
29
  var user = await _userService.GetUserByIdAsync(id);
 
47
  }
48
 
49
  [HttpPut("{id}")]
50
+ [Authorize(Roles = "Librarian")]
51
  public async Task<ActionResult<UserDto>> UpdateUser(int id, [FromBody] UserUpdateRequest request)
52
  {
53
  var updatedUser = await _userService.UpdateUserAsync(id, request);
 
56
  }
57
 
58
  [HttpPatch("{id}/role")]
59
+ [Authorize(Roles = "Librarian")]
60
  public async Task<IActionResult> UpdateUserRole(int id, [FromBody] UserRoleUpdateRequest request)
61
  {
62
  var success = await _userService.UpdateUserRoleAsync(id, request.Role);
 
65
  }
66
 
67
  [HttpDelete("{id}")]
68
+ [Authorize(Roles = "Librarian")]
69
  public async Task<IActionResult> DeleteUser(int id)
70
  {
71
  var success = await _userService.DeleteUserAsync(id);
Backend/Features/Users/UserService.cs CHANGED
@@ -79,7 +79,7 @@ namespace Backend.Features.Users
79
  await _loyaltyService.ProcessEventAsync(
80
  externalUserId: externalUserId,
81
  eventKey: "SIGNUP",
82
- eventValue: 0,
83
  referenceId: $"USR-{user.Id}",
84
  description: "New User Registration",
85
  email: user.Email,
 
79
  await _loyaltyService.ProcessEventAsync(
80
  externalUserId: externalUserId,
81
  eventKey: "SIGNUP",
82
+ eventValue: 20,
83
  referenceId: $"USR-{user.Id}",
84
  description: "New User Registration",
85
  email: user.Email,
Frontend/Controllers/AuthController.cs CHANGED
@@ -31,8 +31,8 @@ namespace Frontend.Controllers
31
  var cookieOptions = new CookieOptions
32
  {
33
  HttpOnly = true,
34
- Secure = true,
35
- SameSite = SameSiteMode.Strict,
36
  Expires = DateTime.UtcNow.AddDays(7)
37
  };
38
  Response.Cookies.Append("AuthToken", response.Token, cookieOptions);
@@ -61,8 +61,8 @@ namespace Frontend.Controllers
61
  var cookieOptions = new CookieOptions
62
  {
63
  HttpOnly = true,
64
- Secure = true,
65
- SameSite = SameSiteMode.Strict,
66
  Expires = DateTime.UtcNow.AddDays(7)
67
  };
68
  Response.Cookies.Append("AuthToken", response.Token, cookieOptions);
 
31
  var cookieOptions = new CookieOptions
32
  {
33
  HttpOnly = true,
34
+ Secure = false,
35
+ SameSite = SameSiteMode.Lax,
36
  Expires = DateTime.UtcNow.AddDays(7)
37
  };
38
  Response.Cookies.Append("AuthToken", response.Token, cookieOptions);
 
61
  var cookieOptions = new CookieOptions
62
  {
63
  HttpOnly = true,
64
+ Secure = false,
65
+ SameSite = SameSiteMode.Lax,
66
  Expires = DateTime.UtcNow.AddDays(7)
67
  };
68
  Response.Cookies.Append("AuthToken", response.Token, cookieOptions);
Frontend/Controllers/BorrowingsController.cs CHANGED
@@ -1,6 +1,7 @@
1
- using Microsoft.AspNetCore.Mvc;
2
- using Frontend.Services;
3
  using Frontend.Models.Dtos;
 
 
 
4
 
5
  namespace Frontend.Controllers
6
  {
@@ -15,16 +16,15 @@ namespace Frontend.Controllers
15
 
16
  public async Task<IActionResult> Index()
17
  {
18
- if (User.IsInRole("Admin"))
19
- {
20
- var allBorrowings = await _apiClient.GetAllBorrowingsAsync();
21
- return View(allBorrowings);
22
- }
23
- else
24
- {
25
- var borrowings = await _apiClient.GetMyBorrowingsAsync();
26
- return View(borrowings);
27
- }
28
  }
29
 
30
  [HttpPost]
@@ -44,7 +44,8 @@ namespace Frontend.Controllers
44
  }
45
 
46
  [HttpPost]
47
- public async Task<IActionResult> Return(int id)
 
48
  {
49
  var result = await _apiClient.ReturnBookAsync(id);
50
  if (result != null)
@@ -55,6 +56,10 @@ namespace Frontend.Controllers
55
  {
56
  TempData["Error"] = "Failed to process return.";
57
  }
 
 
 
 
58
  return RedirectToAction(nameof(Index));
59
  }
60
  }
 
 
 
1
  using Frontend.Models.Dtos;
2
+ using Frontend.Services;
3
+ using Microsoft.AspNetCore.Authorization;
4
+ using Microsoft.AspNetCore.Mvc;
5
 
6
  namespace Frontend.Controllers
7
  {
 
16
 
17
  public async Task<IActionResult> Index()
18
  {
19
+ var borrowings = await _apiClient.GetMyBorrowingsAsync();
20
+ return View(borrowings);
21
+ }
22
+
23
+ [Authorize(Roles = "Librarian")]
24
+ public async Task<IActionResult> Manage()
25
+ {
26
+ var allBorrowings = await _apiClient.GetAllBorrowingsAsync();
27
+ return View(allBorrowings);
 
28
  }
29
 
30
  [HttpPost]
 
44
  }
45
 
46
  [HttpPost]
47
+ [Authorize(Roles = "Librarian")]
48
+ public async Task<IActionResult> Return(int id, bool fromManage = false)
49
  {
50
  var result = await _apiClient.ReturnBookAsync(id);
51
  if (result != null)
 
56
  {
57
  TempData["Error"] = "Failed to process return.";
58
  }
59
+
60
+ if (fromManage)
61
+ return RedirectToAction(nameof(Manage));
62
+
63
  return RedirectToAction(nameof(Index));
64
  }
65
  }
Frontend/Controllers/HomeController.cs CHANGED
@@ -18,37 +18,61 @@ namespace Frontend.Controllers
18
 
19
  public async Task<IActionResult> Index()
20
  {
 
 
 
 
 
 
 
 
 
21
  try
22
  {
 
23
  var books = await _apiClient.GetBooksAsync();
24
- var borrowings = await _apiClient.GetMyBorrowingsAsync();
25
- var members = await _apiClient.GetUsersAsync();
26
-
27
- // Fetch loyalty points (if user is logged in)
28
  if (User.Identity?.IsAuthenticated == true)
29
  {
30
- var loyaltyAccount = await _apiClient.GetMyLoyaltyAccountAsync();
31
- ViewBag.LoyaltyPoints = loyaltyAccount?.CurrentBalance ?? 0;
32
- ViewBag.LoyaltyTier = loyaltyAccount?.Tier ?? "Member";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
34
-
35
- ViewBag.TotalBooks = books.Count();
36
- ViewBag.ActiveLoans = borrowings.Count(b => !b.ReturnDate.HasValue);
37
- ViewBag.OverdueLoans = borrowings.Count(b => !b.ReturnDate.HasValue && b.DueDate < DateTime.UtcNow);
38
- ViewBag.TotalMembers = members.Count();
39
-
40
- ViewBag.RecentBooks = books.Take(6);
41
  }
42
  catch (Exception ex)
43
  {
44
- _logger.LogError(ex, "Failed to load dashboard data");
45
- // Set default values if API fails
46
- ViewBag.TotalBooks = 0;
47
- ViewBag.ActiveLoans = 0;
48
- ViewBag.TotalMembers = 0;
49
- ViewBag.RecentBooks = Enumerable.Empty<Frontend.Models.Dtos.BookDto>();
50
- ViewBag.LoyaltyPoints = 0;
51
- ViewBag.LoyaltyTier = "Member";
52
  }
53
 
54
  return View();
 
18
 
19
  public async Task<IActionResult> Index()
20
  {
21
+ // Set defaults to avoid nulls
22
+ ViewBag.TotalBooks = 0;
23
+ ViewBag.ActiveLoans = 0;
24
+ ViewBag.OverdueLoans = 0;
25
+ ViewBag.TotalMembers = 0;
26
+ ViewBag.LoyaltyPoints = 0;
27
+ ViewBag.LoyaltyStatus = "Not Authenticated";
28
+ ViewBag.RecentBooks = Enumerable.Empty<Frontend.Models.Dtos.BookDto>();
29
+
30
  try
31
  {
32
+ // 1. Basic Stats (Public)
33
  var books = await _apiClient.GetBooksAsync();
34
+ ViewBag.TotalBooks = books.Count();
35
+ ViewBag.RecentBooks = books.Take(6);
36
+
37
+ // 2. Personal/Authenticated Stats
38
  if (User.Identity?.IsAuthenticated == true)
39
  {
40
+ // Fetch borrowings
41
+ try {
42
+ var borrowings = await _apiClient.GetMyBorrowingsAsync();
43
+ ViewBag.ActiveLoans = borrowings.Count(b => !b.ReturnDate.HasValue);
44
+ ViewBag.OverdueLoans = borrowings.Count(b => !b.ReturnDate.HasValue && b.DueDate < DateTime.UtcNow);
45
+ } catch { /* Ignore personal stats failure */ }
46
+
47
+ // Fetch loyalty
48
+ try {
49
+ var loyaltyAccount = await _apiClient.GetMyLoyaltyAccountAsync();
50
+ if (loyaltyAccount != null)
51
+ {
52
+ ViewBag.LoyaltyPoints = loyaltyAccount.CurrentBalance;
53
+ ViewBag.LoyaltyTier = loyaltyAccount.Tier;
54
+ ViewBag.LoyaltyStatus = "Connected";
55
+ }
56
+ else {
57
+ ViewBag.LoyaltyStatus = "Account Not Linked";
58
+ }
59
+ } catch {
60
+ ViewBag.LoyaltyStatus = "Loyalty System Unavailable";
61
+ }
62
+
63
+ // 3. Librarian Only Stats
64
+ if (User.IsInRole("Librarian"))
65
+ {
66
+ try {
67
+ var members = await _apiClient.GetUsersAsync();
68
+ ViewBag.TotalMembers = members.Count();
69
+ } catch { /* Ignore member list failure for non-librarians */ }
70
+ }
71
  }
 
 
 
 
 
 
 
72
  }
73
  catch (Exception ex)
74
  {
75
+ _logger.LogError(ex, "Critical failure on dashboard. Some data might be missing.");
 
 
 
 
 
 
 
76
  }
77
 
78
  return View();
Frontend/Controllers/MembersController.cs CHANGED
@@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Authorization;
5
 
6
  namespace Frontend.Controllers
7
  {
8
- [Authorize(Roles = "Admin")]
9
  public class MembersController : Controller
10
  {
11
  private readonly LibraryApiClient _apiClient;
 
5
 
6
  namespace Frontend.Controllers
7
  {
8
+ [Authorize(Roles = "Librarian")]
9
  public class MembersController : Controller
10
  {
11
  private readonly LibraryApiClient _apiClient;
Frontend/Models/Dtos/LibraryDtos.cs CHANGED
@@ -1,3 +1,5 @@
 
 
1
  namespace Frontend.Models.Dtos
2
  {
3
  // Auth DTOs
@@ -127,10 +129,19 @@ namespace Frontend.Models.Dtos
127
  // Loyalty DTOs
128
  public class LoyaltyAccountDto
129
  {
 
130
  public string Id { get; set; } = string.Empty;
 
 
131
  public string ExternalUserId { get; set; } = string.Empty;
 
 
132
  public double CurrentBalance { get; set; }
 
 
133
  public string Tier { get; set; } = string.Empty;
 
 
134
  public double LifetimePoints { get; set; }
135
  }
136
 
 
1
+ using System.Text.Json.Serialization;
2
+
3
  namespace Frontend.Models.Dtos
4
  {
5
  // Auth DTOs
 
129
  // Loyalty DTOs
130
  public class LoyaltyAccountDto
131
  {
132
+ [JsonPropertyName("id")]
133
  public string Id { get; set; } = string.Empty;
134
+
135
+ [JsonPropertyName("externalUserId")]
136
  public string ExternalUserId { get; set; } = string.Empty;
137
+
138
+ [JsonPropertyName("currentBalance")]
139
  public double CurrentBalance { get; set; }
140
+
141
+ [JsonPropertyName("tier")]
142
  public string Tier { get; set; } = string.Empty;
143
+
144
+ [JsonPropertyName("lifetimePoints")]
145
  public double LifetimePoints { get; set; }
146
  }
147
 
Frontend/Services/LibraryApiClient.cs CHANGED
@@ -1,4 +1,5 @@
1
  using System.Net.Http.Json;
 
2
  using Frontend.Models.Dtos;
3
 
4
  namespace Frontend.Services
@@ -158,14 +159,29 @@ namespace Frontend.Services
158
  }
159
  #endregion
160
 
161
- #region Loyalty
162
  public async Task<LoyaltyAccountDto?> GetMyLoyaltyAccountAsync()
163
  {
164
  try {
165
- return await _httpClient.GetFromJsonAsync<LoyaltyAccountDto>("api/loyalty/my-account");
166
- } catch { return null; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  }
168
- #endregion
169
 
170
  #region Users
171
  public async Task<IEnumerable<UserDto>> GetUsersAsync()
 
1
  using System.Net.Http.Json;
2
+ using System.Text.Json;
3
  using Frontend.Models.Dtos;
4
 
5
  namespace Frontend.Services
 
159
  }
160
  #endregion
161
 
 
162
  public async Task<LoyaltyAccountDto?> GetMyLoyaltyAccountAsync()
163
  {
164
  try {
165
+ // Using exact casing for the route to be safe
166
+ var response = await _httpClient.GetAsync("api/Loyalty/my-account");
167
+ if (response.IsSuccessStatusCode)
168
+ {
169
+ // JsonPropertyName attributes in LoyaltyAccountDto will handle the mapping
170
+ // and correctly populate CurrentBalance from the backend's currentBalance
171
+ return await response.Content.ReadFromJsonAsync<LoyaltyAccountDto>();
172
+ }
173
+ else
174
+ {
175
+ var error = await response.Content.ReadAsStringAsync();
176
+ System.Diagnostics.Debug.WriteLine($"Loyalty API error: {response.StatusCode} - {error}");
177
+ // Throw custom exception or return null based on error
178
+ }
179
+ return null;
180
+ } catch (Exception ex) {
181
+ System.Diagnostics.Debug.WriteLine($"Loyalty API Exception: {ex.Message}");
182
+ return null;
183
+ }
184
  }
 
185
 
186
  #region Users
187
  public async Task<IEnumerable<UserDto>> GetUsersAsync()
Frontend/Views/Auth/Register.cshtml CHANGED
@@ -29,6 +29,11 @@
29
  <input asp-for="Email" type="email" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="name@example.com">
30
  <span asp-validation-for="Email" class="text-xs text-red-500 mt-1"></span>
31
  </div>
 
 
 
 
 
32
  <div>
33
  <label asp-for="Password" class="block text-sm font-medium text-slate-700 mb-1">Password</label>
34
  <input asp-for="Password" type="password" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="••••••••">
 
29
  <input asp-for="Email" type="email" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="name@example.com">
30
  <span asp-validation-for="Email" class="text-xs text-red-500 mt-1"></span>
31
  </div>
32
+ <div>
33
+ <label asp-for="PhoneNumber" class="block text-sm font-medium text-slate-700 mb-1">Phone Number</label>
34
+ <input asp-for="PhoneNumber" type="tel" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="09123456789">
35
+ <span asp-validation-for="PhoneNumber" class="text-xs text-red-500 mt-1"></span>
36
+ </div>
37
  <div>
38
  <label asp-for="Password" class="block text-sm font-medium text-slate-700 mb-1">Password</label>
39
  <input asp-for="Password" type="password" required class="appearance-none relative block w-full px-3 py-2 border border-slate-300 placeholder-slate-400 text-slate-900 rounded-md focus:outline-none focus:ring-slate-900 focus:border-slate-900 focus:z-10 sm:text-sm" placeholder="••••••••">
Frontend/Views/Borrowings/Manage.cshtml ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @model IEnumerable<Frontend.Models.Dtos.BorrowingDto>
2
+ @{
3
+ ViewData["Title"] = "Manage Borrowings";
4
+ }
5
+
6
+ <div class="mb-10">
7
+ <div class="flex items-center justify-between mb-4">
8
+ <div>
9
+ <h1 class="text-3xl font-extrabold tracking-tight text-slate-900 border-b border-slate-100 pb-4 mb-2">Manage All Borrowings</h1>
10
+ <p class="text-slate-500 font-medium">Monitor and process library loans across all members.</p>
11
+ </div>
12
+ <div class="flex gap-3">
13
+ <div class="card px-4 py-2 flex items-center gap-2 bg-slate-50 border-slate-200">
14
+ <span class="text-xs font-bold text-slate-400 uppercase tracking-widest">Total Active:</span>
15
+ <span class="text-lg font-bold text-slate-900">@Model.Count(m => m.Status == "Borrowed")</span>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="card overflow-hidden">
22
+ <div class="overflow-x-auto">
23
+ <table class="w-full text-left border-collapse">
24
+ <thead>
25
+ <tr class="bg-slate-50 border-b border-slate-200">
26
+ <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Book & Loan Info</th>
27
+ <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Member Details</th>
28
+ <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Loan Dates</th>
29
+ <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest text-center">Status</th>
30
+ <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Fines</th>
31
+ <th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest text-right">Actions</th>
32
+ </tr>
33
+ </thead>
34
+ <tbody class="divide-y divide-slate-100">
35
+ @if (!Model.Any())
36
+ {
37
+ <tr>
38
+ <td colspan="6" class="px-6 py-16 text-center text-slate-400 italic bg-white">
39
+ No borrowing records found in the system.
40
+ </td>
41
+ </tr>
42
+ }
43
+ @foreach (var loan in Model.OrderByDescending(l => l.BorrowDate))
44
+ {
45
+ <tr class="hover:bg-slate-50/80 transition-colors">
46
+ <td class="px-6 py-5">
47
+ <div class="flex items-center gap-4">
48
+ <div class="w-10 h-14 bg-slate-900/5 rounded-lg flex items-center justify-center text-slate-400 border border-slate-100">
49
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18 18.246 18.477 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
50
+ </div>
51
+ <div class="min-w-0">
52
+ <p class="text-sm font-bold text-slate-900 truncate mb-0.5">@loan.BookTitle</p>
53
+ <p class="text-[10px] font-mono text-slate-400 uppercase tracking-tight">ID: #@loan.Id</p>
54
+ </div>
55
+ </div>
56
+ </td>
57
+ <td class="px-6 py-5">
58
+ <div class="flex flex-col gap-1">
59
+ <p class="text-sm font-semibold text-slate-800">@loan.UserEmail</p>
60
+ <p class="text-[10px] text-slate-500 font-medium tracking-wide">UID: #@loan.UserId</p>
61
+ </div>
62
+ </td>
63
+ <td class="px-6 py-5">
64
+ <div class="grid gap-1">
65
+ <div class="flex items-center gap-2 text-[11px]">
66
+ <span class="text-slate-400 w-12">Out:</span>
67
+ <span class="font-bold text-slate-700">@loan.BorrowDate.ToString("MMM dd, yyyy")</span>
68
+ </div>
69
+ <div class="flex items-center gap-2 text-[11px]">
70
+ <span class="text-slate-400 w-12">Due:</span>
71
+ @{
72
+ var isOverdue = loan.DueDate < DateTime.Now && loan.Status == "Borrowed";
73
+ }
74
+ <span class="font-bold @(isOverdue ? "text-rose-600 animate-pulse" : "text-amber-700")">@loan.DueDate.ToString("MMM dd, yyyy")</span>
75
+ </div>
76
+ @if (loan.ReturnDate.HasValue)
77
+ {
78
+ <div class="flex items-center gap-2 text-[11px]">
79
+ <span class="text-slate-400 w-12">Back:</span>
80
+ <span class="font-bold text-emerald-600">@loan.ReturnDate.Value.ToString("MMM dd, yyyy")</span>
81
+ </div>
82
+ }
83
+ </div>
84
+ </td>
85
+ <td class="px-6 py-5 text-center">
86
+ <span class="inline-flex items-center px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider
87
+ @(loan.Status == "Borrowed" ? "bg-blue-100 text-blue-700" : "bg-emerald-100 text-emerald-700")">
88
+ @loan.Status
89
+ </span>
90
+ </td>
91
+ <td class="px-6 py-5">
92
+ @if (loan.FineAmount > 0)
93
+ {
94
+ <div class="flex flex-col">
95
+ <span class="text-sm font-extrabold @(loan.IsFinePaid ? "text-slate-400 line-through" : "text-rose-600")">
96
+ MMK @loan.FineAmount.ToString("N0")
97
+ </span>
98
+ @if (loan.IsFinePaid)
99
+ {
100
+ <span class="text-[9px] text-emerald-600 font-bold uppercase mt-0.5 tracking-tighter">Settled</span>
101
+ }
102
+ else
103
+ {
104
+ <span class="text-[9px] text-rose-400 font-bold uppercase mt-0.5 tracking-tighter">Pending</span>
105
+ }
106
+ </div>
107
+ }
108
+ else
109
+ {
110
+ <span class="text-[10px] text-slate-300 font-bold italic">No Fines</span>
111
+ }
112
+ </td>
113
+ <td class="px-6 py-5 text-right">
114
+ @if (loan.Status == "Borrowed")
115
+ {
116
+ <form asp-action="Return" method="POST" onsubmit="return confirm('Process return for this book?');">
117
+ <input type="hidden" name="id" value="@loan.Id" />
118
+ <input type="hidden" name="fromManage" value="true" />
119
+ <button type="submit" class="inline-flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-slate-800 transition-all shadow-sm">
120
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M11 19l-7-7 7-7m8 14l-7-7 7-7"></path></svg>
121
+ Process Return
122
+ </button>
123
+ </form>
124
+ }
125
+ else
126
+ {
127
+ <div class="group relative inline-block">
128
+ <div class="p-2 bg-slate-100 rounded-full text-slate-400 cursor-help">
129
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
130
+ </div>
131
+ <span class="absolute hidden group-hover:block bg-slate-800 text-white text-[9px] px-2 py-1 rounded right-0 top-full mt-1 whitespace-nowrap z-10">Completed</span>
132
+ </div>
133
+ }
134
+ </td>
135
+ </tr>
136
+ }
137
+ </tbody>
138
+ </table>
139
+ </div>
140
+ </div>
Frontend/Views/Home/Index.cshtml CHANGED
@@ -18,6 +18,7 @@
18
  <h2 class="text-4xl font-extrabold">@ViewBag.LoyaltyPoints</h2>
19
  <span class="text-amber-200 font-medium tracking-wide">Points</span>
20
  </div>
 
21
  </div>
22
  <div class="text-right">
23
  <div class="inline-flex items-center gap-2 bg-black/20 backdrop-blur-sm px-4 py-2 rounded-full border border-white/10 shadow-inner">
 
18
  <h2 class="text-4xl font-extrabold">@ViewBag.LoyaltyPoints</h2>
19
  <span class="text-amber-200 font-medium tracking-wide">Points</span>
20
  </div>
21
+ <p class="text-[10px] text-amber-300/60 mt-1 uppercase tracking-tighter">Connection: @ViewBag.LoyaltyStatus</p>
22
  </div>
23
  <div class="text-right">
24
  <div class="inline-flex items-center gap-2 bg-black/20 backdrop-blur-sm px-4 py-2 rounded-full border border-white/10 shadow-inner">
Frontend/Views/Shared/_Layout.cshtml CHANGED
@@ -79,16 +79,18 @@
79
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8"/><path d="M3 16.2V21m0 0h4.8M3 21l6-6"/><path d="M21 7.8V3m0 0h-4.8M21 3l-6 6"/><path d="M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>
80
  Active Loans
81
  </a>
82
- @if (User.Identity?.IsAuthenticated == true && User.IsInRole("Admin"))
83
  {
84
  <a href="/Members" class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "Members" ? "nav-link-active" : "nav-link-inactive")">
85
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
86
  Members
87
  </a>
88
- }
89
-
90
- @if (User.Identity?.IsAuthenticated == true && User.IsInRole("Admin"))
91
- {
 
 
92
  <a href="/Subscriptions" class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "Subscriptions" ? "nav-link-active" : "nav-link-inactive")">
93
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
94
  Memberships
 
79
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="m21 21-6-6m6 6v-4.8m0 4.8h-4.8"/><path d="M3 16.2V21m0 0h4.8M3 21l6-6"/><path d="M21 7.8V3m0 0h-4.8M21 3l-6 6"/><path d="M3 7.8V3m0 0h4.8M3 3l6 6"/></svg>
80
  Active Loans
81
  </a>
82
+ @if (User.Identity?.IsAuthenticated == true && User.IsInRole("Librarian"))
83
  {
84
  <a href="/Members" class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "Members" ? "nav-link-active" : "nav-link-inactive")">
85
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
86
  Members
87
  </a>
88
+
89
+ <a asp-controller="Borrowings" asp-action="Manage" class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "Borrowings" && ViewContext.RouteData.Values["Action"]?.ToString() == "Manage" ? "nav-link-active" : "nav-link-inactive")">
90
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><path d="M11 15h2a2 2 0 1 0 0-4h-2a2 2 0 1 1 0-4h2"/><path d="M12 5v14"/><path d="M18 19V5l-4 4"/><circle cx="12" cy="12" r="10"/></svg>
91
+ Manage Borrowings
92
+ </a>
93
+
94
  <a href="/Subscriptions" class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "Subscriptions" ? "nav-link-active" : "nav-link-inactive")">
95
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="flex-shrink-0 mr-3"><rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/></svg>
96
  Memberships