sannlynnhtun-coding commited on
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 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"] = "Home Page";
3
  }
4
 
5
- <div class="text-center">
6
- <h1 class="display-4">Welcome</h1>
7
- <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</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
- <!DOCTYPE html>
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"] - Frontend</title>
7
- <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
8
- <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
9
- <link rel="stylesheet" href="~/Frontend.styles.css" asp-append-version="true" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  </head>
11
- <body>
12
- <header>
13
- <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
14
- <div class="container-fluid">
15
- <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Frontend</a>
16
- <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
17
- aria-expanded="false" aria-label="Toggle navigation">
18
- <span class="navbar-toggler-icon"></span>
19
- </button>
20
- <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
21
- <ul class="navbar-nav flex-grow-1">
22
- <li class="nav-item">
23
- <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
24
- </li>
25
- <li class="nav-item">
26
- <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
27
- </li>
28
- </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  </div>
30
  </div>
31
- </nav>
32
- </header>
33
- <div class="container">
34
- <main role="main" class="pb-3">
35
- @RenderBody()
36
- </main>
37
- </div>
38
 
39
- <footer class="border-top footer text-muted">
40
- <div class="container">
41
- &copy; 2026 - Frontend - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  </div>
43
- </footer>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
  }