Spaces:
Sleeping
Sleeping
Commit ·
b0a0ed0
1
Parent(s): c49ed24
feat: implement authentication and authorization features, add book and user management controllers, and enhance UI for login and registration
Browse files- Frontend/Controllers/AuthController.cs +110 -0
- Frontend/Controllers/BooksController.cs +109 -0
- Frontend/Controllers/BorrowingsController.cs +61 -0
- Frontend/Controllers/HomeController.cs +30 -3
- Frontend/Controllers/MembersController.cs +141 -0
- Frontend/Controllers/SubscriptionsController.cs +34 -0
- Frontend/Models/Dtos/LibraryDtos.cs +166 -0
- Frontend/Models/MemberDetailsViewModel.cs +12 -0
- Frontend/Models/SubscriptionsViewModel.cs +9 -0
- Frontend/Program.cs +20 -0
- Frontend/Services/LibraryApiClient.cs +197 -0
- Frontend/Views/Auth/Login.cshtml +49 -0
- Frontend/Views/Auth/Register.cshtml +51 -0
- Frontend/Views/Books/Create.cshtml +83 -0
- Frontend/Views/Books/Details.cshtml +71 -0
- Frontend/Views/Books/Edit.cshtml +74 -0
- Frontend/Views/Books/Index.cshtml +74 -0
- Frontend/Views/Borrowings/Index.cshtml +105 -0
- Frontend/Views/Home/Index.cshtml +120 -5
- Frontend/Views/Members/Create.cshtml +90 -0
- Frontend/Views/Members/Details.cshtml +137 -0
- Frontend/Views/Members/Edit.cshtml +72 -0
- Frontend/Views/Members/Index.cshtml +91 -0
- Frontend/Views/Shared/Components/_Card.cshtml +4 -0
- Frontend/Views/Shared/_Layout.cshtml +201 -36
- Frontend/Views/Subscriptions/Index.cshtml +47 -0
- Frontend/appsettings.json +2 -1
Frontend/Controllers/AuthController.cs
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Microsoft.AspNetCore.Mvc;
|
| 2 |
+
using Microsoft.AspNetCore.Authentication;
|
| 3 |
+
using Microsoft.AspNetCore.Authentication.Cookies;
|
| 4 |
+
using System.Security.Claims;
|
| 5 |
+
using Frontend.Models.Dtos;
|
| 6 |
+
using Frontend.Services;
|
| 7 |
+
|
| 8 |
+
namespace Frontend.Controllers
|
| 9 |
+
{
|
| 10 |
+
public class AuthController : Controller
|
| 11 |
+
{
|
| 12 |
+
private readonly LibraryApiClient _apiClient;
|
| 13 |
+
|
| 14 |
+
public AuthController(LibraryApiClient apiClient)
|
| 15 |
+
{
|
| 16 |
+
_apiClient = apiClient;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
[HttpGet]
|
| 20 |
+
public IActionResult Login() => View();
|
| 21 |
+
|
| 22 |
+
[HttpPost]
|
| 23 |
+
public async Task<IActionResult> Login(LoginRequest request)
|
| 24 |
+
{
|
| 25 |
+
if (!ModelState.IsValid) return View(request);
|
| 26 |
+
|
| 27 |
+
var response = await _apiClient.LoginAsync(request);
|
| 28 |
+
if (response != null && !string.IsNullOrEmpty(response.Token))
|
| 29 |
+
{
|
| 30 |
+
// Store token in cookie
|
| 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);
|
| 39 |
+
|
| 40 |
+
await SignInUserAsync(response, request.Email);
|
| 41 |
+
|
| 42 |
+
TempData["Success"] = "Welcome back!";
|
| 43 |
+
return RedirectToAction("Index", "Home");
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
ModelState.AddModelError("", "Invalid email or password.");
|
| 47 |
+
return View(request);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
[HttpGet]
|
| 51 |
+
public IActionResult Register() => View();
|
| 52 |
+
|
| 53 |
+
[HttpPost]
|
| 54 |
+
public async Task<IActionResult> Register(RegisterRequest request)
|
| 55 |
+
{
|
| 56 |
+
if (!ModelState.IsValid) return View(request);
|
| 57 |
+
|
| 58 |
+
var response = await _apiClient.RegisterAsync(request);
|
| 59 |
+
if (response != null && !string.IsNullOrEmpty(response.Token))
|
| 60 |
+
{
|
| 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);
|
| 69 |
+
|
| 70 |
+
await SignInUserAsync(response, request.Email);
|
| 71 |
+
|
| 72 |
+
TempData["Success"] = "Account created successfully!";
|
| 73 |
+
return RedirectToAction("Index", "Home");
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
ModelState.AddModelError("", "Registration failed. Email might already be in use.");
|
| 77 |
+
return View(request);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
private async Task SignInUserAsync(AuthResponse response, string email)
|
| 81 |
+
{
|
| 82 |
+
var claims = new List<Claim>
|
| 83 |
+
{
|
| 84 |
+
new Claim(ClaimTypes.Name, response.FullName),
|
| 85 |
+
new Claim(ClaimTypes.Email, email),
|
| 86 |
+
new Claim(ClaimTypes.Role, string.IsNullOrEmpty(response.Role) ? "Member" : response.Role)
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
| 90 |
+
var authProperties = new AuthenticationProperties
|
| 91 |
+
{
|
| 92 |
+
IsPersistent = true,
|
| 93 |
+
ExpiresUtc = DateTimeOffset.UtcNow.AddDays(7)
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
await HttpContext.SignInAsync(
|
| 97 |
+
CookieAuthenticationDefaults.AuthenticationScheme,
|
| 98 |
+
new ClaimsPrincipal(claimsIdentity),
|
| 99 |
+
authProperties);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
public async Task<IActionResult> Logout()
|
| 103 |
+
{
|
| 104 |
+
Response.Cookies.Delete("AuthToken");
|
| 105 |
+
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
| 106 |
+
TempData["Success"] = "Logged out successfully.";
|
| 107 |
+
return RedirectToAction("Login");
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
}
|
Frontend/Controllers/BooksController.cs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Microsoft.AspNetCore.Mvc;
|
| 2 |
+
using Frontend.Services;
|
| 3 |
+
using Frontend.Models.Dtos;
|
| 4 |
+
|
| 5 |
+
namespace Frontend.Controllers
|
| 6 |
+
{
|
| 7 |
+
public class BooksController : Controller
|
| 8 |
+
{
|
| 9 |
+
private readonly LibraryApiClient _apiClient;
|
| 10 |
+
|
| 11 |
+
public BooksController(LibraryApiClient apiClient)
|
| 12 |
+
{
|
| 13 |
+
_apiClient = apiClient;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
public async Task<IActionResult> Index()
|
| 17 |
+
{
|
| 18 |
+
var books = await _apiClient.GetBooksAsync();
|
| 19 |
+
return View(books);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
public async Task<IActionResult> Details(int id)
|
| 23 |
+
{
|
| 24 |
+
var book = await _apiClient.GetBookAsync(id);
|
| 25 |
+
if (book == null) return NotFound();
|
| 26 |
+
return View(book);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
[HttpGet]
|
| 30 |
+
public async Task<IActionResult> Create()
|
| 31 |
+
{
|
| 32 |
+
ViewBag.Categories = await _apiClient.GetCategoriesAsync();
|
| 33 |
+
return View();
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
[HttpPost]
|
| 37 |
+
public async Task<IActionResult> Create(BookCreateRequest request)
|
| 38 |
+
{
|
| 39 |
+
if (!ModelState.IsValid)
|
| 40 |
+
{
|
| 41 |
+
ViewBag.Categories = await _apiClient.GetCategoriesAsync();
|
| 42 |
+
return View(request);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
var result = await _apiClient.CreateBookAsync(request);
|
| 46 |
+
if (result != null)
|
| 47 |
+
{
|
| 48 |
+
TempData["Success"] = "Book added successfully!";
|
| 49 |
+
return RedirectToAction(nameof(Index));
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
ModelState.AddModelError("", "Failed to create book.");
|
| 53 |
+
ViewBag.Categories = await _apiClient.GetCategoriesAsync();
|
| 54 |
+
return View(request);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
[HttpGet]
|
| 58 |
+
public async Task<IActionResult> Edit(int id)
|
| 59 |
+
{
|
| 60 |
+
var book = await _apiClient.GetBookAsync(id);
|
| 61 |
+
if (book == null) return NotFound();
|
| 62 |
+
|
| 63 |
+
ViewBag.Categories = await _apiClient.GetCategoriesAsync();
|
| 64 |
+
return View(new BookUpdateRequest
|
| 65 |
+
{
|
| 66 |
+
Title = book.Title,
|
| 67 |
+
Author = book.Author,
|
| 68 |
+
Description = book.Description,
|
| 69 |
+
TotalCopies = book.TotalCopies
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
[HttpPost]
|
| 74 |
+
public async Task<IActionResult> Edit(int id, BookUpdateRequest request)
|
| 75 |
+
{
|
| 76 |
+
if (!ModelState.IsValid)
|
| 77 |
+
{
|
| 78 |
+
ViewBag.Categories = await _apiClient.GetCategoriesAsync();
|
| 79 |
+
return View(request);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
var result = await _apiClient.UpdateBookAsync(id, request);
|
| 83 |
+
if (result != null)
|
| 84 |
+
{
|
| 85 |
+
TempData["Success"] = "Book updated successfully!";
|
| 86 |
+
return RedirectToAction(nameof(Index));
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
ModelState.AddModelError("", "Failed to update book.");
|
| 90 |
+
ViewBag.Categories = await _apiClient.GetCategoriesAsync();
|
| 91 |
+
return View(request);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
[HttpPost]
|
| 95 |
+
public async Task<IActionResult> Delete(int id)
|
| 96 |
+
{
|
| 97 |
+
var success = await _apiClient.DeleteBookAsync(id);
|
| 98 |
+
if (success)
|
| 99 |
+
{
|
| 100 |
+
TempData["Success"] = "Book removed from catalog.";
|
| 101 |
+
}
|
| 102 |
+
else
|
| 103 |
+
{
|
| 104 |
+
TempData["Error"] = "Failed to delete book.";
|
| 105 |
+
}
|
| 106 |
+
return RedirectToAction(nameof(Index));
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
}
|
Frontend/Controllers/BorrowingsController.cs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Microsoft.AspNetCore.Mvc;
|
| 2 |
+
using Frontend.Services;
|
| 3 |
+
using Frontend.Models.Dtos;
|
| 4 |
+
|
| 5 |
+
namespace Frontend.Controllers
|
| 6 |
+
{
|
| 7 |
+
public class BorrowingsController : Controller
|
| 8 |
+
{
|
| 9 |
+
private readonly LibraryApiClient _apiClient;
|
| 10 |
+
|
| 11 |
+
public BorrowingsController(LibraryApiClient apiClient)
|
| 12 |
+
{
|
| 13 |
+
_apiClient = apiClient;
|
| 14 |
+
}
|
| 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]
|
| 31 |
+
public async Task<IActionResult> Borrow(int bookId)
|
| 32 |
+
{
|
| 33 |
+
var request = new BorrowRequest { BookId = bookId };
|
| 34 |
+
var result = await _apiClient.BorrowBookAsync(request);
|
| 35 |
+
|
| 36 |
+
if (result != null)
|
| 37 |
+
{
|
| 38 |
+
TempData["Success"] = "Book borrowed successfully!";
|
| 39 |
+
return RedirectToAction(nameof(Index));
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
TempData["Error"] = "Failed to borrow book. Check your subscription or book availability.";
|
| 43 |
+
return RedirectToAction("Details", "Books", new { id = bookId });
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
[HttpPost]
|
| 47 |
+
public async Task<IActionResult> Return(int id)
|
| 48 |
+
{
|
| 49 |
+
var result = await _apiClient.ReturnBookAsync(id);
|
| 50 |
+
if (result != null)
|
| 51 |
+
{
|
| 52 |
+
TempData["Success"] = "Book returned successfully!";
|
| 53 |
+
}
|
| 54 |
+
else
|
| 55 |
+
{
|
| 56 |
+
TempData["Error"] = "Failed to process return.";
|
| 57 |
+
}
|
| 58 |
+
return RedirectToAction(nameof(Index));
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
}
|
Frontend/Controllers/HomeController.cs
CHANGED
|
@@ -1,20 +1,47 @@
|
|
| 1 |
-
using Frontend.Models;
|
| 2 |
using Microsoft.AspNetCore.Mvc;
|
|
|
|
| 3 |
using System.Diagnostics;
|
|
|
|
| 4 |
|
| 5 |
namespace Frontend.Controllers
|
| 6 |
{
|
| 7 |
public class HomeController : Controller
|
| 8 |
{
|
|
|
|
| 9 |
private readonly ILogger<HomeController> _logger;
|
| 10 |
|
| 11 |
-
public HomeController(ILogger<HomeController> logger)
|
| 12 |
{
|
|
|
|
| 13 |
_logger = logger;
|
| 14 |
}
|
| 15 |
|
| 16 |
-
public IActionResult Index()
|
| 17 |
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
return View();
|
| 19 |
}
|
| 20 |
|
|
|
|
|
|
|
| 1 |
using Microsoft.AspNetCore.Mvc;
|
| 2 |
+
using Frontend.Services;
|
| 3 |
using System.Diagnostics;
|
| 4 |
+
using Frontend.Models;
|
| 5 |
|
| 6 |
namespace Frontend.Controllers
|
| 7 |
{
|
| 8 |
public class HomeController : Controller
|
| 9 |
{
|
| 10 |
+
private readonly LibraryApiClient _apiClient;
|
| 11 |
private readonly ILogger<HomeController> _logger;
|
| 12 |
|
| 13 |
+
public HomeController(LibraryApiClient apiClient, ILogger<HomeController> logger)
|
| 14 |
{
|
| 15 |
+
_apiClient = apiClient;
|
| 16 |
_logger = logger;
|
| 17 |
}
|
| 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 |
+
ViewBag.TotalBooks = books.Count();
|
| 28 |
+
ViewBag.ActiveLoans = borrowings.Count(b => !b.ReturnDate.HasValue);
|
| 29 |
+
ViewBag.OverdueLoans = borrowings.Count(b => !b.ReturnDate.HasValue && b.DueDate < DateTime.UtcNow);
|
| 30 |
+
ViewBag.TotalMembers = members.Count();
|
| 31 |
+
|
| 32 |
+
ViewBag.RecentBooks = books.Take(6);
|
| 33 |
+
}
|
| 34 |
+
catch (Exception ex)
|
| 35 |
+
{
|
| 36 |
+
_logger.LogError(ex, "Failed to load dashboard data");
|
| 37 |
+
// Set default values if API fails
|
| 38 |
+
ViewBag.TotalBooks = 0;
|
| 39 |
+
ViewBag.ActiveLoans = 0;
|
| 40 |
+
ViewBag.OverdueLoans = 0;
|
| 41 |
+
ViewBag.TotalMembers = 0;
|
| 42 |
+
ViewBag.RecentBooks = Enumerable.Empty<Frontend.Models.Dtos.BookDto>();
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
return View();
|
| 46 |
}
|
| 47 |
|
Frontend/Controllers/MembersController.cs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Microsoft.AspNetCore.Mvc;
|
| 2 |
+
using Frontend.Services;
|
| 3 |
+
using Frontend.Models.Dtos;
|
| 4 |
+
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;
|
| 12 |
+
|
| 13 |
+
public MembersController(LibraryApiClient apiClient)
|
| 14 |
+
{
|
| 15 |
+
_apiClient = apiClient;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
public async Task<IActionResult> Index()
|
| 19 |
+
{
|
| 20 |
+
var users = await _apiClient.GetUsersAsync();
|
| 21 |
+
return View(users);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
public async Task<IActionResult> Details(int id)
|
| 25 |
+
{
|
| 26 |
+
var user = await _apiClient.GetUserAsync(id);
|
| 27 |
+
if (user == null) return NotFound();
|
| 28 |
+
|
| 29 |
+
var memberships = await _apiClient.GetMembershipsAsync();
|
| 30 |
+
var subscription = await _apiClient.GetUserSubscriptionAsync(id);
|
| 31 |
+
|
| 32 |
+
var viewModel = new Frontend.Models.MemberDetailsViewModel
|
| 33 |
+
{
|
| 34 |
+
User = user,
|
| 35 |
+
AvailableMemberships = memberships?.ToList() ?? new List<MembershipDto>(),
|
| 36 |
+
CurrentSubscription = subscription
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
return View(viewModel);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
[HttpPost]
|
| 43 |
+
public async Task<IActionResult> AssignSubscription(int id, int membershipId)
|
| 44 |
+
{
|
| 45 |
+
var request = new AdminSubscribeRequest { UserId = id, MembershipId = membershipId };
|
| 46 |
+
var result = await _apiClient.AdminSubscribeAsync(request);
|
| 47 |
+
if (result != null)
|
| 48 |
+
{
|
| 49 |
+
TempData["Success"] = "Subscription assigned successfully.";
|
| 50 |
+
}
|
| 51 |
+
else
|
| 52 |
+
{
|
| 53 |
+
TempData["Error"] = "Failed to assign subscription.";
|
| 54 |
+
}
|
| 55 |
+
return RedirectToAction(nameof(Details), new { id });
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
[HttpGet]
|
| 59 |
+
public IActionResult Create()
|
| 60 |
+
{
|
| 61 |
+
return View();
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
[HttpPost]
|
| 65 |
+
public async Task<IActionResult> Create(UserCreateRequest request)
|
| 66 |
+
{
|
| 67 |
+
if (!ModelState.IsValid) return View(request);
|
| 68 |
+
|
| 69 |
+
var result = await _apiClient.CreateUserAsync(request);
|
| 70 |
+
if (result != null)
|
| 71 |
+
{
|
| 72 |
+
TempData["Success"] = "Member created successfully!";
|
| 73 |
+
return RedirectToAction(nameof(Index));
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
ModelState.AddModelError("", "Failed to create member. Email might already exist.");
|
| 77 |
+
return View(request);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
[HttpGet]
|
| 81 |
+
public async Task<IActionResult> Edit(int id)
|
| 82 |
+
{
|
| 83 |
+
var user = await _apiClient.GetUserAsync(id);
|
| 84 |
+
if (user == null) return NotFound();
|
| 85 |
+
|
| 86 |
+
return View(new UserUpdateRequest
|
| 87 |
+
{
|
| 88 |
+
FullName = user.FullName,
|
| 89 |
+
PhoneNumber = user.PhoneNumber,
|
| 90 |
+
StudentId = user.StudentId,
|
| 91 |
+
Address = user.Address
|
| 92 |
+
});
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
[HttpPost]
|
| 96 |
+
public async Task<IActionResult> Edit(int id, UserUpdateRequest request)
|
| 97 |
+
{
|
| 98 |
+
if (!ModelState.IsValid) return View(request);
|
| 99 |
+
|
| 100 |
+
var result = await _apiClient.UpdateUserAsync(id, request);
|
| 101 |
+
if (result != null)
|
| 102 |
+
{
|
| 103 |
+
TempData["Success"] = "Member updated successfully!";
|
| 104 |
+
return RedirectToAction(nameof(Index));
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
ModelState.AddModelError("", "Failed to update member.");
|
| 108 |
+
return View(request);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
[HttpPost]
|
| 112 |
+
public async Task<IActionResult> Delete(int id)
|
| 113 |
+
{
|
| 114 |
+
var success = await _apiClient.DeleteUserAsync(id);
|
| 115 |
+
if (success)
|
| 116 |
+
{
|
| 117 |
+
TempData["Success"] = "Member removed.";
|
| 118 |
+
}
|
| 119 |
+
else
|
| 120 |
+
{
|
| 121 |
+
TempData["Error"] = "Failed to delete member.";
|
| 122 |
+
}
|
| 123 |
+
return RedirectToAction(nameof(Index));
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
[HttpPost]
|
| 127 |
+
public async Task<IActionResult> UpdateRole(int id, string role)
|
| 128 |
+
{
|
| 129 |
+
var success = await _apiClient.UpdateUserRoleAsync(id, role);
|
| 130 |
+
if (success)
|
| 131 |
+
{
|
| 132 |
+
TempData["Success"] = $"Role updated to {role}.";
|
| 133 |
+
}
|
| 134 |
+
else
|
| 135 |
+
{
|
| 136 |
+
TempData["Error"] = "Failed to update role.";
|
| 137 |
+
}
|
| 138 |
+
return RedirectToAction(nameof(Details), new { id });
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
}
|
Frontend/Controllers/SubscriptionsController.cs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Microsoft.AspNetCore.Mvc;
|
| 2 |
+
using Frontend.Services;
|
| 3 |
+
using Frontend.Models.Dtos;
|
| 4 |
+
using Frontend.Models;
|
| 5 |
+
using Microsoft.AspNetCore.Authorization;
|
| 6 |
+
using System.Threading.Tasks;
|
| 7 |
+
using System.Linq;
|
| 8 |
+
|
| 9 |
+
namespace Frontend.Controllers
|
| 10 |
+
{
|
| 11 |
+
[Authorize(Roles = "Admin")]
|
| 12 |
+
public class SubscriptionsController : Controller
|
| 13 |
+
{
|
| 14 |
+
private readonly LibraryApiClient _apiClient;
|
| 15 |
+
|
| 16 |
+
public SubscriptionsController(LibraryApiClient apiClient)
|
| 17 |
+
{
|
| 18 |
+
_apiClient = apiClient;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
public async Task<IActionResult> Index()
|
| 22 |
+
{
|
| 23 |
+
var memberships = await _apiClient.GetMembershipsAsync();
|
| 24 |
+
|
| 25 |
+
var viewModel = new SubscriptionsViewModel
|
| 26 |
+
{
|
| 27 |
+
AvailableMemberships = memberships?.ToList() ?? new List<MembershipDto>()
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
return View(viewModel);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
}
|
| 34 |
+
}
|
Frontend/Models/Dtos/LibraryDtos.cs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
namespace Frontend.Models.Dtos
|
| 2 |
+
{
|
| 3 |
+
// Auth DTOs
|
| 4 |
+
public class RegisterRequest
|
| 5 |
+
{
|
| 6 |
+
public string FullName { get; set; } = string.Empty;
|
| 7 |
+
public string Email { get; set; } = string.Empty;
|
| 8 |
+
public string Password { get; set; } = string.Empty;
|
| 9 |
+
public string? PhoneNumber { get; set; }
|
| 10 |
+
public string? Address { get; set; }
|
| 11 |
+
public string? StudentId { get; set; }
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
public class LoginRequest
|
| 15 |
+
{
|
| 16 |
+
public string Email { get; set; } = string.Empty;
|
| 17 |
+
public string Password { get; set; } = string.Empty;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
public class AuthResponse
|
| 21 |
+
{
|
| 22 |
+
public string Token { get; set; } = string.Empty;
|
| 23 |
+
public string FullName { get; set; } = string.Empty;
|
| 24 |
+
public string Role { get; set; } = string.Empty;
|
| 25 |
+
public DateTime Expiry { get; set; }
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// Book DTOs
|
| 29 |
+
public class BookDto
|
| 30 |
+
{
|
| 31 |
+
public int Id { get; set; }
|
| 32 |
+
public string Title { get; set; } = string.Empty;
|
| 33 |
+
public string Isbn { get; set; } = string.Empty;
|
| 34 |
+
public string Author { get; set; } = string.Empty;
|
| 35 |
+
public string Status { get; set; } = string.Empty;
|
| 36 |
+
public bool IsActive { get; set; }
|
| 37 |
+
public string? Description { get; set; }
|
| 38 |
+
public int TotalCopies { get; set; }
|
| 39 |
+
public int AvailableCopies { get; set; }
|
| 40 |
+
public DateTime CreatedAt { get; set; }
|
| 41 |
+
public DateTime? UpdatedAt { get; set; }
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
public class BookCreateRequest
|
| 45 |
+
{
|
| 46 |
+
public string Title { get; set; } = string.Empty;
|
| 47 |
+
public string Isbn { get; set; } = string.Empty;
|
| 48 |
+
public string Author { get; set; } = string.Empty;
|
| 49 |
+
public string? Description { get; set; }
|
| 50 |
+
public int TotalCopies { get; set; }
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
public class BookUpdateRequest
|
| 54 |
+
{
|
| 55 |
+
public string Title { get; set; } = string.Empty;
|
| 56 |
+
public string Author { get; set; } = string.Empty;
|
| 57 |
+
public string? Description { get; set; }
|
| 58 |
+
public int TotalCopies { get; set; }
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Borrowing DTOs
|
| 62 |
+
public class BorrowingDto
|
| 63 |
+
{
|
| 64 |
+
public int Id { get; set; }
|
| 65 |
+
public int UserId { get; set; }
|
| 66 |
+
public string UserEmail { get; set; } = string.Empty;
|
| 67 |
+
public int BookId { get; set; }
|
| 68 |
+
public string BookTitle { get; set; } = string.Empty;
|
| 69 |
+
public DateTime BorrowDate { get; set; }
|
| 70 |
+
public DateTime DueDate { get; set; }
|
| 71 |
+
public DateTime? ReturnDate { get; set; } // This is ActualReturnDate in some contexts
|
| 72 |
+
public string Status { get; set; } = string.Empty;
|
| 73 |
+
public decimal FineAmount { get; set; }
|
| 74 |
+
public bool IsFinePaid { get; set; }
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
public class BorrowRequest
|
| 78 |
+
{
|
| 79 |
+
public int BookId { get; set; }
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Category DTOs
|
| 83 |
+
public class CategoryDto
|
| 84 |
+
{
|
| 85 |
+
public int Id { get; set; }
|
| 86 |
+
public string Name { get; set; } = string.Empty;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
public class CategoryCreateRequest
|
| 90 |
+
{
|
| 91 |
+
public string Name { get; set; } = string.Empty;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Subscription & Membership DTOs
|
| 95 |
+
public class MembershipDto
|
| 96 |
+
{
|
| 97 |
+
public int Id { get; set; }
|
| 98 |
+
public string Type { get; set; } = string.Empty;
|
| 99 |
+
public int MaxBooks { get; set; }
|
| 100 |
+
public int BorrowingDays { get; set; }
|
| 101 |
+
public decimal Price { get; set; }
|
| 102 |
+
public int DurationMonths { get; set; }
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
public class SubscriptionDto
|
| 106 |
+
{
|
| 107 |
+
public int Id { get; set; }
|
| 108 |
+
public int UserId { get; set; }
|
| 109 |
+
public int MembershipId { get; set; }
|
| 110 |
+
public string MembershipType { get; set; } = string.Empty;
|
| 111 |
+
public DateTime StartDate { get; set; }
|
| 112 |
+
public DateTime ExpiryDate { get; set; }
|
| 113 |
+
public bool IsActive { get; set; }
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
public class SubscribeRequest
|
| 117 |
+
{
|
| 118 |
+
public int MembershipId { get; set; }
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
public class AdminSubscribeRequest
|
| 122 |
+
{
|
| 123 |
+
public int UserId { get; set; }
|
| 124 |
+
public int MembershipId { get; set; }
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// User DTOs
|
| 128 |
+
public class UserDto
|
| 129 |
+
{
|
| 130 |
+
public int Id { get; set; }
|
| 131 |
+
public string FullName { get; set; } = string.Empty;
|
| 132 |
+
public string Email { get; set; } = string.Empty;
|
| 133 |
+
public string? PhoneNumber { get; set; }
|
| 134 |
+
public string Role { get; set; } = string.Empty;
|
| 135 |
+
public bool IsActive { get; set; }
|
| 136 |
+
public string? StudentId { get; set; }
|
| 137 |
+
public string? Address { get; set; }
|
| 138 |
+
public DateTime CreatedAt { get; set; }
|
| 139 |
+
public DateTime? UpdatedAt { get; set; }
|
| 140 |
+
public bool? BanStatus { get; set; }
|
| 141 |
+
public DateTime? SuspensionEndDate { get; set; }
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
public class UserCreateRequest
|
| 145 |
+
{
|
| 146 |
+
public string FullName { get; set; } = string.Empty;
|
| 147 |
+
public string Email { get; set; } = string.Empty;
|
| 148 |
+
public string Password { get; set; } = string.Empty;
|
| 149 |
+
public string? PhoneNumber { get; set; }
|
| 150 |
+
public string? StudentId { get; set; }
|
| 151 |
+
public string? Address { get; set; }
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
public class UserUpdateRequest
|
| 155 |
+
{
|
| 156 |
+
public string FullName { get; set; } = string.Empty;
|
| 157 |
+
public string? PhoneNumber { get; set; }
|
| 158 |
+
public string? StudentId { get; set; }
|
| 159 |
+
public string? Address { get; set; }
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
public class UserRoleUpdateRequest
|
| 163 |
+
{
|
| 164 |
+
public string Role { get; set; } = string.Empty;
|
| 165 |
+
}
|
| 166 |
+
}
|
Frontend/Models/MemberDetailsViewModel.cs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Frontend.Models.Dtos;
|
| 2 |
+
using System.Collections.Generic;
|
| 3 |
+
|
| 4 |
+
namespace Frontend.Models
|
| 5 |
+
{
|
| 6 |
+
public class MemberDetailsViewModel
|
| 7 |
+
{
|
| 8 |
+
public UserDto User { get; set; } = new();
|
| 9 |
+
public SubscriptionDto? CurrentSubscription { get; set; }
|
| 10 |
+
public List<MembershipDto> AvailableMemberships { get; set; } = new();
|
| 11 |
+
}
|
| 12 |
+
}
|
Frontend/Models/SubscriptionsViewModel.cs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using Frontend.Models.Dtos;
|
| 2 |
+
|
| 3 |
+
namespace Frontend.Models
|
| 4 |
+
{
|
| 5 |
+
public class SubscriptionsViewModel
|
| 6 |
+
{
|
| 7 |
+
public List<MembershipDto> AvailableMemberships { get; set; } = new();
|
| 8 |
+
}
|
| 9 |
+
}
|
Frontend/Program.cs
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
|
|
|
|
|
| 1 |
var builder = WebApplication.CreateBuilder(args);
|
| 2 |
|
| 3 |
// Add services to the container.
|
| 4 |
builder.Services.AddControllersWithViews();
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
var app = builder.Build();
|
| 7 |
|
| 8 |
// Configure the HTTP request pipeline.
|
|
@@ -18,6 +37,7 @@ app.UseStaticFiles();
|
|
| 18 |
|
| 19 |
app.UseRouting();
|
| 20 |
|
|
|
|
| 21 |
app.UseAuthorization();
|
| 22 |
|
| 23 |
app.MapControllerRoute(
|
|
|
|
| 1 |
+
using Microsoft.AspNetCore.Authentication.Cookies;
|
| 2 |
+
|
| 3 |
var builder = WebApplication.CreateBuilder(args);
|
| 4 |
|
| 5 |
// Add services to the container.
|
| 6 |
builder.Services.AddControllersWithViews();
|
| 7 |
|
| 8 |
+
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
| 9 |
+
.AddCookie(options =>
|
| 10 |
+
{
|
| 11 |
+
options.LoginPath = "/Auth/Login";
|
| 12 |
+
options.AccessDeniedPath = "/Auth/AccessDenied";
|
| 13 |
+
options.ExpireTimeSpan = TimeSpan.FromDays(7);
|
| 14 |
+
options.SlidingExpiration = true;
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
builder.Services.AddHttpClient("LibraryBackend", client =>
|
| 18 |
+
{
|
| 19 |
+
client.BaseAddress = new Uri(builder.Configuration["BackendApiBaseUrl"] ?? "https://localhost:7028");
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
builder.Services.AddHttpContextAccessor();
|
| 23 |
+
builder.Services.AddScoped<Frontend.Services.LibraryApiClient>();
|
| 24 |
+
|
| 25 |
var app = builder.Build();
|
| 26 |
|
| 27 |
// Configure the HTTP request pipeline.
|
|
|
|
| 37 |
|
| 38 |
app.UseRouting();
|
| 39 |
|
| 40 |
+
app.UseAuthentication();
|
| 41 |
app.UseAuthorization();
|
| 42 |
|
| 43 |
app.MapControllerRoute(
|
Frontend/Services/LibraryApiClient.cs
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
using System.Net.Http.Json;
|
| 2 |
+
using Frontend.Models.Dtos;
|
| 3 |
+
|
| 4 |
+
namespace Frontend.Services
|
| 5 |
+
{
|
| 6 |
+
public class LibraryApiClient
|
| 7 |
+
{
|
| 8 |
+
private readonly HttpClient _httpClient;
|
| 9 |
+
private readonly IHttpContextAccessor _httpContextAccessor;
|
| 10 |
+
|
| 11 |
+
public LibraryApiClient(IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor)
|
| 12 |
+
{
|
| 13 |
+
_httpClient = httpClientFactory.CreateClient("LibraryBackend");
|
| 14 |
+
_httpContextAccessor = httpContextAccessor;
|
| 15 |
+
|
| 16 |
+
AddAuthHeaderFromCookie();
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
private void AddAuthHeaderFromCookie()
|
| 20 |
+
{
|
| 21 |
+
var token = _httpContextAccessor.HttpContext?.Request.Cookies["AuthToken"];
|
| 22 |
+
if (!string.IsNullOrEmpty(token))
|
| 23 |
+
{
|
| 24 |
+
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
public void SetToken(string token)
|
| 29 |
+
{
|
| 30 |
+
if (!string.IsNullOrEmpty(token))
|
| 31 |
+
{
|
| 32 |
+
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
#region Auth
|
| 37 |
+
public async Task<AuthResponse?> LoginAsync(LoginRequest request)
|
| 38 |
+
{
|
| 39 |
+
var response = await _httpClient.PostAsJsonAsync("api/auth/login", request);
|
| 40 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<AuthResponse>() : null;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
public async Task<AuthResponse?> RegisterAsync(RegisterRequest request)
|
| 44 |
+
{
|
| 45 |
+
var response = await _httpClient.PostAsJsonAsync("api/auth/register", request);
|
| 46 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<AuthResponse>() : null;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
public async Task<object?> GetProfileAsync()
|
| 50 |
+
{
|
| 51 |
+
return await _httpClient.GetFromJsonAsync<object>("api/auth/me");
|
| 52 |
+
}
|
| 53 |
+
#endregion
|
| 54 |
+
|
| 55 |
+
#region Books
|
| 56 |
+
public async Task<IEnumerable<BookDto>> GetBooksAsync()
|
| 57 |
+
{
|
| 58 |
+
return await _httpClient.GetFromJsonAsync<IEnumerable<BookDto>>("api/books") ?? Enumerable.Empty<BookDto>();
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
public async Task<BookDto?> GetBookAsync(int id)
|
| 62 |
+
{
|
| 63 |
+
return await _httpClient.GetFromJsonAsync<BookDto>($"api/books/{id}");
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
public async Task<BookDto?> CreateBookAsync(BookCreateRequest request)
|
| 67 |
+
{
|
| 68 |
+
var response = await _httpClient.PostAsJsonAsync("api/books", request);
|
| 69 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BookDto>() : null;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
public async Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request)
|
| 73 |
+
{
|
| 74 |
+
var response = await _httpClient.PutAsJsonAsync($"api/books/{id}", request);
|
| 75 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BookDto>() : null;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
public async Task<bool> DeleteBookAsync(int id)
|
| 79 |
+
{
|
| 80 |
+
var response = await _httpClient.DeleteAsync($"api/books/{id}");
|
| 81 |
+
return response.IsSuccessStatusCode;
|
| 82 |
+
}
|
| 83 |
+
#endregion
|
| 84 |
+
|
| 85 |
+
#region Borrowings
|
| 86 |
+
public async Task<BorrowingDto?> BorrowBookAsync(BorrowRequest request)
|
| 87 |
+
{
|
| 88 |
+
var response = await _httpClient.PostAsJsonAsync("api/borrowings/borrow", request);
|
| 89 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BorrowingDto>() : null;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
public async Task<BorrowingDto?> ReturnBookAsync(int id)
|
| 93 |
+
{
|
| 94 |
+
var response = await _httpClient.PostAsync($"api/borrowings/return/{id}", null);
|
| 95 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<BorrowingDto>() : null;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
public async Task<IEnumerable<BorrowingDto>> GetMyBorrowingsAsync()
|
| 99 |
+
{
|
| 100 |
+
return await _httpClient.GetFromJsonAsync<IEnumerable<BorrowingDto>>("api/borrowings/me") ?? Enumerable.Empty<BorrowingDto>();
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
public async Task<IEnumerable<BorrowingDto>> GetAllBorrowingsAsync()
|
| 104 |
+
{
|
| 105 |
+
return await _httpClient.GetFromJsonAsync<IEnumerable<BorrowingDto>>("api/borrowings") ?? Enumerable.Empty<BorrowingDto>();
|
| 106 |
+
}
|
| 107 |
+
#endregion
|
| 108 |
+
|
| 109 |
+
#region Categories
|
| 110 |
+
public async Task<IEnumerable<CategoryDto>> GetCategoriesAsync()
|
| 111 |
+
{
|
| 112 |
+
return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories") ?? Enumerable.Empty<CategoryDto>();
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
public async Task<CategoryDto?> CreateCategoryAsync(CategoryCreateRequest request)
|
| 116 |
+
{
|
| 117 |
+
var response = await _httpClient.PostAsJsonAsync("api/categories", request);
|
| 118 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<CategoryDto>() : null;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
public async Task<bool> DeleteCategoryAsync(int id)
|
| 122 |
+
{
|
| 123 |
+
var response = await _httpClient.DeleteAsync($"api/categories/{id}");
|
| 124 |
+
return response.IsSuccessStatusCode;
|
| 125 |
+
}
|
| 126 |
+
#endregion
|
| 127 |
+
|
| 128 |
+
#region Subscriptions
|
| 129 |
+
public async Task<IEnumerable<MembershipDto>> GetMembershipsAsync()
|
| 130 |
+
{
|
| 131 |
+
return await _httpClient.GetFromJsonAsync<IEnumerable<MembershipDto>>("api/memberships") ?? Enumerable.Empty<MembershipDto>();
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
public async Task<SubscriptionDto?> GetMySubscriptionAsync()
|
| 135 |
+
{
|
| 136 |
+
try {
|
| 137 |
+
return await _httpClient.GetFromJsonAsync<SubscriptionDto>("api/subscriptions/me");
|
| 138 |
+
} catch { return null; }
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
public async Task<SubscriptionDto?> SubscribeAsync(SubscribeRequest request)
|
| 142 |
+
{
|
| 143 |
+
var response = await _httpClient.PostAsJsonAsync("api/subscriptions/subscribe", request);
|
| 144 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<SubscriptionDto>() : null;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
public async Task<SubscriptionDto?> GetUserSubscriptionAsync(int userId)
|
| 148 |
+
{
|
| 149 |
+
try {
|
| 150 |
+
return await _httpClient.GetFromJsonAsync<SubscriptionDto>($"api/subscriptions/user/{userId}");
|
| 151 |
+
} catch { return null; }
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
public async Task<SubscriptionDto?> AdminSubscribeAsync(AdminSubscribeRequest request)
|
| 155 |
+
{
|
| 156 |
+
var response = await _httpClient.PostAsJsonAsync("api/subscriptions/admin-subscribe", request);
|
| 157 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<SubscriptionDto>() : null;
|
| 158 |
+
}
|
| 159 |
+
#endregion
|
| 160 |
+
|
| 161 |
+
#region Users
|
| 162 |
+
public async Task<IEnumerable<UserDto>> GetUsersAsync()
|
| 163 |
+
{
|
| 164 |
+
return await _httpClient.GetFromJsonAsync<IEnumerable<UserDto>>("api/users") ?? Enumerable.Empty<UserDto>();
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
public async Task<UserDto?> GetUserAsync(int id)
|
| 168 |
+
{
|
| 169 |
+
return await _httpClient.GetFromJsonAsync<UserDto>($"api/users/{id}");
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
public async Task<UserDto?> CreateUserAsync(UserCreateRequest request)
|
| 173 |
+
{
|
| 174 |
+
var response = await _httpClient.PostAsJsonAsync("api/users", request);
|
| 175 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<UserDto>() : null;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
public async Task<UserDto?> UpdateUserAsync(int id, UserUpdateRequest request)
|
| 179 |
+
{
|
| 180 |
+
var response = await _httpClient.PutAsJsonAsync($"api/users/{id}", request);
|
| 181 |
+
return response.IsSuccessStatusCode ? await response.Content.ReadFromJsonAsync<UserDto>() : null;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
public async Task<bool> UpdateUserRoleAsync(int id, string role)
|
| 185 |
+
{
|
| 186 |
+
var response = await _httpClient.PutAsJsonAsync($"api/users/role/{id}", new UserRoleUpdateRequest { Role = role });
|
| 187 |
+
return response.IsSuccessStatusCode;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
public async Task<bool> DeleteUserAsync(int id)
|
| 191 |
+
{
|
| 192 |
+
var response = await _httpClient.DeleteAsync($"api/users/{id}");
|
| 193 |
+
return response.IsSuccessStatusCode;
|
| 194 |
+
}
|
| 195 |
+
#endregion
|
| 196 |
+
}
|
| 197 |
+
}
|
Frontend/Views/Auth/Login.cshtml
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model Frontend.Models.Dtos.LoginRequest
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = "Sign In";
|
| 4 |
+
Layout = "_Layout";
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
<div class="min-h-[calc(100vh-200px)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
| 8 |
+
<div class="max-w-md w-full space-y-8 bg-white p-10 rounded-2xl border border-slate-200 shadow-xl">
|
| 9 |
+
<div>
|
| 10 |
+
<div class="mx-auto h-12 w-12 bg-slate-900 rounded-xl flex items-center justify-center text-white mb-4">
|
| 11 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
|
| 12 |
+
</div>
|
| 13 |
+
<h2 class="text-center text-3xl font-extrabold text-slate-900 tracking-tight">Welcome back</h2>
|
| 14 |
+
<p class="mt-2 text-center text-sm text-slate-500">
|
| 15 |
+
Continue where you left off.
|
| 16 |
+
</p>
|
| 17 |
+
</div>
|
| 18 |
+
<form class="mt-8 space-y-6" asp-action="Login" method="POST">
|
| 19 |
+
<div asp-validation-summary="ModelOnly" class="text-red-500 text-sm mb-4 bg-red-50 p-3 rounded-lg border border-red-100"></div>
|
| 20 |
+
|
| 21 |
+
<div class="space-y-4">
|
| 22 |
+
<div>
|
| 23 |
+
<label asp-for="Email" class="block text-sm font-medium text-slate-700 mb-1">Email Address</label>
|
| 24 |
+
<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">
|
| 25 |
+
<span asp-validation-for="Email" class="text-xs text-red-500 mt-1"></span>
|
| 26 |
+
</div>
|
| 27 |
+
<div>
|
| 28 |
+
<div class="flex items-center justify-between mb-1">
|
| 29 |
+
<label asp-for="Password" class="block text-sm font-medium text-slate-700">Password</label>
|
| 30 |
+
<a href="#" class="text-xs font-medium text-slate-900 hover:underline">Forgot password?</a>
|
| 31 |
+
</div>
|
| 32 |
+
<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="••••••••">
|
| 33 |
+
<span asp-validation-for="Password" class="text-xs text-red-500 mt-1"></span>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<div>
|
| 38 |
+
<button type="submit" class="group relative w-full flex justify-center py-2.5 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-slate-900 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900 transition-all">
|
| 39 |
+
Sign In
|
| 40 |
+
</button>
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
<div class="text-center">
|
| 44 |
+
<span class="text-sm text-slate-500">Don't have an account?</span>
|
| 45 |
+
<a asp-action="Register" class="text-sm font-medium text-slate-900 hover:underline ml-1">Create one for free</a>
|
| 46 |
+
</div>
|
| 47 |
+
</form>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
Frontend/Views/Auth/Register.cshtml
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model Frontend.Models.Dtos.RegisterRequest
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = "Create Account";
|
| 4 |
+
Layout = "_Layout";
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
<div class="min-h-[calc(100vh-200px)] flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
| 8 |
+
<div class="max-w-md w-full space-y-8 bg-white p-10 rounded-2xl border border-slate-200 shadow-xl">
|
| 9 |
+
<div>
|
| 10 |
+
<div class="mx-auto h-12 w-12 bg-slate-900 rounded-xl flex items-center justify-center text-white mb-4">
|
| 11 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
|
| 12 |
+
</div>
|
| 13 |
+
<h2 class="text-center text-3xl font-extrabold text-slate-900 tracking-tight">Join our Library</h2>
|
| 14 |
+
<p class="mt-2 text-center text-sm text-slate-500">
|
| 15 |
+
Start your reading journey today.
|
| 16 |
+
</p>
|
| 17 |
+
</div>
|
| 18 |
+
<form class="mt-8 space-y-6" asp-action="Register" method="POST">
|
| 19 |
+
<div asp-validation-summary="ModelOnly" class="text-red-500 text-sm mb-4 bg-red-50 p-3 rounded-lg border border-red-100"></div>
|
| 20 |
+
|
| 21 |
+
<div class="space-y-4">
|
| 22 |
+
<div>
|
| 23 |
+
<label asp-for="FullName" class="block text-sm font-medium text-slate-700 mb-1">Full Name</label>
|
| 24 |
+
<input asp-for="FullName" type="text" 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="John Doe">
|
| 25 |
+
<span asp-validation-for="FullName" class="text-xs text-red-500 mt-1"></span>
|
| 26 |
+
</div>
|
| 27 |
+
<div>
|
| 28 |
+
<label asp-for="Email" class="block text-sm font-medium text-slate-700 mb-1">Email Address</label>
|
| 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="••••••••">
|
| 35 |
+
<span asp-validation-for="Password" class="text-xs text-red-500 mt-1"></span>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
<div>
|
| 40 |
+
<button type="submit" class="group relative w-full flex justify-center py-2.5 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-slate-900 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900 transition-all">
|
| 41 |
+
Create Account
|
| 42 |
+
</button>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<div class="text-center">
|
| 46 |
+
<span class="text-sm text-slate-500">Already have an account?</span>
|
| 47 |
+
<a asp-action="Login" class="text-sm font-medium text-slate-900 hover:underline ml-1">Sign in</a>
|
| 48 |
+
</div>
|
| 49 |
+
</form>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
Frontend/Views/Books/Create.cshtml
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model Frontend.Models.Dtos.BookCreateRequest
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = "Add New Book";
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
<div class="mb-8">
|
| 7 |
+
<a asp-action="Index" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-900 mb-6 group">
|
| 8 |
+
<svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
| 9 |
+
Back to Catalog
|
| 10 |
+
</a>
|
| 11 |
+
|
| 12 |
+
<div class="max-w-3xl mx-auto">
|
| 13 |
+
<div class="mb-8">
|
| 14 |
+
<h1 class="text-3xl font-bold tracking-tight text-slate-900">Add New Book</h1>
|
| 15 |
+
<p class="mt-2 text-slate-500">Fill in the details below to add a new book to the library catalog.</p>
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
<div class="card p-6 sm:p-8">
|
| 19 |
+
<form asp-action="Create" method="post" class="space-y-6">
|
| 20 |
+
<!-- Validation Summary -->
|
| 21 |
+
<div asp-validation-summary="ModelOnly" class="text-sm text-red-600 bg-red-50 p-3 rounded-md"></div>
|
| 22 |
+
|
| 23 |
+
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
| 24 |
+
<!-- Title -->
|
| 25 |
+
<div class="sm:col-span-2">
|
| 26 |
+
<label asp-for="Title" class="block text-sm font-medium text-slate-700">Book Title <span class="text-rose-500">*</span></label>
|
| 27 |
+
<div class="mt-1">
|
| 28 |
+
<input asp-for="Title" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="e.g. The Great Gatsby" required />
|
| 29 |
+
<span asp-validation-for="Title" class="mt-1 text-xs text-rose-500"></span>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<!-- Author -->
|
| 34 |
+
<div>
|
| 35 |
+
<label asp-for="Author" class="block text-sm font-medium text-slate-700">Author <span class="text-rose-500">*</span></label>
|
| 36 |
+
<div class="mt-1">
|
| 37 |
+
<input asp-for="Author" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="e.g. F. Scott Fitzgerald" required />
|
| 38 |
+
<span asp-validation-for="Author" class="mt-1 text-xs text-rose-500"></span>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<!-- ISBN -->
|
| 43 |
+
<div>
|
| 44 |
+
<label asp-for="Isbn" class="block text-sm font-medium text-slate-700">ISBN <span class="text-rose-500">*</span></label>
|
| 45 |
+
<div class="mt-1">
|
| 46 |
+
<input asp-for="Isbn" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="e.g. 978-0743273565" required />
|
| 47 |
+
<span asp-validation-for="Isbn" class="mt-1 text-xs text-rose-500"></span>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<!-- Total Copies -->
|
| 52 |
+
<div>
|
| 53 |
+
<label asp-for="TotalCopies" class="block text-sm font-medium text-slate-700">Total Copies <span class="text-rose-500">*</span></label>
|
| 54 |
+
<div class="mt-1">
|
| 55 |
+
<input asp-for="TotalCopies" type="number" min="1" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="1" required />
|
| 56 |
+
<span asp-validation-for="TotalCopies" class="mt-1 text-xs text-rose-500"></span>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<div class="hidden sm:block"></div> <!-- Empty spacer -->
|
| 61 |
+
|
| 62 |
+
<!-- Description -->
|
| 63 |
+
<div class="sm:col-span-2">
|
| 64 |
+
<label asp-for="Description" class="block text-sm font-medium text-slate-700">Description</label>
|
| 65 |
+
<div class="mt-1">
|
| 66 |
+
<textarea asp-for="Description" rows="4" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="Brief summary of the book..."></textarea>
|
| 67 |
+
<span asp-validation-for="Description" class="mt-1 text-xs text-rose-500"></span>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
|
| 73 |
+
<a asp-action="Index" class="btn-secondary">Cancel</a>
|
| 74 |
+
<button type="submit" class="btn-primary">Add Book</button>
|
| 75 |
+
</div>
|
| 76 |
+
</form>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
@section Scripts {
|
| 82 |
+
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
| 83 |
+
}
|
Frontend/Views/Books/Details.cshtml
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model Frontend.Models.Dtos.BookDto
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = Model.Title;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
<div class="mb-8">
|
| 7 |
+
<a asp-action="Index" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-900 mb-6 group">
|
| 8 |
+
<svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
| 9 |
+
Back to Catalog
|
| 10 |
+
</a>
|
| 11 |
+
|
| 12 |
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
| 13 |
+
<!-- Book Cover Image -->
|
| 14 |
+
<div class="lg:col-span-1">
|
| 15 |
+
<div class="aspect-[2/3] w-full rounded-2xl bg-slate-100 border border-slate-200 shadow-sm flex items-center justify-center text-slate-300">
|
| 16 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<!-- Book Info -->
|
| 21 |
+
<div class="lg:col-span-2 space-y-8">
|
| 22 |
+
<div>
|
| 23 |
+
<span class="inline-flex items-center rounded-full bg-slate-100 px-3 py-1 text-xs font-semibold text-slate-600 mb-4 tracking-wide uppercase">@Model.Status</span>
|
| 24 |
+
<h1 class="text-4xl font-extrabold text-slate-900 tracking-tight">@Model.Title</h1>
|
| 25 |
+
<p class="text-xl text-slate-500 mt-2 font-medium">by @Model.Author</p>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div class="prose prose-slate max-w-none">
|
| 29 |
+
<h3 class="text-lg font-bold text-slate-900 border-b border-slate-100 pb-2 mb-4 uppercase tracking-wider text-sm">Description</h3>
|
| 30 |
+
<p class="text-slate-600 leading-relaxed">
|
| 31 |
+
@(string.IsNullOrEmpty(Model.Description) ? "No description available for this book." : Model.Description)
|
| 32 |
+
</p>
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
<div class="grid grid-cols-2 gap-8 border-y border-slate-100 py-6">
|
| 36 |
+
<div>
|
| 37 |
+
<h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">ISBN</h4>
|
| 38 |
+
<p class="text-sm font-semibold text-slate-900">@Model.Isbn</p>
|
| 39 |
+
</div>
|
| 40 |
+
<div>
|
| 41 |
+
<h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Availability</h4>
|
| 42 |
+
<p class="text-sm font-semibold @(Model.AvailableCopies > 0 ? "text-emerald-600" : "text-rose-600")">
|
| 43 |
+
@(Model.AvailableCopies > 0 ? $"{Model.AvailableCopies} / {Model.TotalCopies} units in stock" : "Currently occupied")
|
| 44 |
+
</p>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
<div class="flex items-center gap-4 pt-4">
|
| 49 |
+
@if (Model.AvailableCopies > 0)
|
| 50 |
+
{
|
| 51 |
+
<form asp-controller="Borrowings" asp-action="Borrow" method="POST">
|
| 52 |
+
<input type="hidden" name="BookId" value="@Model.Id" />
|
| 53 |
+
<button type="submit" class="btn-primary py-3 px-8 text-base shadow-lg shadow-slate-200">Borrow This Book</button>
|
| 54 |
+
</form>
|
| 55 |
+
}
|
| 56 |
+
else
|
| 57 |
+
{
|
| 58 |
+
<button disabled class="btn-primary py-3 px-8 text-base opacity-50 cursor-not-allowed">Out of Stock</button>
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn-secondary py-3 px-8 text-base">Edit Details</a>
|
| 62 |
+
|
| 63 |
+
<form asp-action="Delete" asp-route-id="@Model.Id" method="POST" onsubmit="return confirm('Are you sure you want to remove this book from the catalog?');">
|
| 64 |
+
<button type="submit" class="p-3 text-rose-600 hover:text-rose-700 hover:bg-rose-50 rounded-xl transition-colors">
|
| 65 |
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
| 66 |
+
</button>
|
| 67 |
+
</form>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
Frontend/Views/Books/Edit.cshtml
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model Frontend.Models.Dtos.BookUpdateRequest
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = "Edit Book";
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
<div class="mb-8">
|
| 7 |
+
<a asp-action="Index" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-900 mb-6 group">
|
| 8 |
+
<svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
| 9 |
+
Back to Catalog
|
| 10 |
+
</a>
|
| 11 |
+
|
| 12 |
+
<div class="max-w-3xl mx-auto">
|
| 13 |
+
<div class="mb-8">
|
| 14 |
+
<h1 class="text-3xl font-bold tracking-tight text-slate-900">Edit Book</h1>
|
| 15 |
+
<p class="mt-2 text-slate-500">Update the details for this book.</p>
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
<div class="card p-6 sm:p-8">
|
| 19 |
+
<form asp-action="Edit" method="post" class="space-y-6">
|
| 20 |
+
<!-- Validation Summary -->
|
| 21 |
+
<div asp-validation-summary="ModelOnly" class="text-sm text-red-600 bg-red-50 p-3 rounded-md"></div>
|
| 22 |
+
|
| 23 |
+
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
| 24 |
+
<!-- Title -->
|
| 25 |
+
<div class="sm:col-span-2">
|
| 26 |
+
<label asp-for="Title" class="block text-sm font-medium text-slate-700">Book Title <span class="text-rose-500">*</span></label>
|
| 27 |
+
<div class="mt-1">
|
| 28 |
+
<input asp-for="Title" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" required />
|
| 29 |
+
<span asp-validation-for="Title" class="mt-1 text-xs text-rose-500"></span>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<!-- Author -->
|
| 34 |
+
<div class="sm:col-span-2">
|
| 35 |
+
<label asp-for="Author" class="block text-sm font-medium text-slate-700">Author <span class="text-rose-500">*</span></label>
|
| 36 |
+
<div class="mt-1">
|
| 37 |
+
<input asp-for="Author" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" required />
|
| 38 |
+
<span asp-validation-for="Author" class="mt-1 text-xs text-rose-500"></span>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<!-- Total Copies -->
|
| 43 |
+
<div>
|
| 44 |
+
<label asp-for="TotalCopies" class="block text-sm font-medium text-slate-700">Total Copies <span class="text-rose-500">*</span></label>
|
| 45 |
+
<div class="mt-1">
|
| 46 |
+
<input asp-for="TotalCopies" type="number" min="1" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" required />
|
| 47 |
+
<span asp-validation-for="TotalCopies" class="mt-1 text-xs text-rose-500"></span>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div class="hidden sm:block"></div> <!-- Empty spacer -->
|
| 52 |
+
|
| 53 |
+
<!-- Description -->
|
| 54 |
+
<div class="sm:col-span-2">
|
| 55 |
+
<label asp-for="Description" class="block text-sm font-medium text-slate-700">Description</label>
|
| 56 |
+
<div class="mt-1">
|
| 57 |
+
<textarea asp-for="Description" rows="4" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white"></textarea>
|
| 58 |
+
<span asp-validation-for="Description" class="mt-1 text-xs text-rose-500"></span>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
|
| 64 |
+
<a asp-action="Index" class="btn-secondary">Cancel</a>
|
| 65 |
+
<button type="submit" class="btn-primary">Save Changes</button>
|
| 66 |
+
</div>
|
| 67 |
+
</form>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
@section Scripts {
|
| 73 |
+
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
| 74 |
+
}
|
Frontend/Views/Books/Index.cshtml
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model IEnumerable<Frontend.Models.Dtos.BookDto>
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = "Books Catalog";
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
| 7 |
+
<div>
|
| 8 |
+
<h1 class="text-3xl font-bold tracking-tight text-slate-900">Books Catalog</h1>
|
| 9 |
+
<p class="mt-2 text-slate-500">Explore and discover your next great read.</p>
|
| 10 |
+
</div>
|
| 11 |
+
<div class="flex items-center gap-3">
|
| 12 |
+
<a asp-action="Create" class="btn-primary">
|
| 13 |
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
| 14 |
+
Add New Book
|
| 15 |
+
</a>
|
| 16 |
+
</div>
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-10">
|
| 20 |
+
<div class="md:col-span-3">
|
| 21 |
+
<div class="relative">
|
| 22 |
+
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 23 |
+
<svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
| 24 |
+
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
|
| 25 |
+
</svg>
|
| 26 |
+
</div>
|
| 27 |
+
<input type="text" class="block w-full pl-10 pr-3 py-2 border border-slate-200 rounded-lg bg-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900 focus:border-transparent sm:text-sm shadow-sm" placeholder="Search by title, author or ISBN...">
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<!-- Book Grid -->
|
| 33 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-8">
|
| 34 |
+
@foreach (var book in Model)
|
| 35 |
+
{
|
| 36 |
+
<div class="group card hover:shadow-lg transition-all duration-300">
|
| 37 |
+
<div class="relative aspect-[2/3] w-full bg-slate-100 flex items-center justify-center text-slate-300 overflow-hidden text-center p-4">
|
| 38 |
+
<div class="flex flex-col items-center gap-4">
|
| 39 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
|
| 40 |
+
<span class="text-[10px] font-bold text-slate-400">@book.Isbn</span>
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
@if (book.AvailableCopies <= 0)
|
| 44 |
+
{
|
| 45 |
+
<div class="absolute inset-0 bg-slate-900/60 backdrop-blur-[2px] flex items-center justify-center">
|
| 46 |
+
<span class="bg-white text-slate-900 text-[10px] font-bold uppercase tracking-wider px-2 py-1 rounded">Out of Stock</span>
|
| 47 |
+
</div>
|
| 48 |
+
}
|
| 49 |
+
</div>
|
| 50 |
+
<div class="p-4">
|
| 51 |
+
<div class="flex items-start justify-between gap-2">
|
| 52 |
+
<div class="min-w-0">
|
| 53 |
+
<h3 class="text-sm font-bold text-slate-900 truncate group-hover:text-amber-600 transition-colors">
|
| 54 |
+
<a asp-action="Details" asp-route-id="@book.Id">@book.Title</a>
|
| 55 |
+
</h3>
|
| 56 |
+
<p class="text-xs text-slate-500 mt-0.5 truncate">@book.Author</p>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
<div class="mt-4 flex items-center justify-between">
|
| 60 |
+
<span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-600">@book.Status</span>
|
| 61 |
+
<span class="text-[10px] font-medium @(book.AvailableCopies > 0 ? "text-emerald-600" : "text-rose-600")">
|
| 62 |
+
@book.AvailableCopies / @book.TotalCopies
|
| 63 |
+
</span>
|
| 64 |
+
</div>
|
| 65 |
+
<div class="mt-4 pt-4 border-t border-slate-50 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
| 66 |
+
<a asp-action="Details" asp-route-id="@book.Id" class="flex-1 btn-secondary py-1 text-[11px]">Details</a>
|
| 67 |
+
<a asp-action="Edit" asp-route-id="@book.Id" class="p-1.5 rounded-md border border-slate-200 hover:bg-slate-50 text-slate-600">
|
| 68 |
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
|
| 69 |
+
</a>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
}
|
| 74 |
+
</div>
|
Frontend/Views/Borrowings/Index.cshtml
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model IEnumerable<Frontend.Models.Dtos.BorrowingDto>
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = "My Loans";
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
<div class="mb-8">
|
| 7 |
+
<h1 class="text-3xl font-bold tracking-tight text-slate-900">@(User.IsInRole("Admin") ? "All Active Loans" : "My Loans")</h1>
|
| 8 |
+
<p class="mt-2 text-slate-500">Track your current borrowings and return upcoming books.</p>
|
| 9 |
+
</div>
|
| 10 |
+
|
| 11 |
+
<div class="card overflow-hidden">
|
| 12 |
+
<div class="overflow-x-auto">
|
| 13 |
+
<table class="w-full text-left border-collapse">
|
| 14 |
+
<thead>
|
| 15 |
+
<tr class="bg-slate-50/50 border-b border-slate-100">
|
| 16 |
+
<th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Book Information</th>
|
| 17 |
+
@if (User.IsInRole("Admin"))
|
| 18 |
+
{
|
| 19 |
+
<th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Member</th>
|
| 20 |
+
}
|
| 21 |
+
<th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Dates</th>
|
| 22 |
+
<th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Status</th>
|
| 23 |
+
<th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest">Fines</th>
|
| 24 |
+
<th class="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-widest text-right">Actions</th>
|
| 25 |
+
</tr>
|
| 26 |
+
</thead>
|
| 27 |
+
<tbody class="divide-y divide-slate-100">
|
| 28 |
+
@if (!Model.Any())
|
| 29 |
+
{
|
| 30 |
+
<tr>
|
| 31 |
+
<td colspan="@(User.IsInRole("Admin") ? 6 : 5)" class="px-6 py-12 text-center text-slate-400 italic">
|
| 32 |
+
You don't have any borrowing history yet.
|
| 33 |
+
</td>
|
| 34 |
+
</tr>
|
| 35 |
+
}
|
| 36 |
+
@foreach (var loan in Model)
|
| 37 |
+
{
|
| 38 |
+
<tr class="hover:bg-slate-50 transition-colors group">
|
| 39 |
+
<td class="px-6 py-4">
|
| 40 |
+
<div class="flex items-center gap-3">
|
| 41 |
+
<div class="w-10 h-14 bg-slate-100 rounded flex items-center justify-center text-slate-300">
|
| 42 |
+
<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>
|
| 43 |
+
</div>
|
| 44 |
+
<div class="min-w-0">
|
| 45 |
+
<p class="text-sm font-bold text-slate-900 truncate">@loan.BookTitle</p>
|
| 46 |
+
<p class="text-[11px] text-slate-500 mt-0.5">Loan ID: #@loan.Id</p>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
</td>
|
| 50 |
+
@if (User.IsInRole("Admin"))
|
| 51 |
+
{
|
| 52 |
+
<td class="px-6 py-4">
|
| 53 |
+
<div class="text-sm font-medium text-slate-900 truncate">@loan.UserEmail</div>
|
| 54 |
+
<div class="text-[11px] text-slate-500 mt-0.5">User ID: #@loan.UserId</div>
|
| 55 |
+
</td>
|
| 56 |
+
}
|
| 57 |
+
<td class="px-6 py-4">
|
| 58 |
+
<div class="space-y-1">
|
| 59 |
+
<p class="text-[11px]"><span class="text-slate-400">Borrowed:</span> <span class="font-medium text-slate-700">@loan.BorrowDate.ToString("MMM dd, yyyy")</span></p>
|
| 60 |
+
<p class="text-[11px]"><span class="text-slate-400">Due:</span> <span class="font-medium @(loan.DueDate < DateTime.Now && loan.Status == "Borrowed" ? "text-rose-600" : "text-slate-700")">@loan.DueDate.ToString("MMM dd, yyyy")</span></p>
|
| 61 |
+
@if (loan.ReturnDate.HasValue)
|
| 62 |
+
{
|
| 63 |
+
<p class="text-[11px]"><span class="text-slate-400">Returned:</span> <span class="font-medium text-emerald-600">@loan.ReturnDate.Value.ToString("MMM dd, yyyy")</span></p>
|
| 64 |
+
}
|
| 65 |
+
</div>
|
| 66 |
+
</td>
|
| 67 |
+
<td class="px-6 py-4">
|
| 68 |
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
| 69 |
+
@(loan.Status == "Borrowed" ? "bg-amber-100 text-amber-700" : "bg-emerald-100 text-emerald-700")">
|
| 70 |
+
@loan.Status
|
| 71 |
+
</span>
|
| 72 |
+
</td>
|
| 73 |
+
<td class="px-6 py-4">
|
| 74 |
+
@if (loan.FineAmount > 0)
|
| 75 |
+
{
|
| 76 |
+
<div class="flex flex-col">
|
| 77 |
+
<span class="text-xs font-bold @(loan.IsFinePaid ? "text-slate-500 line-through" : "text-rose-600")">
|
| 78 |
+
MMK @loan.FineAmount.ToString("N0")
|
| 79 |
+
</span>
|
| 80 |
+
@if (loan.IsFinePaid)
|
| 81 |
+
{
|
| 82 |
+
<span class="text-[10px] text-emerald-600 font-medium">Paid</span>
|
| 83 |
+
}
|
| 84 |
+
</div>
|
| 85 |
+
}
|
| 86 |
+
else
|
| 87 |
+
{
|
| 88 |
+
<span class="text-[11px] text-slate-400">No Fines</span>
|
| 89 |
+
}
|
| 90 |
+
</td>
|
| 91 |
+
<td class="px-6 py-4 text-right">
|
| 92 |
+
@if (loan.Status == "Borrowed")
|
| 93 |
+
{
|
| 94 |
+
<form asp-action="Return" method="POST" onsubmit="return confirm('Ready to return this book?');">
|
| 95 |
+
<input type="hidden" name="id" value="@loan.Id" />
|
| 96 |
+
<button type="submit" class="btn-secondary py-1.5 px-4 text-xs font-bold">@(User.IsInRole("Admin") ? "Process Return" : "Return Book")</button>
|
| 97 |
+
</form>
|
| 98 |
+
}
|
| 99 |
+
</td>
|
| 100 |
+
</tr>
|
| 101 |
+
}
|
| 102 |
+
</tbody>
|
| 103 |
+
</table>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
Frontend/Views/Home/Index.cshtml
CHANGED
|
@@ -1,8 +1,123 @@
|
|
| 1 |
-
|
| 2 |
-
ViewData["Title"] = "
|
| 3 |
}
|
| 4 |
|
| 5 |
-
<div class="
|
| 6 |
-
<h1 class="
|
| 7 |
-
<p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
</div>
|
|
|
|
| 1 |
+
@{
|
| 2 |
+
ViewData["Title"] = "Dashboard";
|
| 3 |
}
|
| 4 |
|
| 5 |
+
<div class="mb-10">
|
| 6 |
+
<h1 class="text-3xl font-extrabold tracking-tight text-slate-900 border-b border-slate-100 pb-4 mb-2">Welcome Back</h1>
|
| 7 |
+
<p class="text-slate-500 font-medium">Here's what's happening with the library today.</p>
|
| 8 |
+
</div>
|
| 9 |
+
|
| 10 |
+
<!-- Stats Grid -->
|
| 11 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
| 12 |
+
<div class="card p-6 flex flex-col gap-3 group hover:border-slate-300">
|
| 13 |
+
<div class="flex items-center justify-between">
|
| 14 |
+
<div class="p-2 bg-slate-50 rounded-lg group-hover:bg-slate-100 transition-colors">
|
| 15 |
+
<svg class="w-5 h-5 text-slate-600" 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>
|
| 16 |
+
</div>
|
| 17 |
+
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Books</span>
|
| 18 |
+
</div>
|
| 19 |
+
<div>
|
| 20 |
+
<div class="text-3xl font-bold text-slate-900">@ViewBag.TotalBooks</div>
|
| 21 |
+
<div class="text-xs text-slate-500 mt-1 font-medium italic">Total in catalog</div>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<div class="card p-6 flex flex-col gap-3 group hover:border-slate-300">
|
| 26 |
+
<div class="flex items-center justify-between">
|
| 27 |
+
<div class="p-2 bg-emerald-50 rounded-lg group-hover:bg-emerald-100 transition-colors">
|
| 28 |
+
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4"></path></svg>
|
| 29 |
+
</div>
|
| 30 |
+
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Active Loans</span>
|
| 31 |
+
</div>
|
| 32 |
+
<div>
|
| 33 |
+
<div class="text-3xl font-bold text-slate-900">@ViewBag.ActiveLoans</div>
|
| 34 |
+
<div class="text-xs text-slate-500 mt-1 font-medium italic">Checking out right now</div>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<div class="card p-6 flex flex-col gap-3 group hover:border-slate-300 border-l-4 border-l-rose-500">
|
| 39 |
+
<div class="flex items-center justify-between">
|
| 40 |
+
<div class="p-2 bg-rose-50 rounded-lg group-hover:bg-rose-100 transition-colors">
|
| 41 |
+
<svg class="w-5 h-5 text-rose-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
| 42 |
+
</div>
|
| 43 |
+
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest text-rose-500">Overdue</span>
|
| 44 |
+
</div>
|
| 45 |
+
<div>
|
| 46 |
+
<div class="text-3xl font-bold text-rose-600">@ViewBag.OverdueLoans</div>
|
| 47 |
+
<div class="text-xs text-rose-500 mt-1 font-medium italic">Require attention</div>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div class="card p-6 flex flex-col gap-3 group hover:border-slate-300">
|
| 52 |
+
<div class="flex items-center justify-between">
|
| 53 |
+
<div class="p-2 bg-blue-50 rounded-lg group-hover:bg-blue-100 transition-colors">
|
| 54 |
+
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
|
| 55 |
+
</div>
|
| 56 |
+
<span class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Active Members</span>
|
| 57 |
+
</div>
|
| 58 |
+
<div>
|
| 59 |
+
<div class="text-3xl font-bold text-slate-900">@ViewBag.TotalMembers</div>
|
| 60 |
+
<div class="text-xs text-slate-500 mt-1 font-medium italic">In the community</div>
|
| 61 |
+
</div>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<!-- Recent Arrivals -->
|
| 66 |
+
<div class="mb-12">
|
| 67 |
+
<div class="flex items-center justify-between mb-8">
|
| 68 |
+
<h2 class="text-2xl font-bold text-slate-900 tracking-tight">Recent Arrivals</h2>
|
| 69 |
+
<a asp-controller="Books" asp-action="Index" class="text-sm font-bold text-slate-500 hover:text-slate-900 flex items-center group">
|
| 70 |
+
Browse All
|
| 71 |
+
<svg class="w-4 h-4 ml-1 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"></path></svg>
|
| 72 |
+
</a>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
| 76 |
+
@foreach (var book in (IEnumerable<Frontend.Models.Dtos.BookDto>)ViewBag.RecentBooks)
|
| 77 |
+
{
|
| 78 |
+
<div class="group cursor-pointer">
|
| 79 |
+
<div class="aspect-[2/3] w-full bg-slate-100 rounded-xl mb-3 flex items-center justify-center text-slate-300 group-hover:shadow-lg transition-all duration-300 relative overflow-hidden">
|
| 80 |
+
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
|
| 81 |
+
@if (book.AvailableCopies <= 0)
|
| 82 |
+
{
|
| 83 |
+
<div class="absolute inset-0 bg-slate-900/40 backdrop-blur-[1px] flex items-center justify-center">
|
| 84 |
+
<span class="bg-white text-slate-900 text-[8px] font-bold uppercase px-2 py-1 rounded">Out</span>
|
| 85 |
+
</div>
|
| 86 |
+
}
|
| 87 |
+
</div>
|
| 88 |
+
<h3 class="text-xs font-bold text-slate-900 truncate group-hover:text-amber-600 transition-colors">
|
| 89 |
+
<a asp-controller="Books" asp-action="Details" asp-route-id="@book.Id">@book.Title</a>
|
| 90 |
+
</h3>
|
| 91 |
+
<p class="text-[10px] text-slate-500 truncate mt-0.5 font-medium">@book.Author</p>
|
| 92 |
+
</div>
|
| 93 |
+
}
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<!-- Quick Actions -->
|
| 98 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
| 99 |
+
<div class="bg-slate-900 text-white rounded-3xl p-8 flex flex-col justify-between overflow-hidden relative group">
|
| 100 |
+
<div class="relative z-10">
|
| 101 |
+
<h2 class="text-2xl font-bold mb-2">Explore the Library</h2>
|
| 102 |
+
<p class="text-slate-400 text-sm max-w-[280px]">Access thousands of books across all categories with a premium member account.</p>
|
| 103 |
+
</div>
|
| 104 |
+
<div class="mt-8 relative z-10">
|
| 105 |
+
<a asp-controller="Books" asp-action="Index" class="inline-flex items-center bg-white text-slate-900 px-6 py-2.5 rounded-full text-sm font-bold hover:bg-slate-100 transition-colors">
|
| 106 |
+
View Catalog
|
| 107 |
+
</a>
|
| 108 |
+
</div>
|
| 109 |
+
<svg class="absolute bottom-0 right-0 w-48 h-48 text-slate-800 -mb-12 -mr-12 opacity-50 transition-transform group-hover:scale-110" fill="currentColor" viewBox="0 0 24 24"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
<div class="bg-amber-50 rounded-3xl p-8 flex flex-col justify-between group border border-amber-100">
|
| 113 |
+
<div>
|
| 114 |
+
<h2 class="text-2xl font-bold text-slate-900 mb-2">My Current Loans</h2>
|
| 115 |
+
<p class="text-slate-600 text-sm max-w-[280px]">Don't forget to return your books on time to avoid late fees.</p>
|
| 116 |
+
</div>
|
| 117 |
+
<div class="mt-8">
|
| 118 |
+
<a asp-controller="Borrowings" asp-action="Index" class="inline-flex items-center text-slate-900 px-6 py-2.5 rounded-full text-sm font-bold border-2 border-slate-900 hover:bg-slate-900 hover:text-white transition-all">
|
| 119 |
+
Manage My Loans
|
| 120 |
+
</a>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
</div>
|
Frontend/Views/Members/Create.cshtml
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model Frontend.Models.Dtos.UserCreateRequest
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = "Add New Member";
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
<div class="mb-8">
|
| 7 |
+
<a asp-action="Index" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-900 mb-6 group">
|
| 8 |
+
<svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
| 9 |
+
Back to Directory
|
| 10 |
+
</a>
|
| 11 |
+
|
| 12 |
+
<div class="max-w-3xl mx-auto">
|
| 13 |
+
<div class="mb-8">
|
| 14 |
+
<h1 class="text-3xl font-bold tracking-tight text-slate-900">Add New Member</h1>
|
| 15 |
+
<p class="mt-2 text-slate-500">Register a new member to the library system.</p>
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
<div class="card p-6 sm:p-8">
|
| 19 |
+
<form asp-action="Create" method="post" class="space-y-6">
|
| 20 |
+
<!-- Validation Summary -->
|
| 21 |
+
<div asp-validation-summary="ModelOnly" class="text-sm text-red-600 bg-red-50 p-3 rounded-md"></div>
|
| 22 |
+
|
| 23 |
+
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
| 24 |
+
<!-- FullName -->
|
| 25 |
+
<div class="sm:col-span-2">
|
| 26 |
+
<label asp-for="FullName" class="block text-sm font-medium text-slate-700">Full Name <span class="text-rose-500">*</span></label>
|
| 27 |
+
<div class="mt-1">
|
| 28 |
+
<input asp-for="FullName" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="John Doe" required />
|
| 29 |
+
<span asp-validation-for="FullName" class="mt-1 text-xs text-rose-500"></span>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<!-- Email -->
|
| 34 |
+
<div>
|
| 35 |
+
<label asp-for="Email" class="block text-sm font-medium text-slate-700">Email Address <span class="text-rose-500">*</span></label>
|
| 36 |
+
<div class="mt-1">
|
| 37 |
+
<input asp-for="Email" type="email" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="you@domain.com" required />
|
| 38 |
+
<span asp-validation-for="Email" class="mt-1 text-xs text-rose-500"></span>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<!-- Password -->
|
| 43 |
+
<div>
|
| 44 |
+
<label asp-for="Password" class="block text-sm font-medium text-slate-700">Password <span class="text-rose-500">*</span></label>
|
| 45 |
+
<div class="mt-1">
|
| 46 |
+
<input asp-for="Password" type="password" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" required />
|
| 47 |
+
<span asp-validation-for="Password" class="mt-1 text-xs text-rose-500"></span>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<!-- Phone Number -->
|
| 52 |
+
<div>
|
| 53 |
+
<label asp-for="PhoneNumber" class="block text-sm font-medium text-slate-700">Phone Number</label>
|
| 54 |
+
<div class="mt-1">
|
| 55 |
+
<input asp-for="PhoneNumber" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="+123456789" />
|
| 56 |
+
<span asp-validation-for="PhoneNumber" class="mt-1 text-xs text-rose-500"></span>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<!-- Student ID -->
|
| 61 |
+
<div>
|
| 62 |
+
<label asp-for="StudentId" class="block text-sm font-medium text-slate-700">Student ID</label>
|
| 63 |
+
<div class="mt-1">
|
| 64 |
+
<input asp-for="StudentId" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="Optional" />
|
| 65 |
+
<span asp-validation-for="StudentId" class="mt-1 text-xs text-rose-500"></span>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<!-- Address -->
|
| 70 |
+
<div class="sm:col-span-2">
|
| 71 |
+
<label asp-for="Address" class="block text-sm font-medium text-slate-700">Address</label>
|
| 72 |
+
<div class="mt-1">
|
| 73 |
+
<textarea asp-for="Address" rows="3" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" placeholder="Full address details"></textarea>
|
| 74 |
+
<span asp-validation-for="Address" class="mt-1 text-xs text-rose-500"></span>
|
| 75 |
+
</div>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
|
| 80 |
+
<a asp-action="Index" class="btn-secondary">Cancel</a>
|
| 81 |
+
<button type="submit" class="btn-primary">Create Member</button>
|
| 82 |
+
</div>
|
| 83 |
+
</form>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
@section Scripts {
|
| 89 |
+
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
| 90 |
+
}
|
Frontend/Views/Members/Details.cshtml
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model Frontend.Models.MemberDetailsViewModel
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = "Member Details";
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
<div class="mb-8">
|
| 7 |
+
<a asp-action="Index" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-900 mb-6 group">
|
| 8 |
+
<svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
| 9 |
+
Back to Directory
|
| 10 |
+
</a>
|
| 11 |
+
|
| 12 |
+
<div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
| 13 |
+
<div class="lg:col-span-1">
|
| 14 |
+
<div class="aspect-square w-full rounded-2xl bg-slate-900 flex flex-col items-center justify-center text-white p-6 shadow-xl">
|
| 15 |
+
<div class="text-6xl font-black mb-4 tracking-tighter">
|
| 16 |
+
@(Model.User.FullName?.Substring(0, 2).ToUpper() ?? "U")
|
| 17 |
+
</div>
|
| 18 |
+
<h2 class="text-xl font-bold text-center">@Model.User.FullName</h2>
|
| 19 |
+
<div class="mt-2 inline-flex rounded-full px-3 py-1 text-xs font-semibold leading-5 @(Model.User.Role == "Admin" ? "bg-amber-500/20 text-amber-200" : "bg-blue-500/20 text-blue-200")">
|
| 20 |
+
@Model.User.Role
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<div class="lg:col-span-2 space-y-8">
|
| 26 |
+
<div>
|
| 27 |
+
<h1 class="text-4xl font-extrabold text-slate-900 tracking-tight mb-2">Member Profile</h1>
|
| 28 |
+
<div class="flex items-center gap-3">
|
| 29 |
+
@if (Model.User.BanStatus == true)
|
| 30 |
+
{
|
| 31 |
+
<span class="inline-flex items-center rounded-full bg-rose-100 px-3 py-1 text-xs font-semibold text-rose-800 tracking-wide uppercase">Suspended</span>
|
| 32 |
+
}
|
| 33 |
+
else if (Model.User.IsActive)
|
| 34 |
+
{
|
| 35 |
+
<span class="inline-flex items-center rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-800 tracking-wide uppercase">Active</span>
|
| 36 |
+
}
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-8 border-y border-slate-100 py-6">
|
| 41 |
+
<div>
|
| 42 |
+
<h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Email</h4>
|
| 43 |
+
<p class="text-sm font-semibold text-slate-900">@Model.User.Email</p>
|
| 44 |
+
</div>
|
| 45 |
+
<div>
|
| 46 |
+
<h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Phone</h4>
|
| 47 |
+
<p class="text-sm font-semibold text-slate-900">@(string.IsNullOrEmpty(Model.User.PhoneNumber) ? "-" : Model.User.PhoneNumber)</p>
|
| 48 |
+
</div>
|
| 49 |
+
<div>
|
| 50 |
+
<h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Student ID</h4>
|
| 51 |
+
<p class="text-sm font-semibold text-slate-900">@(string.IsNullOrEmpty(Model.User.StudentId) ? "-" : Model.User.StudentId)</p>
|
| 52 |
+
</div>
|
| 53 |
+
<div>
|
| 54 |
+
<h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Joined</h4>
|
| 55 |
+
<p class="text-sm font-semibold text-slate-900">@Model.User.CreatedAt.ToString("MMM dd, yyyy")</p>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="sm:col-span-2">
|
| 58 |
+
<h4 class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Address</h4>
|
| 59 |
+
<p class="text-sm font-semibold text-slate-900">@(string.IsNullOrEmpty(Model.User.Address) ? "-" : Model.User.Address)</p>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div class="flex items-center gap-4 pt-4 flex-wrap">
|
| 64 |
+
<a asp-action="Edit" asp-route-id="@Model.User.Id" class="btn-primary py-3 px-8 text-base shadow-lg shadow-slate-200">Edit Details</a>
|
| 65 |
+
|
| 66 |
+
<form asp-action="UpdateRole" asp-route-id="@Model.User.Id" method="POST" class="inline-block">
|
| 67 |
+
<input type="hidden" name="role" value="@(Model.User.Role == "Admin" ? "Member" : "Admin")" />
|
| 68 |
+
<button type="submit" class="btn-secondary py-3 px-8 text-base">
|
| 69 |
+
Make @(Model.User.Role == "Admin" ? "Member" : "Admin")
|
| 70 |
+
</button>
|
| 71 |
+
</form>
|
| 72 |
+
|
| 73 |
+
<form asp-action="Delete" asp-route-id="@Model.User.Id" method="POST" onsubmit="return confirm('Are you sure you want to completely remove this member? This action cannot be undone.');">
|
| 74 |
+
<button type="submit" class="p-3 text-rose-600 hover:text-rose-700 hover:bg-rose-50 rounded-xl transition-colors">
|
| 75 |
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
| 76 |
+
</button>
|
| 77 |
+
</form>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<!-- Subscription Management -->
|
| 81 |
+
<div class="mt-8 pt-8 border-t border-slate-100">
|
| 82 |
+
<h3 class="text-2xl font-bold text-slate-900 mb-6">Manage Subscription</h3>
|
| 83 |
+
|
| 84 |
+
@if (Model.CurrentSubscription != null && Model.CurrentSubscription.IsActive)
|
| 85 |
+
{
|
| 86 |
+
<div class="bg-green-50 border border-green-200 rounded-xl p-4 mb-6">
|
| 87 |
+
<div class="flex justify-between items-center">
|
| 88 |
+
<div>
|
| 89 |
+
<span class="text-sm text-green-800 font-semibold uppercase tracking-wider">Active Plan</span>
|
| 90 |
+
<h4 class="text-xl font-bold text-green-900">@Model.CurrentSubscription.MembershipType</h4>
|
| 91 |
+
</div>
|
| 92 |
+
<div class="text-right">
|
| 93 |
+
<div class="text-xs text-green-700">Expires On</div>
|
| 94 |
+
<div class="font-bold text-green-900">@Model.CurrentSubscription.ExpiryDate.ToString("MMM dd, yyyy")</div>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
}
|
| 99 |
+
else
|
| 100 |
+
{
|
| 101 |
+
<div class="bg-slate-50 border border-slate-200 rounded-xl p-4 mb-6">
|
| 102 |
+
<p class="text-slate-600 font-medium">No active subscription for this member.</p>
|
| 103 |
+
</div>
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
<h4 class="text-lg font-semibold text-slate-900 mb-4">Assign New Plan</h4>
|
| 107 |
+
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
| 108 |
+
@foreach (var plan in Model.AvailableMemberships)
|
| 109 |
+
{
|
| 110 |
+
var isCurrent = Model.CurrentSubscription?.IsActive == true && Model.CurrentSubscription.MembershipId == plan.Id;
|
| 111 |
+
<div class="card p-4 relative @(isCurrent ? "ring-2 ring-slate-900 border-slate-900" : "")">
|
| 112 |
+
@if (isCurrent)
|
| 113 |
+
{
|
| 114 |
+
<div class="absolute top-0 right-0 -mt-2 -mr-2 bg-slate-900 text-white text-xs font-bold px-2 py-1 rounded-md">Current</div>
|
| 115 |
+
}
|
| 116 |
+
<h5 class="font-bold text-slate-900 text-lg mb-1">@plan.Type</h5>
|
| 117 |
+
<p class="text-slate-500 font-medium text-sm mb-4">@plan.Price.ToString("N0") MMK / @plan.DurationMonths mo</p>
|
| 118 |
+
|
| 119 |
+
@if (!isCurrent)
|
| 120 |
+
{
|
| 121 |
+
<form asp-action="AssignSubscription" method="post" onsubmit="return confirm('Assign @plan.Type plan to this member?');">
|
| 122 |
+
<input type="hidden" name="id" value="@Model.User.Id" />
|
| 123 |
+
<input type="hidden" name="membershipId" value="@plan.Id" />
|
| 124 |
+
<button type="submit" class="w-full btn-secondary text-sm">Assign Plan</button>
|
| 125 |
+
</form>
|
| 126 |
+
}
|
| 127 |
+
else
|
| 128 |
+
{
|
| 129 |
+
<button disabled class="w-full btn-secondary text-sm opacity-50 cursor-not-allowed">Current Plan</button>
|
| 130 |
+
}
|
| 131 |
+
</div>
|
| 132 |
+
}
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
Frontend/Views/Members/Edit.cshtml
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model Frontend.Models.Dtos.UserUpdateRequest
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = "Edit Member";
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
<div class="mb-8">
|
| 7 |
+
<a asp-action="Index" class="inline-flex items-center text-sm font-medium text-slate-500 hover:text-slate-900 mb-6 group">
|
| 8 |
+
<svg class="w-4 h-4 mr-2 transition-transform group-hover:-translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
| 9 |
+
Back to Directory
|
| 10 |
+
</a>
|
| 11 |
+
|
| 12 |
+
<div class="max-w-3xl mx-auto">
|
| 13 |
+
<div class="mb-8">
|
| 14 |
+
<h1 class="text-3xl font-bold tracking-tight text-slate-900">Edit Member</h1>
|
| 15 |
+
<p class="mt-2 text-slate-500">Update member's profile details.</p>
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
<div class="card p-6 sm:p-8">
|
| 19 |
+
<form asp-action="Edit" method="post" class="space-y-6">
|
| 20 |
+
<!-- Validation Summary -->
|
| 21 |
+
<div asp-validation-summary="ModelOnly" class="text-sm text-red-600 bg-red-50 p-3 rounded-md"></div>
|
| 22 |
+
|
| 23 |
+
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
| 24 |
+
<!-- FullName -->
|
| 25 |
+
<div class="sm:col-span-2">
|
| 26 |
+
<label asp-for="FullName" class="block text-sm font-medium text-slate-700">Full Name <span class="text-rose-500">*</span></label>
|
| 27 |
+
<div class="mt-1">
|
| 28 |
+
<input asp-for="FullName" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" required />
|
| 29 |
+
<span asp-validation-for="FullName" class="mt-1 text-xs text-rose-500"></span>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<!-- Phone Number -->
|
| 34 |
+
<div>
|
| 35 |
+
<label asp-for="PhoneNumber" class="block text-sm font-medium text-slate-700">Phone Number</label>
|
| 36 |
+
<div class="mt-1">
|
| 37 |
+
<input asp-for="PhoneNumber" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" />
|
| 38 |
+
<span asp-validation-for="PhoneNumber" class="mt-1 text-xs text-rose-500"></span>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<!-- Student ID -->
|
| 43 |
+
<div>
|
| 44 |
+
<label asp-for="StudentId" class="block text-sm font-medium text-slate-700">Student ID</label>
|
| 45 |
+
<div class="mt-1">
|
| 46 |
+
<input asp-for="StudentId" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white" />
|
| 47 |
+
<span asp-validation-for="StudentId" class="mt-1 text-xs text-rose-500"></span>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<!-- Address -->
|
| 52 |
+
<div class="sm:col-span-2">
|
| 53 |
+
<label asp-for="Address" class="block text-sm font-medium text-slate-700">Address</label>
|
| 54 |
+
<div class="mt-1">
|
| 55 |
+
<textarea asp-for="Address" rows="3" class="block w-full rounded-md border-slate-300 shadow-sm focus:border-slate-900 focus:ring-slate-900 sm:text-sm px-3 py-2 border bg-white"></textarea>
|
| 56 |
+
<span asp-validation-for="Address" class="mt-1 text-xs text-rose-500"></span>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
|
| 62 |
+
<a asp-action="Index" class="btn-secondary">Cancel</a>
|
| 63 |
+
<button type="submit" class="btn-primary">Save Changes</button>
|
| 64 |
+
</div>
|
| 65 |
+
</form>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
@section Scripts {
|
| 71 |
+
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
|
| 72 |
+
}
|
Frontend/Views/Members/Index.cshtml
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model IEnumerable<Frontend.Models.Dtos.UserDto>
|
| 2 |
+
@{
|
| 3 |
+
ViewData["Title"] = "Members Directory";
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
|
| 7 |
+
<div>
|
| 8 |
+
<h1 class="text-3xl font-bold tracking-tight text-slate-900">Members Directory</h1>
|
| 9 |
+
<p class="mt-2 text-slate-500">Manage library members and administrator accounts.</p>
|
| 10 |
+
</div>
|
| 11 |
+
<div class="flex items-center gap-3">
|
| 12 |
+
<a asp-action="Create" class="btn-primary">
|
| 13 |
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
| 14 |
+
Add New Member
|
| 15 |
+
</a>
|
| 16 |
+
</div>
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
<div class="card overflow-hidden">
|
| 20 |
+
<div class="overflow-x-auto">
|
| 21 |
+
<table class="min-w-full divide-y divide-slate-200">
|
| 22 |
+
<thead class="bg-slate-50">
|
| 23 |
+
<tr>
|
| 24 |
+
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Member</th>
|
| 25 |
+
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Contact</th>
|
| 26 |
+
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Role</th>
|
| 27 |
+
<th scope="col" class="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Status</th>
|
| 28 |
+
<th scope="col" class="relative px-6 py-3"><span class="sr-only">Actions</span></th>
|
| 29 |
+
</tr>
|
| 30 |
+
</thead>
|
| 31 |
+
<tbody class="bg-white divide-y divide-slate-200">
|
| 32 |
+
@foreach (var user in Model)
|
| 33 |
+
{
|
| 34 |
+
<tr class="hover:bg-slate-50 transition-colors">
|
| 35 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 36 |
+
<div class="flex items-center">
|
| 37 |
+
<div class="h-10 w-10 flex-shrink-0 rounded-full bg-slate-900 flex items-center justify-center text-white font-bold text-sm">
|
| 38 |
+
@(user.FullName?.Substring(0, 2).ToUpper() ?? "U")
|
| 39 |
+
</div>
|
| 40 |
+
<div class="ml-4">
|
| 41 |
+
<div class="text-sm font-medium text-slate-900">
|
| 42 |
+
<a asp-action="Details" asp-route-id="@user.Id" class="hover:underline">@user.FullName</a>
|
| 43 |
+
</div>
|
| 44 |
+
@if (!string.IsNullOrEmpty(user.StudentId))
|
| 45 |
+
{
|
| 46 |
+
<div class="text-xs text-slate-500 text-slate-500">ID: @user.StudentId</div>
|
| 47 |
+
}
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
</td>
|
| 51 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 52 |
+
<div class="text-sm text-slate-900">@user.Email</div>
|
| 53 |
+
<div class="text-xs text-slate-500">@(string.IsNullOrEmpty(user.PhoneNumber) ? "No phone" : user.PhoneNumber)</div>
|
| 54 |
+
</td>
|
| 55 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 56 |
+
<span class="inline-flex rounded-full px-2 text-xs font-semibold leading-5 @(user.Role == "Admin" ? "bg-amber-100 text-amber-800" : "bg-blue-100 text-blue-800")">
|
| 57 |
+
@user.Role
|
| 58 |
+
</span>
|
| 59 |
+
</td>
|
| 60 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
| 61 |
+
@if (user.BanStatus == true)
|
| 62 |
+
{
|
| 63 |
+
<span class="inline-flex rounded-full bg-rose-100 px-2 text-xs font-semibold leading-5 text-rose-800">
|
| 64 |
+
Suspended
|
| 65 |
+
</span>
|
| 66 |
+
}
|
| 67 |
+
else if (user.IsActive)
|
| 68 |
+
{
|
| 69 |
+
<span class="inline-flex rounded-full bg-emerald-100 px-2 text-xs font-semibold leading-5 text-emerald-800">
|
| 70 |
+
Active
|
| 71 |
+
</span>
|
| 72 |
+
}
|
| 73 |
+
else
|
| 74 |
+
{
|
| 75 |
+
<span class="inline-flex rounded-full bg-slate-100 px-2 text-xs font-semibold leading-5 text-slate-800">
|
| 76 |
+
Inactive
|
| 77 |
+
</span>
|
| 78 |
+
}
|
| 79 |
+
</td>
|
| 80 |
+
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
| 81 |
+
<div class="flex items-center justify-end gap-3">
|
| 82 |
+
<a asp-action="Details" asp-route-id="@user.Id" class="text-slate-600 hover:text-slate-900 transition-colors">Details</a>
|
| 83 |
+
<a asp-action="Edit" asp-route-id="@user.Id" class="text-indigo-600 hover:text-indigo-900 transition-colors">Edit</a>
|
| 84 |
+
</div>
|
| 85 |
+
</td>
|
| 86 |
+
</tr>
|
| 87 |
+
}
|
| 88 |
+
</tbody>
|
| 89 |
+
</table>
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
Frontend/Views/Shared/Components/_Card.cshtml
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model string
|
| 2 |
+
<div class="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden @Model">
|
| 3 |
+
@RenderBody()
|
| 4 |
+
</div>
|
Frontend/Views/Shared/_Layout.cshtml
CHANGED
|
@@ -1,48 +1,213 @@
|
|
| 1 |
-
|
| 2 |
-
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
-
<title>@ViewData["Title"] -
|
| 7 |
-
|
| 8 |
-
<
|
| 9 |
-
<link rel="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
</head>
|
| 11 |
-
<body>
|
| 12 |
-
<
|
| 13 |
-
<
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
<
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
<
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
</
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
</div>
|
| 30 |
</div>
|
| 31 |
-
</
|
| 32 |
-
</header>
|
| 33 |
-
<div class="container">
|
| 34 |
-
<main role="main" class="pb-3">
|
| 35 |
-
@RenderBody()
|
| 36 |
-
</main>
|
| 37 |
-
</div>
|
| 38 |
|
| 39 |
-
|
| 40 |
-
<div class="
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
</div>
|
| 43 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
| 45 |
-
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
| 46 |
<script src="~/js/site.js" asp-append-version="true"></script>
|
| 47 |
@await RenderSectionAsync("Scripts", required: false)
|
| 48 |
</body>
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" class="h-full bg-slate-50">
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>@ViewData["Title"] - Library Management</title>
|
| 7 |
+
|
| 8 |
+
<!-- Google Fonts: Inter -->
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 12 |
+
|
| 13 |
+
<!-- Tailwind CSS Play CDN -->
|
| 14 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 15 |
+
<script>
|
| 16 |
+
tailwind.config = {
|
| 17 |
+
theme: {
|
| 18 |
+
extend: {
|
| 19 |
+
fontFamily: {
|
| 20 |
+
sans: ['Inter', 'sans-serif'],
|
| 21 |
+
},
|
| 22 |
+
colors: {
|
| 23 |
+
primary: {
|
| 24 |
+
50: '#f8fafc',
|
| 25 |
+
100: '#f1f5f9',
|
| 26 |
+
200: '#e2e8f0',
|
| 27 |
+
300: '#cbd5e1',
|
| 28 |
+
400: '#94a3b8',
|
| 29 |
+
500: '#64748b',
|
| 30 |
+
600: '#475569',
|
| 31 |
+
700: '#334155',
|
| 32 |
+
800: '#1e293b',
|
| 33 |
+
900: '#0f172a',
|
| 34 |
+
950: '#020617',
|
| 35 |
+
},
|
| 36 |
+
},
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
</script>
|
| 41 |
+
|
| 42 |
+
<style type="text/tailwindcss">
|
| 43 |
+
@@layer base {
|
| 44 |
+
body { @@apply text-slate-900 antialiased h-full; }
|
| 45 |
+
}
|
| 46 |
+
@@layer components {
|
| 47 |
+
.nav-link { @@apply flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-md transition-colors; }
|
| 48 |
+
.nav-link-active { @@apply bg-slate-900 text-white; }
|
| 49 |
+
.nav-link-inactive { @@apply text-slate-600 hover:text-slate-900 hover:bg-slate-100; }
|
| 50 |
+
|
| 51 |
+
.card { @@apply bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden; }
|
| 52 |
+
.btn-primary { @@apply inline-flex items-center justify-center rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white shadow transition-colors hover:bg-slate-900/90 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50; }
|
| 53 |
+
.btn-secondary { @@apply inline-flex items-center justify-center rounded-md border border-slate-200 bg-white px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50; }
|
| 54 |
+
}
|
| 55 |
+
</style>
|
| 56 |
</head>
|
| 57 |
+
<body class="h-full">
|
| 58 |
+
<div class="flex h-full overflow-hidden">
|
| 59 |
+
<!-- Sidebar -->
|
| 60 |
+
<aside class="hidden md:flex md:w-64 md:flex-col md:fixed md:inset-y-0 z-50">
|
| 61 |
+
<div class="flex flex-col flex-grow border-r border-slate-200 bg-white pt-5 pb-4 overflow-y-auto">
|
| 62 |
+
<div class="flex items-center flex-shrink-0 px-4 gap-2">
|
| 63 |
+
<div class="bg-slate-900 rounded-lg p-1.5 text-white">
|
| 64 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/><path d="M4 12V4l4 4"/><path d="m12 6 4 4"/></svg>
|
| 65 |
+
</div>
|
| 66 |
+
<span class="text-lg font-bold tracking-tight">Library OS</span>
|
| 67 |
+
</div>
|
| 68 |
+
<div class="mt-8 flex-grow flex flex-col">
|
| 69 |
+
<nav class="flex-1 px-2 space-y-1">
|
| 70 |
+
<a asp-controller="Home" asp-action="Index" class="nav-link @(ViewContext.RouteData.Values["Controller"]?.ToString() == "Home" ? "nav-link-active" : "nav-link-inactive")">
|
| 71 |
+
<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="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="15" rx="1"/></svg>
|
| 72 |
+
Dashboard
|
| 73 |
+
</a>
|
| 74 |
+
<a href="/Books" class="nav-link nav-link-inactive">
|
| 75 |
+
<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="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>
|
| 76 |
+
Books Catalog
|
| 77 |
+
</a>
|
| 78 |
+
<a href="/Borrowings" class="nav-link nav-link-inactive">
|
| 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
|
| 95 |
+
</a>
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
</nav>
|
| 99 |
+
</div>
|
| 100 |
+
<div class="px-4 py-4 border-t border-slate-100">
|
| 101 |
+
<div class="flex items-center gap-3">
|
| 102 |
+
<div class="h-8 w-8 rounded-full bg-slate-900 flex items-center justify-center text-white font-bold text-xs uppercase">
|
| 103 |
+
@(User.Identity?.IsAuthenticated == true ? User.Identity.Name?.Substring(0, 2) : "GU")
|
| 104 |
+
</div>
|
| 105 |
+
<div class="flex flex-col">
|
| 106 |
+
@if (User.Identity?.IsAuthenticated == true)
|
| 107 |
+
{
|
| 108 |
+
<span class="text-sm font-medium">@User.Identity.Name</span>
|
| 109 |
+
<span class="text-xs text-slate-500">@User.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Role)?.Value</span>
|
| 110 |
+
}
|
| 111 |
+
else
|
| 112 |
+
{
|
| 113 |
+
<span class="text-sm font-medium">Guest User</span>
|
| 114 |
+
<span class="text-xs text-slate-500">Sign in to borrow</span>
|
| 115 |
+
}
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
</div>
|
| 119 |
</div>
|
| 120 |
+
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
+
<!-- Main Content Wrapper -->
|
| 123 |
+
<div class="md:pl-64 flex flex-col flex-1 w-0">
|
| 124 |
+
<!-- Topbar -->
|
| 125 |
+
<header class="flex-shrink-0 flex h-16 bg-white border-bottom border-slate-200">
|
| 126 |
+
<div class="flex-1 px-4 flex justify-between">
|
| 127 |
+
<div class="flex-1 flex items-center">
|
| 128 |
+
<div class="w-full max-w-sm">
|
| 129 |
+
<label for="search" class="sr-only">Search</label>
|
| 130 |
+
<div class="relative">
|
| 131 |
+
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
| 132 |
+
<svg class="h-4 w-4 text-slate-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
| 133 |
+
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
|
| 134 |
+
</svg>
|
| 135 |
+
</div>
|
| 136 |
+
<input id="search" name="search" class="block w-full pl-10 pr-3 py-2 border border-slate-200 rounded-md leading-5 bg-slate-50 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-900 focus:border-slate-900 sm:text-sm transition-all" placeholder="Search books, authors, ISBN..." type="search">
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
<div class="ml-4 flex items-center md:ml-6 gap-4">
|
| 141 |
+
<button type="button" class="bg-white p-1 rounded-full text-slate-400 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900">
|
| 142 |
+
<span class="sr-only">View notifications</span>
|
| 143 |
+
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
| 144 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
| 145 |
+
</svg>
|
| 146 |
+
</button>
|
| 147 |
+
@if (User.Identity?.IsAuthenticated == true)
|
| 148 |
+
{
|
| 149 |
+
<a href="/Auth/Logout" class="btn-secondary py-1.5">Logout</a>
|
| 150 |
+
}
|
| 151 |
+
else
|
| 152 |
+
{
|
| 153 |
+
<a href="/Auth/Login" class="btn-primary py-1.5">Sign In</a>
|
| 154 |
+
}
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
</header>
|
| 158 |
+
|
| 159 |
+
<!-- Main Content -->
|
| 160 |
+
<main class="flex-1 relative overflow-y-auto focus:outline-none">
|
| 161 |
+
<div class="py-8 px-4 sm:px-6 md:px-8 max-w-7xl mx-auto">
|
| 162 |
+
@RenderBody()
|
| 163 |
+
</div>
|
| 164 |
+
</main>
|
| 165 |
</div>
|
| 166 |
+
</div>
|
| 167 |
+
|
| 168 |
+
<!-- Toast Notification Container -->
|
| 169 |
+
<div id="toast-container" class="fixed bottom-5 right-5 z-[100] flex flex-col gap-3 pointer-events-none"></div>
|
| 170 |
+
|
| 171 |
+
<script>
|
| 172 |
+
function showToast(message, type = 'success') {
|
| 173 |
+
const container = document.getElementById('toast-container');
|
| 174 |
+
const toast = document.createElement('div');
|
| 175 |
+
|
| 176 |
+
const bgColor = type === 'success' ? 'bg-slate-900' : 'bg-red-600';
|
| 177 |
+
const icon = type === 'success'
|
| 178 |
+
? '<svg class="w-4 h-4 text-white" 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>'
|
| 179 |
+
: '<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>';
|
| 180 |
+
|
| 181 |
+
toast.className = `${bgColor} text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 transition-all duration-300 transform translate-y-10 opacity-0 pointer-events-auto min-w-[250px]`;
|
| 182 |
+
toast.innerHTML = `
|
| 183 |
+
<div class="shrink-0">${icon}</div>
|
| 184 |
+
<div class="text-sm font-medium tracking-tight">${message}</div>
|
| 185 |
+
`;
|
| 186 |
+
|
| 187 |
+
container.appendChild(toast);
|
| 188 |
+
|
| 189 |
+
// Animate in
|
| 190 |
+
setTimeout(() => {
|
| 191 |
+
toast.classList.remove('translate-y-10', 'opacity-0');
|
| 192 |
+
}, 10);
|
| 193 |
+
|
| 194 |
+
// Remove after 3 seconds
|
| 195 |
+
setTimeout(() => {
|
| 196 |
+
toast.classList.add('translate-y-[-10px]', 'opacity-0');
|
| 197 |
+
setTimeout(() => toast.remove(), 300);
|
| 198 |
+
}, 3000);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// Auto-show notifications from TempData if available
|
| 202 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 203 |
+
const success = '@TempData["Success"]';
|
| 204 |
+
const error = '@TempData["Error"]';
|
| 205 |
+
if (success) showToast(success, 'success');
|
| 206 |
+
if (error) showToast(error, 'error');
|
| 207 |
+
});
|
| 208 |
+
</script>
|
| 209 |
+
|
| 210 |
<script src="~/lib/jquery/dist/jquery.min.js"></script>
|
|
|
|
| 211 |
<script src="~/js/site.js" asp-append-version="true"></script>
|
| 212 |
@await RenderSectionAsync("Scripts", required: false)
|
| 213 |
</body>
|
Frontend/Views/Subscriptions/Index.cshtml
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@model Frontend.Models.SubscriptionsViewModel
|
| 2 |
+
|
| 3 |
+
@{
|
| 4 |
+
ViewData["Title"] = "Memberships";
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
<div class="mb-8">
|
| 8 |
+
<h1 class="text-2xl font-bold text-slate-900">Memberships</h1>
|
| 9 |
+
<p class="text-slate-600 mt-1">Available membership plans in the system</p>
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
<!-- Available Plans Section -->
|
| 13 |
+
<div>
|
| 14 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 15 |
+
@foreach (var plan in Model.AvailableMemberships)
|
| 16 |
+
{
|
| 17 |
+
<div class="card flex flex-col h-full relative transition duration-200 hover:shadow-md">
|
| 18 |
+
<div class="p-6 border-b border-slate-100 flex-grow">
|
| 19 |
+
<h3 class="text-lg font-bold text-slate-900 uppercase tracking-wider mb-2">@plan.Type</h3>
|
| 20 |
+
<div class="flex items-baseline gap-1 mb-4">
|
| 21 |
+
<span class="text-3xl font-extrabold text-slate-900">@plan.Price.ToString("N0") <span class="text-lg text-slate-500 font-medium">MMK</span></span>
|
| 22 |
+
<span class="text-slate-500 font-medium">/ @(plan.DurationMonths == 1 ? "month" : $"{plan.DurationMonths} months")</span>
|
| 23 |
+
</div>
|
| 24 |
+
|
| 25 |
+
<ul class="space-y-3 mt-6">
|
| 26 |
+
<li class="flex gap-2">
|
| 27 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-500 flex-shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
|
| 28 |
+
<span class="text-slate-600">Borrow up to <span class="font-bold text-slate-900">@plan.MaxBooks books</span> at a time</span>
|
| 29 |
+
</li>
|
| 30 |
+
<li class="flex gap-2">
|
| 31 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-500 flex-shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
|
| 32 |
+
<span class="text-slate-600">Keep books for <span class="font-bold text-slate-900">@plan.BorrowingDays days</span></span>
|
| 33 |
+
</li>
|
| 34 |
+
<li class="flex gap-2">
|
| 35 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-green-500 flex-shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
|
| 36 |
+
<span class="text-slate-600">Full catalog access</span>
|
| 37 |
+
</li>
|
| 38 |
+
</ul>
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<div class="p-4 bg-slate-50 mt-auto text-center border-t border-slate-100">
|
| 42 |
+
<span class="text-xs text-slate-500 font-medium uppercase tracking-widest">System Plan</span>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
}
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
Frontend/appsettings.json
CHANGED
|
@@ -5,5 +5,6 @@
|
|
| 5 |
"Microsoft.AspNetCore": "Warning"
|
| 6 |
}
|
| 7 |
},
|
| 8 |
-
"AllowedHosts": "*"
|
|
|
|
| 9 |
}
|
|
|
|
| 5 |
"Microsoft.AspNetCore": "Warning"
|
| 6 |
}
|
| 7 |
},
|
| 8 |
+
"AllowedHosts": "*",
|
| 9 |
+
"BackendApiBaseUrl": "https://localhost:7028"
|
| 10 |
}
|