Spaces:
Sleeping
Sleeping
Yuyuqt commited on
Commit ·
c312181
1
Parent(s): 1a7e978
add: subscription rewards, register rewards, fix some features
Browse files- Backend/Features/Auth/AuthService.cs +18 -1
- Backend/Features/Books/BookController.cs +5 -5
- Backend/Features/Borrowing/BorrowingController.cs +4 -4
- Backend/Features/Loyalty/LoyaltyController.cs +5 -1
- Backend/Features/Loyalty/LoyaltyService.cs +13 -1
- Backend/Features/Subscriptions/SubscriptionController.cs +2 -2
- Backend/Features/Users/UserController.cs +5 -5
- Backend/Features/Users/UserService.cs +1 -1
- Frontend/Controllers/AuthController.cs +4 -4
- Frontend/Controllers/BorrowingsController.cs +18 -13
- Frontend/Controllers/HomeController.cs +46 -22
- Frontend/Controllers/MembersController.cs +1 -1
- Frontend/Models/Dtos/LibraryDtos.cs +11 -0
- Frontend/Services/LibraryApiClient.cs +20 -4
- Frontend/Views/Auth/Register.cshtml +5 -0
- Frontend/Views/Borrowings/Manage.cshtml +140 -0
- Frontend/Views/Home/Index.cshtml +1 -0
- Frontend/Views/Shared/_Layout.cshtml +7 -5
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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 = "
|
| 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 = "
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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:
|
| 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 =
|
| 35 |
-
SameSite = SameSiteMode.
|
| 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 =
|
| 65 |
-
SameSite = SameSiteMode.
|
| 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 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
}
|
| 28 |
}
|
| 29 |
|
| 30 |
[HttpPost]
|
|
@@ -44,7 +44,8 @@ namespace Frontend.Controllers
|
|
| 44 |
}
|
| 45 |
|
| 46 |
[HttpPost]
|
| 47 |
-
|
|
|
|
| 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 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
//
|
| 28 |
if (User.Identity?.IsAuthenticated == true)
|
| 29 |
{
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, "
|
| 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 = "
|
| 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 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("
|
| 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 |
-
|
| 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
|