Spaces:
Sleeping
Sleeping
Yuyuqt commited on
Commit ·
861f0db
1
Parent(s): 36144b0
add: book categories and filter
Browse files- Backend/Features/Books/BookController.cs +2 -2
- Backend/Features/Books/BookService.cs +51 -9
- Backend/Features/Categories/CategoryController.cs +7 -0
- Backend/Features/Categories/CategoryService.cs +10 -0
- Frontend/Controllers/BooksController.cs +7 -3
- Frontend/Models/Dtos/LibraryDtos.cs +9 -0
- Frontend/Services/LibraryApiClient.cs +8 -2
- Frontend/Views/Books/Create.cshtml +25 -0
- Frontend/Views/Books/Edit.cshtml +27 -0
- Frontend/Views/Books/Index.cshtml +41 -5
Backend/Features/Books/BookController.cs
CHANGED
|
@@ -16,9 +16,9 @@ namespace Backend.Features.Books
|
|
| 16 |
|
| 17 |
[HttpGet]
|
| 18 |
[Authorize]
|
| 19 |
-
public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks()
|
| 20 |
{
|
| 21 |
-
var books = await _bookService.GetAllBooksAsync();
|
| 22 |
return Ok(books);
|
| 23 |
}
|
| 24 |
|
|
|
|
| 16 |
|
| 17 |
[HttpGet]
|
| 18 |
[Authorize]
|
| 19 |
+
public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks([FromQuery] int? categoryId = null)
|
| 20 |
{
|
| 21 |
+
var books = await _bookService.GetAllBooksAsync(categoryId);
|
| 22 |
return Ok(books);
|
| 23 |
}
|
| 24 |
|
Backend/Features/Books/BookService.cs
CHANGED
|
@@ -5,7 +5,7 @@ namespace Backend.Features.Books
|
|
| 5 |
{
|
| 6 |
public interface IBookService
|
| 7 |
{
|
| 8 |
-
Task<IEnumerable<BookDto>> GetAllBooksAsync();
|
| 9 |
Task<BookDto?> GetBookByIdAsync(int id);
|
| 10 |
Task<BookDto> CreateBookAsync(BookCreateRequest request);
|
| 11 |
Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request);
|
|
@@ -21,17 +21,26 @@ namespace Backend.Features.Books
|
|
| 21 |
_context = context;
|
| 22 |
}
|
| 23 |
|
| 24 |
-
public async Task<IEnumerable<BookDto>> GetAllBooksAsync()
|
| 25 |
{
|
| 26 |
-
|
| 27 |
-
.
|
| 28 |
-
.
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
public async Task<BookDto?> GetBookByIdAsync(int id)
|
| 33 |
{
|
| 34 |
-
var book = await _context.Books
|
|
|
|
|
|
|
| 35 |
if (book == null || !book.IsActive) return null;
|
| 36 |
return MapToDto(book);
|
| 37 |
}
|
|
@@ -56,6 +65,16 @@ namespace Backend.Features.Books
|
|
| 56 |
CreatedAt = DateTime.UtcNow
|
| 57 |
};
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
_context.Books.Add(book);
|
| 60 |
await _context.SaveChangesAsync();
|
| 61 |
|
|
@@ -64,7 +83,9 @@ namespace Backend.Features.Books
|
|
| 64 |
|
| 65 |
public async Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request)
|
| 66 |
{
|
| 67 |
-
var book = await _context.Books
|
|
|
|
|
|
|
| 68 |
if (book == null || !book.IsActive) return null;
|
| 69 |
|
| 70 |
book.Title = request.Title;
|
|
@@ -81,6 +102,17 @@ namespace Backend.Features.Books
|
|
| 81 |
|
| 82 |
book.UpdatedAt = DateTime.UtcNow;
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
await _context.SaveChangesAsync();
|
| 85 |
return MapToDto(book);
|
| 86 |
}
|
|
@@ -111,7 +143,8 @@ namespace Backend.Features.Books
|
|
| 111 |
TotalCopies = book.TotalCopies,
|
| 112 |
AvailableCopies = book.AvailableCopies,
|
| 113 |
CreatedAt = book.CreatedAt,
|
| 114 |
-
UpdatedAt = book.UpdatedAt
|
|
|
|
| 115 |
};
|
| 116 |
}
|
| 117 |
}
|
|
@@ -129,6 +162,13 @@ namespace Backend.Features.Books
|
|
| 129 |
public int AvailableCopies { get; set; }
|
| 130 |
public DateTime CreatedAt { get; set; }
|
| 131 |
public DateTime? UpdatedAt { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
}
|
| 133 |
|
| 134 |
public class BookCreateRequest
|
|
@@ -138,6 +178,7 @@ namespace Backend.Features.Books
|
|
| 138 |
public string Author { get; set; } = string.Empty;
|
| 139 |
public string? Description { get; set; }
|
| 140 |
public int TotalCopies { get; set; }
|
|
|
|
| 141 |
}
|
| 142 |
|
| 143 |
public class BookUpdateRequest
|
|
@@ -146,5 +187,6 @@ namespace Backend.Features.Books
|
|
| 146 |
public string Author { get; set; } = string.Empty;
|
| 147 |
public string? Description { get; set; }
|
| 148 |
public int TotalCopies { get; set; }
|
|
|
|
| 149 |
}
|
| 150 |
}
|
|
|
|
| 5 |
{
|
| 6 |
public interface IBookService
|
| 7 |
{
|
| 8 |
+
Task<IEnumerable<BookDto>> GetAllBooksAsync(int? categoryId = null);
|
| 9 |
Task<BookDto?> GetBookByIdAsync(int id);
|
| 10 |
Task<BookDto> CreateBookAsync(BookCreateRequest request);
|
| 11 |
Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request);
|
|
|
|
| 21 |
_context = context;
|
| 22 |
}
|
| 23 |
|
| 24 |
+
public async Task<IEnumerable<BookDto>> GetAllBooksAsync(int? categoryId = null)
|
| 25 |
{
|
| 26 |
+
var query = _context.Books
|
| 27 |
+
.Include(b => b.Categories)
|
| 28 |
+
.Where(b => b.IsActive);
|
| 29 |
+
|
| 30 |
+
if (categoryId.HasValue)
|
| 31 |
+
{
|
| 32 |
+
query = query.Where(b => b.Categories.Any(c => c.Id == categoryId.Value));
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
var books = await query.ToListAsync();
|
| 36 |
+
return books.Select(b => MapToDto(b));
|
| 37 |
}
|
| 38 |
|
| 39 |
public async Task<BookDto?> GetBookByIdAsync(int id)
|
| 40 |
{
|
| 41 |
+
var book = await _context.Books
|
| 42 |
+
.Include(b => b.Categories)
|
| 43 |
+
.FirstOrDefaultAsync(b => b.Id == id);
|
| 44 |
if (book == null || !book.IsActive) return null;
|
| 45 |
return MapToDto(book);
|
| 46 |
}
|
|
|
|
| 65 |
CreatedAt = DateTime.UtcNow
|
| 66 |
};
|
| 67 |
|
| 68 |
+
// Attach selected categories
|
| 69 |
+
if (request.CategoryIds != null && request.CategoryIds.Any())
|
| 70 |
+
{
|
| 71 |
+
var categories = await _context.Categories
|
| 72 |
+
.Where(c => request.CategoryIds.Contains(c.Id))
|
| 73 |
+
.ToListAsync();
|
| 74 |
+
foreach (var cat in categories)
|
| 75 |
+
book.Categories.Add(cat);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
_context.Books.Add(book);
|
| 79 |
await _context.SaveChangesAsync();
|
| 80 |
|
|
|
|
| 83 |
|
| 84 |
public async Task<BookDto?> UpdateBookAsync(int id, BookUpdateRequest request)
|
| 85 |
{
|
| 86 |
+
var book = await _context.Books
|
| 87 |
+
.Include(b => b.Categories)
|
| 88 |
+
.FirstOrDefaultAsync(b => b.Id == id);
|
| 89 |
if (book == null || !book.IsActive) return null;
|
| 90 |
|
| 91 |
book.Title = request.Title;
|
|
|
|
| 102 |
|
| 103 |
book.UpdatedAt = DateTime.UtcNow;
|
| 104 |
|
| 105 |
+
// Sync categories: replace existing with new selection
|
| 106 |
+
book.Categories.Clear();
|
| 107 |
+
if (request.CategoryIds != null && request.CategoryIds.Any())
|
| 108 |
+
{
|
| 109 |
+
var categories = await _context.Categories
|
| 110 |
+
.Where(c => request.CategoryIds.Contains(c.Id))
|
| 111 |
+
.ToListAsync();
|
| 112 |
+
foreach (var cat in categories)
|
| 113 |
+
book.Categories.Add(cat);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
await _context.SaveChangesAsync();
|
| 117 |
return MapToDto(book);
|
| 118 |
}
|
|
|
|
| 143 |
TotalCopies = book.TotalCopies,
|
| 144 |
AvailableCopies = book.AvailableCopies,
|
| 145 |
CreatedAt = book.CreatedAt,
|
| 146 |
+
UpdatedAt = book.UpdatedAt,
|
| 147 |
+
Categories = book.Categories.Select(c => new BookCategoryDto { Id = c.Id, Name = c.Name }).ToList()
|
| 148 |
};
|
| 149 |
}
|
| 150 |
}
|
|
|
|
| 162 |
public int AvailableCopies { get; set; }
|
| 163 |
public DateTime CreatedAt { get; set; }
|
| 164 |
public DateTime? UpdatedAt { get; set; }
|
| 165 |
+
public List<BookCategoryDto> Categories { get; set; } = new();
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
public class BookCategoryDto
|
| 169 |
+
{
|
| 170 |
+
public int Id { get; set; }
|
| 171 |
+
public string Name { get; set; } = string.Empty;
|
| 172 |
}
|
| 173 |
|
| 174 |
public class BookCreateRequest
|
|
|
|
| 178 |
public string Author { get; set; } = string.Empty;
|
| 179 |
public string? Description { get; set; }
|
| 180 |
public int TotalCopies { get; set; }
|
| 181 |
+
public List<int>? CategoryIds { get; set; }
|
| 182 |
}
|
| 183 |
|
| 184 |
public class BookUpdateRequest
|
|
|
|
| 187 |
public string Author { get; set; } = string.Empty;
|
| 188 |
public string? Description { get; set; }
|
| 189 |
public int TotalCopies { get; set; }
|
| 190 |
+
public List<int>? CategoryIds { get; set; }
|
| 191 |
}
|
| 192 |
}
|
Backend/Features/Categories/CategoryController.cs
CHANGED
|
@@ -22,6 +22,13 @@ namespace Backend.Features.Categories
|
|
| 22 |
return Ok(categories);
|
| 23 |
}
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
[HttpPost]
|
| 26 |
//[Authorize(Roles = "Librarian")]
|
| 27 |
public async Task<ActionResult<CategoryDto>> CreateCategory([FromBody] CategoryCreateRequest request)
|
|
|
|
| 22 |
return Ok(categories);
|
| 23 |
}
|
| 24 |
|
| 25 |
+
[HttpGet("with-books")]
|
| 26 |
+
public async Task<ActionResult<IEnumerable<CategoryDto>>> GetCategoriesWithBooks()
|
| 27 |
+
{
|
| 28 |
+
var categories = await _categoryService.GetCategoriesWithBooksAsync();
|
| 29 |
+
return Ok(categories);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
[HttpPost]
|
| 33 |
//[Authorize(Roles = "Librarian")]
|
| 34 |
public async Task<ActionResult<CategoryDto>> CreateCategory([FromBody] CategoryCreateRequest request)
|
Backend/Features/Categories/CategoryService.cs
CHANGED
|
@@ -6,6 +6,7 @@ namespace Backend.Features.Categories
|
|
| 6 |
public interface ICategoryService
|
| 7 |
{
|
| 8 |
Task<IEnumerable<CategoryDto>> GetAllCategoriesAsync();
|
|
|
|
| 9 |
Task<CategoryDto> CreateCategoryAsync(CategoryCreateRequest request);
|
| 10 |
Task<bool> DeleteCategoryAsync(int id);
|
| 11 |
}
|
|
@@ -26,6 +27,15 @@ namespace Backend.Features.Categories
|
|
| 26 |
.ToListAsync();
|
| 27 |
}
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
public async Task<CategoryDto> CreateCategoryAsync(CategoryCreateRequest request)
|
| 30 |
{
|
| 31 |
if (await _context.Categories.AnyAsync(c => c.Name == request.Name))
|
|
|
|
| 6 |
public interface ICategoryService
|
| 7 |
{
|
| 8 |
Task<IEnumerable<CategoryDto>> GetAllCategoriesAsync();
|
| 9 |
+
Task<IEnumerable<CategoryDto>> GetCategoriesWithBooksAsync();
|
| 10 |
Task<CategoryDto> CreateCategoryAsync(CategoryCreateRequest request);
|
| 11 |
Task<bool> DeleteCategoryAsync(int id);
|
| 12 |
}
|
|
|
|
| 27 |
.ToListAsync();
|
| 28 |
}
|
| 29 |
|
| 30 |
+
public async Task<IEnumerable<CategoryDto>> GetCategoriesWithBooksAsync()
|
| 31 |
+
{
|
| 32 |
+
// Only return categories that have at least one active book in BookCategories
|
| 33 |
+
return await _context.Categories
|
| 34 |
+
.Where(c => c.Books.Any(b => b.IsActive))
|
| 35 |
+
.Select(c => new CategoryDto { Id = c.Id, Name = c.Name })
|
| 36 |
+
.ToListAsync();
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
public async Task<CategoryDto> CreateCategoryAsync(CategoryCreateRequest request)
|
| 40 |
{
|
| 41 |
if (await _context.Categories.AnyAsync(c => c.Name == request.Name))
|
Frontend/Controllers/BooksController.cs
CHANGED
|
@@ -13,10 +13,10 @@ namespace Frontend.Controllers
|
|
| 13 |
_apiClient = apiClient;
|
| 14 |
}
|
| 15 |
|
| 16 |
-
public async Task<IActionResult> Index(int page = 1)
|
| 17 |
{
|
| 18 |
const int pageSize = 8;
|
| 19 |
-
var allBooks = await _apiClient.GetBooksAsync() ?? Enumerable.Empty<BookDto>();
|
| 20 |
var totalCount = allBooks.Count();
|
| 21 |
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
| 22 |
page = Math.Max(1, Math.Min(page, Math.Max(1, totalPages)));
|
|
@@ -30,6 +30,9 @@ namespace Frontend.Controllers
|
|
| 30 |
PageSize = pageSize
|
| 31 |
};
|
| 32 |
|
|
|
|
|
|
|
|
|
|
| 33 |
return View(pagedResult);
|
| 34 |
}
|
| 35 |
|
|
@@ -80,7 +83,8 @@ namespace Frontend.Controllers
|
|
| 80 |
Title = book.Title,
|
| 81 |
Author = book.Author,
|
| 82 |
Description = book.Description,
|
| 83 |
-
TotalCopies = book.TotalCopies
|
|
|
|
| 84 |
});
|
| 85 |
}
|
| 86 |
|
|
|
|
| 13 |
_apiClient = apiClient;
|
| 14 |
}
|
| 15 |
|
| 16 |
+
public async Task<IActionResult> Index(int page = 1, int? categoryId = null)
|
| 17 |
{
|
| 18 |
const int pageSize = 8;
|
| 19 |
+
var allBooks = await _apiClient.GetBooksAsync(categoryId) ?? Enumerable.Empty<BookDto>();
|
| 20 |
var totalCount = allBooks.Count();
|
| 21 |
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
| 22 |
page = Math.Max(1, Math.Min(page, Math.Max(1, totalPages)));
|
|
|
|
| 30 |
PageSize = pageSize
|
| 31 |
};
|
| 32 |
|
| 33 |
+
ViewBag.Categories = await _apiClient.GetCategoriesWithBooksAsync();
|
| 34 |
+
ViewBag.SelectedCategoryId = categoryId;
|
| 35 |
+
|
| 36 |
return View(pagedResult);
|
| 37 |
}
|
| 38 |
|
|
|
|
| 83 |
Title = book.Title,
|
| 84 |
Author = book.Author,
|
| 85 |
Description = book.Description,
|
| 86 |
+
TotalCopies = book.TotalCopies,
|
| 87 |
+
CategoryIds = book.Categories.Select(c => c.Id).ToList()
|
| 88 |
});
|
| 89 |
}
|
| 90 |
|
Frontend/Models/Dtos/LibraryDtos.cs
CHANGED
|
@@ -41,6 +41,13 @@ namespace Frontend.Models.Dtos
|
|
| 41 |
public int AvailableCopies { get; set; }
|
| 42 |
public DateTime CreatedAt { get; set; }
|
| 43 |
public DateTime? UpdatedAt { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
public class BookCreateRequest
|
|
@@ -50,6 +57,7 @@ namespace Frontend.Models.Dtos
|
|
| 50 |
public string Author { get; set; } = string.Empty;
|
| 51 |
public string? Description { get; set; }
|
| 52 |
public int TotalCopies { get; set; }
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
public class BookUpdateRequest
|
|
@@ -58,6 +66,7 @@ namespace Frontend.Models.Dtos
|
|
| 58 |
public string Author { get; set; } = string.Empty;
|
| 59 |
public string? Description { get; set; }
|
| 60 |
public int TotalCopies { get; set; }
|
|
|
|
| 61 |
}
|
| 62 |
|
| 63 |
// Borrowing DTOs
|
|
|
|
| 41 |
public int AvailableCopies { get; set; }
|
| 42 |
public DateTime CreatedAt { get; set; }
|
| 43 |
public DateTime? UpdatedAt { get; set; }
|
| 44 |
+
public List<BookCategoryDto> Categories { get; set; } = new();
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
public class BookCategoryDto
|
| 48 |
+
{
|
| 49 |
+
public int Id { get; set; }
|
| 50 |
+
public string Name { get; set; } = string.Empty;
|
| 51 |
}
|
| 52 |
|
| 53 |
public class BookCreateRequest
|
|
|
|
| 57 |
public string Author { get; set; } = string.Empty;
|
| 58 |
public string? Description { get; set; }
|
| 59 |
public int TotalCopies { get; set; }
|
| 60 |
+
public List<int>? CategoryIds { get; set; }
|
| 61 |
}
|
| 62 |
|
| 63 |
public class BookUpdateRequest
|
|
|
|
| 66 |
public string Author { get; set; } = string.Empty;
|
| 67 |
public string? Description { get; set; }
|
| 68 |
public int TotalCopies { get; set; }
|
| 69 |
+
public List<int>? CategoryIds { get; set; }
|
| 70 |
}
|
| 71 |
|
| 72 |
// Borrowing DTOs
|
Frontend/Services/LibraryApiClient.cs
CHANGED
|
@@ -54,9 +54,10 @@ namespace Frontend.Services
|
|
| 54 |
#endregion
|
| 55 |
|
| 56 |
#region Books
|
| 57 |
-
public async Task<IEnumerable<BookDto>> GetBooksAsync()
|
| 58 |
{
|
| 59 |
-
|
|
|
|
| 60 |
}
|
| 61 |
|
| 62 |
public async Task<BookDto?> GetBookAsync(int id)
|
|
@@ -113,6 +114,11 @@ namespace Frontend.Services
|
|
| 113 |
return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories") ?? Enumerable.Empty<CategoryDto>();
|
| 114 |
}
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
public async Task<CategoryDto?> CreateCategoryAsync(CategoryCreateRequest request)
|
| 117 |
{
|
| 118 |
var response = await _httpClient.PostAsJsonAsync("api/categories", request);
|
|
|
|
| 54 |
#endregion
|
| 55 |
|
| 56 |
#region Books
|
| 57 |
+
public async Task<IEnumerable<BookDto>> GetBooksAsync(int? categoryId = null)
|
| 58 |
{
|
| 59 |
+
var url = categoryId.HasValue ? $"api/books?categoryId={categoryId}" : "api/books";
|
| 60 |
+
return await _httpClient.GetFromJsonAsync<IEnumerable<BookDto>>(url) ?? Enumerable.Empty<BookDto>();
|
| 61 |
}
|
| 62 |
|
| 63 |
public async Task<BookDto?> GetBookAsync(int id)
|
|
|
|
| 114 |
return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories") ?? Enumerable.Empty<CategoryDto>();
|
| 115 |
}
|
| 116 |
|
| 117 |
+
public async Task<IEnumerable<CategoryDto>> GetCategoriesWithBooksAsync()
|
| 118 |
+
{
|
| 119 |
+
return await _httpClient.GetFromJsonAsync<IEnumerable<CategoryDto>>("api/categories/with-books") ?? Enumerable.Empty<CategoryDto>();
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
public async Task<CategoryDto?> CreateCategoryAsync(CategoryCreateRequest request)
|
| 123 |
{
|
| 124 |
var response = await _httpClient.PostAsJsonAsync("api/categories", request);
|
Frontend/Views/Books/Create.cshtml
CHANGED
|
@@ -67,6 +67,31 @@
|
|
| 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">
|
|
|
|
| 67 |
<span asp-validation-for="Description" class="mt-1 text-xs text-rose-500"></span>
|
| 68 |
</div>
|
| 69 |
</div>
|
| 70 |
+
|
| 71 |
+
<!-- Categories -->
|
| 72 |
+
<div class="sm:col-span-2">
|
| 73 |
+
<label class="block text-sm font-medium text-slate-700 mb-2">Categories</label>
|
| 74 |
+
@{
|
| 75 |
+
var allCategories = ViewBag.Categories as IEnumerable<Frontend.Models.Dtos.CategoryDto>;
|
| 76 |
+
}
|
| 77 |
+
@if (allCategories != null && allCategories.Any())
|
| 78 |
+
{
|
| 79 |
+
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
| 80 |
+
@foreach (var cat in allCategories)
|
| 81 |
+
{
|
| 82 |
+
<label class="flex items-center gap-2 px-3 py-2 rounded-md border border-slate-200 bg-slate-50 hover:bg-slate-100 cursor-pointer text-sm text-slate-700 transition-colors">
|
| 83 |
+
<input type="checkbox" name="CategoryIds" value="@cat.Id"
|
| 84 |
+
class="h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-900" />
|
| 85 |
+
@cat.Name
|
| 86 |
+
</label>
|
| 87 |
+
}
|
| 88 |
+
</div>
|
| 89 |
+
}
|
| 90 |
+
else
|
| 91 |
+
{
|
| 92 |
+
<p class="text-sm text-slate-400 italic">No categories available. Add some first.</p>
|
| 93 |
+
}
|
| 94 |
+
</div>
|
| 95 |
</div>
|
| 96 |
|
| 97 |
<div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
|
Frontend/Views/Books/Edit.cshtml
CHANGED
|
@@ -58,6 +58,33 @@
|
|
| 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">
|
|
|
|
| 58 |
<span asp-validation-for="Description" class="mt-1 text-xs text-rose-500"></span>
|
| 59 |
</div>
|
| 60 |
</div>
|
| 61 |
+
|
| 62 |
+
<!-- Categories -->
|
| 63 |
+
<div class="sm:col-span-2">
|
| 64 |
+
<label class="block text-sm font-medium text-slate-700 mb-2">Categories</label>
|
| 65 |
+
@{
|
| 66 |
+
var allCategories = ViewBag.Categories as IEnumerable<Frontend.Models.Dtos.CategoryDto>;
|
| 67 |
+
var selectedIds = Model.CategoryIds ?? new List<int>();
|
| 68 |
+
}
|
| 69 |
+
@if (allCategories != null && allCategories.Any())
|
| 70 |
+
{
|
| 71 |
+
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
| 72 |
+
@foreach (var cat in allCategories)
|
| 73 |
+
{
|
| 74 |
+
<label class="flex items-center gap-2 px-3 py-2 rounded-md border border-slate-200 bg-slate-50 hover:bg-slate-100 cursor-pointer text-sm text-slate-700 transition-colors">
|
| 75 |
+
<input type="checkbox" name="CategoryIds" value="@cat.Id"
|
| 76 |
+
class="h-4 w-4 rounded border-slate-300 text-slate-900 focus:ring-slate-900"
|
| 77 |
+
@(selectedIds.Contains(cat.Id) ? "checked" : "") />
|
| 78 |
+
@cat.Name
|
| 79 |
+
</label>
|
| 80 |
+
}
|
| 81 |
+
</div>
|
| 82 |
+
}
|
| 83 |
+
else
|
| 84 |
+
{
|
| 85 |
+
<p class="text-sm text-slate-400 italic">No categories available. Add some first.</p>
|
| 86 |
+
}
|
| 87 |
+
</div>
|
| 88 |
</div>
|
| 89 |
|
| 90 |
<div class="pt-5 border-t border-slate-100 flex justify-end gap-3">
|
Frontend/Views/Books/Index.cshtml
CHANGED
|
@@ -22,7 +22,7 @@
|
|
| 22 |
}
|
| 23 |
</div>
|
| 24 |
|
| 25 |
-
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-
|
| 26 |
<div class="md:col-span-3">
|
| 27 |
<div class="relative">
|
| 28 |
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
@@ -35,6 +35,30 @@
|
|
| 35 |
</div>
|
| 36 |
</div>
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
<!-- Book Grid -->
|
| 39 |
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 gap-8">
|
| 40 |
@foreach (var book in Model.Items)
|
|
@@ -62,7 +86,19 @@
|
|
| 62 |
<p class="text-xs text-slate-500 mt-0.5 truncate">@book.Author</p>
|
| 63 |
</div>
|
| 64 |
</div>
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
<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>
|
| 67 |
<span class="text-[10px] font-medium @(book.AvailableCopies > 0 ? "text-emerald-600" : "text-rose-600")">
|
| 68 |
@book.AvailableCopies / @book.TotalCopies
|
|
@@ -96,7 +132,7 @@
|
|
| 96 |
@* Previous *@
|
| 97 |
@if (Model.HasPreviousPage)
|
| 98 |
{
|
| 99 |
-
<a asp-action="Index" asp-route-page="@(Model.CurrentPage - 1)"
|
| 100 |
class="inline-flex items-center px-3 py-1.5 rounded-md border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors">
|
| 101 |
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 102 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
@@ -123,7 +159,7 @@
|
|
| 123 |
}
|
| 124 |
else if (p == 1 || p == Model.TotalPages || (p >= Model.CurrentPage - 2 && p <= Model.CurrentPage + 2))
|
| 125 |
{
|
| 126 |
-
<a asp-action="Index" asp-route-page="@p"
|
| 127 |
class="inline-flex items-center justify-center w-8 h-8 rounded-md border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors">@p</a>
|
| 128 |
}
|
| 129 |
else if (p == Model.CurrentPage - 3 || p == Model.CurrentPage + 3)
|
|
@@ -135,7 +171,7 @@
|
|
| 135 |
@* Next *@
|
| 136 |
@if (Model.HasNextPage)
|
| 137 |
{
|
| 138 |
-
<a asp-action="Index" asp-route-page="@(Model.CurrentPage + 1)"
|
| 139 |
class="inline-flex items-center px-3 py-1.5 rounded-md border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors">
|
| 140 |
Next
|
| 141 |
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
| 22 |
}
|
| 23 |
</div>
|
| 24 |
|
| 25 |
+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
| 26 |
<div class="md:col-span-3">
|
| 27 |
<div class="relative">
|
| 28 |
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
|
|
| 35 |
</div>
|
| 36 |
</div>
|
| 37 |
|
| 38 |
+
@* Category Filter Pills *@
|
| 39 |
+
@{
|
| 40 |
+
var categories = ViewBag.Categories as IEnumerable<Frontend.Models.Dtos.CategoryDto> ?? Enumerable.Empty<Frontend.Models.Dtos.CategoryDto>();
|
| 41 |
+
int? selectedCategoryId = ViewBag.SelectedCategoryId;
|
| 42 |
+
}
|
| 43 |
+
@if (categories.Any())
|
| 44 |
+
{
|
| 45 |
+
<div class="flex flex-wrap items-center gap-2 mb-8">
|
| 46 |
+
<span class="text-xs font-semibold text-slate-400 uppercase tracking-wider mr-1">Filter:</span>
|
| 47 |
+
<a asp-action="Index" asp-route-page="1"
|
| 48 |
+
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border transition-colors @(selectedCategoryId == null ? "bg-slate-900 text-white border-slate-900" : "bg-white text-slate-600 border-slate-200 hover:border-slate-400 hover:text-slate-900")">
|
| 49 |
+
All
|
| 50 |
+
</a>
|
| 51 |
+
@foreach (var cat in categories)
|
| 52 |
+
{
|
| 53 |
+
<a asp-action="Index" asp-route-page="1" asp-route-categoryId="@cat.Id"
|
| 54 |
+
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-semibold border transition-colors @(selectedCategoryId == cat.Id ? "bg-slate-900 text-white border-slate-900" : "bg-white text-slate-600 border-slate-200 hover:border-slate-400 hover:text-slate-900")">
|
| 55 |
+
@cat.Name
|
| 56 |
+
</a>
|
| 57 |
+
}
|
| 58 |
+
</div>
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
<!-- Book Grid -->
|
| 63 |
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 gap-8">
|
| 64 |
@foreach (var book in Model.Items)
|
|
|
|
| 86 |
<p class="text-xs text-slate-500 mt-0.5 truncate">@book.Author</p>
|
| 87 |
</div>
|
| 88 |
</div>
|
| 89 |
+
@if (book.Categories.Any())
|
| 90 |
+
{
|
| 91 |
+
<div class="mt-2 flex flex-wrap gap-1">
|
| 92 |
+
@foreach (var cat in book.Categories)
|
| 93 |
+
{
|
| 94 |
+
<a asp-action="Index" asp-route-page="1" asp-route-categoryId="@cat.Id"
|
| 95 |
+
class="inline-flex items-center rounded-full bg-amber-50 border border-amber-200 px-2 py-0.5 text-[9px] font-semibold text-amber-700 hover:bg-amber-100 transition-colors">
|
| 96 |
+
@cat.Name
|
| 97 |
+
</a>
|
| 98 |
+
}
|
| 99 |
+
</div>
|
| 100 |
+
}
|
| 101 |
+
<div class="mt-3 flex items-center justify-between">
|
| 102 |
<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>
|
| 103 |
<span class="text-[10px] font-medium @(book.AvailableCopies > 0 ? "text-emerald-600" : "text-rose-600")">
|
| 104 |
@book.AvailableCopies / @book.TotalCopies
|
|
|
|
| 132 |
@* Previous *@
|
| 133 |
@if (Model.HasPreviousPage)
|
| 134 |
{
|
| 135 |
+
<a asp-action="Index" asp-route-page="@(Model.CurrentPage - 1)" asp-route-categoryId="@selectedCategoryId"
|
| 136 |
class="inline-flex items-center px-3 py-1.5 rounded-md border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors">
|
| 137 |
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 138 |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
|
|
| 159 |
}
|
| 160 |
else if (p == 1 || p == Model.TotalPages || (p >= Model.CurrentPage - 2 && p <= Model.CurrentPage + 2))
|
| 161 |
{
|
| 162 |
+
<a asp-action="Index" asp-route-page="@p" asp-route-categoryId="@selectedCategoryId"
|
| 163 |
class="inline-flex items-center justify-center w-8 h-8 rounded-md border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors">@p</a>
|
| 164 |
}
|
| 165 |
else if (p == Model.CurrentPage - 3 || p == Model.CurrentPage + 3)
|
|
|
|
| 171 |
@* Next *@
|
| 172 |
@if (Model.HasNextPage)
|
| 173 |
{
|
| 174 |
+
<a asp-action="Index" asp-route-page="@(Model.CurrentPage + 1)" asp-route-categoryId="@selectedCategoryId"
|
| 175 |
class="inline-flex items-center px-3 py-1.5 rounded-md border border-slate-200 text-sm font-medium text-slate-600 hover:bg-slate-50 hover:text-slate-900 transition-colors">
|
| 176 |
Next
|
| 177 |
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|