维护入口

This commit is contained in:
Zzzz
2026-04-27 23:38:19 +08:00
parent 4155d5b1ea
commit 63738f108a
21 changed files with 1009 additions and 8 deletions
@@ -27,6 +27,8 @@ import javax.servlet.http.HttpSession;
public class BookManagementServlet extends HttpServlet {
private static final String MANAGE_JSP = "/WEB-INF/jsp/books/manage.jsp";
private static final String FORM_JSP = "/WEB-INF/jsp/books/form.jsp";
private static final String CATEGORY_MANAGE_JSP = "/WEB-INF/jsp/books/categories.jsp";
private static final String CATEGORY_FORM_JSP = "/WEB-INF/jsp/books/category-form.jsp";
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
private static final String FLASH_SUCCESS = "flashSuccess";
private static final String FLASH_ERROR = "flashError";
@@ -50,6 +52,19 @@ public class BookManagementServlet extends HttpServlet {
showEditForm(request, response);
return;
}
if ("/book-categories".equals(path)) {
showCategoryList(request, response);
return;
}
if ("/book-categories/new".equals(path)) {
renderCategoryForm(request, response, "Create category", "/book-categories", new BookCategory(),
Collections.emptyMap(), Collections.emptyMap(), null);
return;
}
if ("/book-categories/edit".equals(path)) {
showEditCategoryForm(request, response);
return;
}
if (!"/books".equals(path)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
@@ -73,6 +88,18 @@ public class BookManagementServlet extends HttpServlet {
deleteBook(request, response);
return;
}
if ("/book-categories".equals(path)) {
createCategory(request, response);
return;
}
if ("/book-categories/update".equals(path)) {
updateCategory(request, response);
return;
}
if ("/book-categories/delete".equals(path)) {
deleteCategory(request, response);
return;
}
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
@@ -116,6 +143,32 @@ public class BookManagementServlet extends HttpServlet {
Collections.emptyMap(), Collections.emptyMap(), null);
}
private void showCategoryList(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
applyFlash(request);
ServiceResult<List<BookCategory>> result = bookService.listCategories();
request.setAttribute("categories", result.isSuccessful() ? result.getData() : Collections.emptyList());
if (!result.isSuccessful()) {
request.setAttribute("errorMessage", result.getMessage());
}
request.getRequestDispatcher(CATEGORY_MANAGE_JSP).forward(request, response);
}
private void showEditCategoryForm(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long id = requiredLong(request.getParameter("id"), -1L);
ServiceResult<Optional<BookCategory>> result = bookService.findCategory(id);
if (!result.isSuccessful() || !result.getData().isPresent()) {
flashError(request, result.isSuccessful() ? "Category was not found." : result.getMessage());
response.sendRedirect(request.getContextPath() + "/book-categories");
return;
}
renderCategoryForm(request, response, "Edit category", "/book-categories/update", result.getData().get(),
Collections.emptyMap(), Collections.emptyMap(), null);
}
private void createBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
BookForm form = readBookForm(request, false);
if (!form.getErrors().isEmpty()) {
@@ -167,6 +220,60 @@ public class BookManagementServlet extends HttpServlet {
response.sendRedirect(request.getContextPath() + "/books");
}
private void createCategory(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
CategoryForm form = readCategoryForm(request, false);
if (!form.getErrors().isEmpty()) {
renderCategoryForm(request, response, "Create category", "/book-categories", form.getCategory(),
form.getValues(), form.getErrors(), "Please correct the highlighted category fields.");
return;
}
ServiceResult<Long> result = bookService.createCategory(currentUser(request), form.getCategory());
if (!result.isSuccessful()) {
handleCategoryFormFailure(request, response, "Create category", "/book-categories", form, result);
return;
}
flashSuccess(request, result.getMessage());
response.sendRedirect(request.getContextPath() + "/book-categories");
}
private void updateCategory(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
CategoryForm form = readCategoryForm(request, true);
if (!form.getErrors().isEmpty()) {
renderCategoryForm(request, response, "Edit category", "/book-categories/update", form.getCategory(),
form.getValues(), form.getErrors(), "Please correct the highlighted category fields.");
return;
}
ServiceResult<Void> result = bookService.updateCategory(currentUser(request), form.getCategory());
if (!result.isSuccessful()) {
handleCategoryFormFailure(request, response, "Edit category", "/book-categories/update", form, result);
return;
}
flashSuccess(request, result.getMessage());
response.sendRedirect(request.getContextPath() + "/book-categories");
}
private void deleteCategory(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
long id = requiredLong(request.getParameter("id"), -1L);
ServiceResult<Void> result = bookService.deleteCategory(currentUser(request), id);
if (isPermissionDenied(result)) {
forwardDenied(request, response, result.getMessage());
return;
}
if (result.isSuccessful()) {
flashSuccess(request, result.getMessage());
} else {
flashError(request, result.getMessage());
}
response.sendRedirect(request.getContextPath() + "/book-categories");
}
private void handleFormFailure(HttpServletRequest request, HttpServletResponse response, String title,
String action, BookForm form, ServiceResult<?> result)
throws ServletException, IOException {
@@ -178,6 +285,17 @@ public class BookManagementServlet extends HttpServlet {
result.getMessage());
}
private void handleCategoryFormFailure(HttpServletRequest request, HttpServletResponse response, String title,
String action, CategoryForm form, ServiceResult<?> result)
throws ServletException, IOException {
if (isPermissionDenied(result)) {
forwardDenied(request, response, result.getMessage());
return;
}
renderCategoryForm(request, response, title, action, form.getCategory(), form.getValues(),
result.getErrors(), result.getMessage());
}
private void renderForm(HttpServletRequest request, HttpServletResponse response, String title, String action,
Book book, Map<String, String> formValues, Map<String, String> errors, String errorMessage)
throws ServletException, IOException {
@@ -199,6 +317,21 @@ public class BookManagementServlet extends HttpServlet {
request.getRequestDispatcher(FORM_JSP).forward(request, response);
}
private void renderCategoryForm(HttpServletRequest request, HttpServletResponse response, String title,
String action, BookCategory category, Map<String, String> formValues,
Map<String, String> errors, String errorMessage)
throws ServletException, IOException {
request.setAttribute("formTitle", title);
request.setAttribute("formAction", action);
request.setAttribute("category", category);
request.setAttribute("formValues", formValues);
request.setAttribute("errors", errors);
if (errorMessage != null && !errorMessage.isEmpty()) {
request.setAttribute("errorMessage", errorMessage);
}
request.getRequestDispatcher(CATEGORY_FORM_JSP).forward(request, response);
}
private BookForm readBookForm(HttpServletRequest request, boolean requireId) {
Map<String, String> values = formValues(request);
Map<String, String> errors = new LinkedHashMap<>();
@@ -237,6 +370,27 @@ public class BookManagementServlet extends HttpServlet {
return values;
}
private CategoryForm readCategoryForm(HttpServletRequest request, boolean requireId) {
Map<String, String> values = categoryFormValues(request);
Map<String, String> errors = new LinkedHashMap<>();
BookCategory category = new BookCategory();
if (requireId) {
category.setId(parseLong(values.get("id"), "id", "Select a valid category.", errors));
}
category.setName(values.get("name"));
category.setDescription(values.get("description"));
return new CategoryForm(category, values, errors);
}
private Map<String, String> categoryFormValues(HttpServletRequest request) {
Map<String, String> values = new LinkedHashMap<>();
values.put("id", trim(request.getParameter("id")));
values.put("name", trim(request.getParameter("name")));
values.put("description", trim(request.getParameter("description")));
return values;
}
private BookSearchCriteria searchCriteria(HttpServletRequest request) {
return new BookSearchCriteria(
request.getParameter("identifier"),
@@ -368,4 +522,28 @@ public class BookManagementServlet extends HttpServlet {
return errors;
}
}
private static final class CategoryForm {
private final BookCategory category;
private final Map<String, String> values;
private final Map<String, String> errors;
private CategoryForm(BookCategory category, Map<String, String> values, Map<String, String> errors) {
this.category = category;
this.values = values;
this.errors = errors;
}
private BookCategory getCategory() {
return category;
}
private Map<String, String> getValues() {
return values;
}
private Map<String, String> getErrors() {
return errors;
}
}
}
@@ -10,6 +10,18 @@ import java.util.Optional;
public interface BookDao {
List<BookCategory> findAllCategories();
Optional<BookCategory> findCategoryById(long id);
Optional<BookCategory> findCategoryByName(String name);
long createCategory(BookCategory category);
boolean updateCategory(BookCategory category);
boolean deleteCategory(long id);
int countBooksByCategoryId(long categoryId);
List<Book> search(BookSearchCriteria criteria);
Optional<Book> findById(long id);
@@ -33,6 +33,32 @@ public class JdbcBookDao implements BookDao {
+ "FROM book_categories "
+ "ORDER BY name";
private static final String FIND_CATEGORY_BY_ID = ""
+ "SELECT id, name, description "
+ "FROM book_categories "
+ "WHERE id = ?";
private static final String FIND_CATEGORY_BY_NAME = ""
+ "SELECT id, name, description "
+ "FROM book_categories "
+ "WHERE name = ?";
private static final String CREATE_CATEGORY = ""
+ "INSERT INTO book_categories (name, description) "
+ "VALUES (?, ?)";
private static final String UPDATE_CATEGORY = ""
+ "UPDATE book_categories "
+ "SET name = ?, description = ? "
+ "WHERE id = ?";
private static final String DELETE_CATEGORY = "DELETE FROM book_categories WHERE id = ?";
private static final String COUNT_BOOKS_BY_CATEGORY = ""
+ "SELECT COUNT(*) "
+ "FROM books "
+ "WHERE category_id = ?";
private static final String FIND_BY_ID = "SELECT " + BOOK_COLUMNS + BOOK_FROM + "WHERE b.id = ?";
private static final String FIND_BY_IDENTIFIER = "SELECT " + BOOK_COLUMNS + BOOK_FROM
@@ -66,6 +92,86 @@ public class JdbcBookDao implements BookDao {
}
}
@Override
public Optional<BookCategory> findCategoryById(long id) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(FIND_CATEGORY_BY_ID)) {
statement.setLong(1, id);
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next() ? Optional.of(mapCategory(resultSet)) : Optional.empty();
}
} catch (SQLException ex) {
throw new DaoException("Unable to load book category by id", ex);
}
}
@Override
public Optional<BookCategory> findCategoryByName(String name) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(FIND_CATEGORY_BY_NAME)) {
statement.setString(1, name);
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next() ? Optional.of(mapCategory(resultSet)) : Optional.empty();
}
} catch (SQLException ex) {
throw new DaoException("Unable to load book category by name", ex);
}
}
@Override
public long createCategory(BookCategory category) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(CREATE_CATEGORY, Statement.RETURN_GENERATED_KEYS)) {
bindCategory(statement, category);
statement.executeUpdate();
try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
if (generatedKeys.next()) {
return generatedKeys.getLong(1);
}
}
throw new DaoException("Unable to read generated book category id", null);
} catch (SQLException ex) {
throw new DaoException("Unable to create book category", ex);
}
}
@Override
public boolean updateCategory(BookCategory category) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(UPDATE_CATEGORY)) {
bindCategory(statement, category);
statement.setLong(3, category.getId());
return statement.executeUpdate() == 1;
} catch (SQLException ex) {
throw new DaoException("Unable to update book category", ex);
}
}
@Override
public boolean deleteCategory(long id) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(DELETE_CATEGORY)) {
statement.setLong(1, id);
return statement.executeUpdate() == 1;
} catch (SQLException ex) {
throw new DaoException("Unable to delete book category", ex);
}
}
@Override
public int countBooksByCategoryId(long categoryId) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(COUNT_BOOKS_BY_CATEGORY)) {
statement.setLong(1, categoryId);
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next() ? resultSet.getInt(1) : 0;
}
} catch (SQLException ex) {
throw new DaoException("Unable to count books by category", ex);
}
}
@Override
public List<Book> search(BookSearchCriteria criteria) {
List<Object> parameters = new ArrayList<>();
@@ -194,6 +300,11 @@ public class JdbcBookDao implements BookDao {
statement.setString(7, book.getStatus().getCode());
}
private void bindCategory(PreparedStatement statement, BookCategory category) throws SQLException {
statement.setString(1, category.getName());
statement.setString(2, category.getDescription());
}
private Book mapBook(ResultSet resultSet) throws SQLException {
Book book = new Book();
book.setId(resultSet.getLong("id"));
@@ -29,6 +29,7 @@ public class AuthorizationFilter implements Filter {
new PathRule("/admin/system-logs", Permission.VIEW_SYSTEM_LOGS),
new PathRule("/reports", Permission.VIEW_REPORTS),
new PathRule("/borrowing", Permission.MANAGE_BORROWING),
new PathRule("/book-categories", Permission.MANAGE_BOOKS),
new PathRule("/books", Permission.MANAGE_BOOKS),
new PathRule("/readers", Permission.MANAGE_READERS),
new PathRule("/catalog", Permission.VIEW_CATALOG),
@@ -11,6 +11,14 @@ import java.util.Optional;
public interface BookService {
ServiceResult<List<BookCategory>> listCategories();
ServiceResult<Optional<BookCategory>> findCategory(long id);
ServiceResult<Long> createCategory(AuthenticatedUser actor, BookCategory category);
ServiceResult<Void> updateCategory(AuthenticatedUser actor, BookCategory category);
ServiceResult<Void> deleteCategory(AuthenticatedUser actor, long id);
ServiceResult<List<Book>> searchBooks(BookSearchCriteria criteria);
ServiceResult<Optional<Book>> findBook(long id);
@@ -23,6 +23,7 @@ public class BookServiceImpl implements BookService {
private static final String UNAVAILABLE_MESSAGE =
"Book service is temporarily unavailable. Please try again later.";
private static final String VALIDATION_MESSAGE = "Please correct the highlighted book fields.";
private static final String CATEGORY_VALIDATION_MESSAGE = "Please correct the highlighted category fields.";
private static final String DENIED_MESSAGE = "You do not have permission to manage books.";
private final BookDao bookDao;
@@ -47,6 +48,111 @@ public class BookServiceImpl implements BookService {
}
}
@Override
public ServiceResult<Optional<BookCategory>> findCategory(long id) {
if (id <= 0) {
return ServiceResult.failure("Select a valid category.");
}
try {
return ServiceResult.success(bookDao.findCategoryById(id));
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to load book category id=" + id, ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<Long> createCategory(AuthenticatedUser actor, BookCategory category) {
if (!canManageBooks(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
normalize(category);
Map<String, String> errors = validate(category, false);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure(CATEGORY_VALIDATION_MESSAGE, errors);
}
try {
if (bookDao.findCategoryByName(category.getName()).isPresent()) {
errors.put("name", "Category name is already in use.");
return ServiceResult.validationFailure(CATEGORY_VALIDATION_MESSAGE, errors);
}
long id = bookDao.createCategory(category);
LOGGER.info("Created book category id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(id, "Category created.");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to create book category actorId=" + actor.getId()
+ " name=" + safeCategoryName(category), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<Void> updateCategory(AuthenticatedUser actor, BookCategory category) {
if (!canManageBooks(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
normalize(category);
Map<String, String> errors = validate(category, true);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure(CATEGORY_VALIDATION_MESSAGE, errors);
}
try {
Optional<BookCategory> existingWithName = bookDao.findCategoryByName(category.getName());
if (existingWithName.isPresent() && existingWithName.get().getId() != category.getId()) {
errors.put("name", "Category name is already in use.");
return ServiceResult.validationFailure(CATEGORY_VALIDATION_MESSAGE, errors);
}
if (!bookDao.updateCategory(category)) {
return ServiceResult.failure("Category was not found.");
}
LOGGER.info("Updated book category id=" + category.getId() + " actorId=" + actor.getId());
return ServiceResult.success(null, "Category updated.");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to update book category id=" + category.getId()
+ " actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<Void> deleteCategory(AuthenticatedUser actor, long id) {
if (!canManageBooks(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
if (id <= 0) {
return ServiceResult.failure("Select a valid category.");
}
try {
if (!bookDao.findCategoryById(id).isPresent()) {
return ServiceResult.failure("Category was not found.");
}
if (bookDao.countBooksByCategoryId(id) > 0) {
Map<String, String> errors = new LinkedHashMap<>();
errors.put("category", "Category is used by existing books and cannot be deleted.");
return ServiceResult.validationFailure("Category is used by existing books and cannot be deleted.",
errors);
}
if (!bookDao.deleteCategory(id)) {
return ServiceResult.failure("Category was not found.");
}
LOGGER.info("Deleted book category id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(null, "Category deleted.");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to delete book category id=" + id + " actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<List<Book>> searchBooks(BookSearchCriteria criteria) {
BookSearchCriteria normalized = criteria == null ? new BookSearchCriteria() : criteria;
@@ -171,6 +277,14 @@ public class BookServiceImpl implements BookService {
book.setAuthor(trim(book.getAuthor()));
}
private void normalize(BookCategory category) {
if (category == null) {
return;
}
category.setName(trim(category.getName()));
category.setDescription(trim(category.getDescription()));
}
private Map<String, String> validate(Book book, boolean requireId) {
Map<String, String> errors = new LinkedHashMap<>();
if (book == null) {
@@ -202,6 +316,27 @@ public class BookServiceImpl implements BookService {
return errors;
}
private Map<String, String> validate(BookCategory category, boolean requireId) {
Map<String, String> errors = new LinkedHashMap<>();
if (category == null) {
errors.put("category", "Category details are required.");
return errors;
}
if (requireId && category.getId() <= 0) {
errors.put("id", "Select a valid category.");
}
requireLength(errors, "name", category.getName(), "Category name", 96);
if (category.getDescription() != null && category.getDescription().length() > 255) {
errors.put("description", "Description must be 255 characters or fewer.");
}
return errors;
}
private String safeCategoryName(BookCategory category) {
return category == null ? "" : category.getName();
}
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
if (value == null || value.isEmpty()) {
errors.put(field, label + " is required.");
@@ -0,0 +1,89 @@
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Manage Categories - MZH Library</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="category-title">
<div>
<p class="eyebrow">Book Management</p>
<h1 id="category-title">Manage categories</h1>
<p>Maintain catalog groupings used by book records and search filters.</p>
</div>
<div class="hero-actions">
<a class="button button-primary" href="${pageContext.request.contextPath}/book-categories/new">New category</a>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Manage books</a>
</div>
</section>
<c:if test="${not empty successMessage}">
<div class="message message-success" role="status">
<c:out value="${successMessage}" />
</div>
</c:if>
<c:if test="${not empty errorMessage}">
<div class="message message-error" role="alert">
<c:out value="${errorMessage}" />
</div>
</c:if>
<section class="table-panel" aria-labelledby="category-results-title">
<h2 id="category-results-title">Category records</h2>
<c:choose>
<c:when test="${empty categories}">
<p class="empty-state">No categories have been created yet.</p>
</c:when>
<c:otherwise>
<div class="table-scroll">
<table class="data-table category-table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Description</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<c:forEach var="category" items="${categories}">
<tr>
<td><c:out value="${category.name}" /></td>
<td>
<c:choose>
<c:when test="${empty category.description}">
<span class="muted-text">No description</span>
</c:when>
<c:otherwise>
<c:out value="${category.description}" />
</c:otherwise>
</c:choose>
</td>
<td>
<div class="table-actions">
<a class="button button-secondary"
href="${pageContext.request.contextPath}/book-categories/edit?id=${category.id}">Edit</a>
<form action="${pageContext.request.contextPath}/book-categories/delete"
method="post"
onsubmit="return confirm('Delete this category?');">
<input type="hidden" name="id" value="${category.id}">
<button class="button button-danger" type="submit">Delete</button>
</form>
</div>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</c:otherwise>
</c:choose>
</section>
</main>
</body>
</html>
@@ -0,0 +1,66 @@
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><c:out value="${formTitle}" /> - MZH Library</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="form-panel" aria-labelledby="category-form-title">
<p class="eyebrow">Book Management</p>
<h1 id="category-form-title"><c:out value="${formTitle}" /></h1>
<c:if test="${not empty errorMessage}">
<div class="message message-error" role="alert">
<c:out value="${errorMessage}" />
</div>
</c:if>
<c:set var="hasFormValues" value="${not empty formValues}" />
<c:set var="nameValue" value="${hasFormValues ? formValues.name : category.name}" />
<c:set var="descriptionValue" value="${hasFormValues ? formValues.description : category.description}" />
<form class="category-form" action="${pageContext.request.contextPath}${formAction}" method="post" novalidate>
<c:if test="${category.id > 0}">
<input type="hidden" name="id" value="${category.id}">
</c:if>
<div class="form-grid">
<div class="form-field">
<label for="name">Category name</label>
<input id="name" name="name" type="text" value="${fn:escapeXml(nameValue)}" required>
<c:if test="${not empty errors.name}">
<span class="field-error"><c:out value="${errors.name}" /></span>
</c:if>
</div>
<div class="form-field form-field-wide">
<label for="description">Description</label>
<textarea id="description" name="description" rows="4">${fn:escapeXml(descriptionValue)}</textarea>
<c:if test="${not empty errors.description}">
<span class="field-error"><c:out value="${errors.description}" /></span>
</c:if>
</div>
</div>
<c:if test="${not empty errors.category}">
<div class="message message-error" role="alert">
<c:out value="${errors.category}" />
</div>
</c:if>
<div class="form-actions">
<button class="button button-primary" type="submit">Save</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">Cancel</a>
</div>
</form>
</section>
</main>
</body>
</html>
+5 -1
View File
@@ -16,7 +16,10 @@
<p class="eyebrow">Book Management</p>
<h1 id="manage-title">Manage books</h1>
<p>Create, update, delete, and review inventory for catalog records.</p>
<a class="button button-primary" href="${pageContext.request.contextPath}/books/new">New book</a>
<div class="hero-actions">
<a class="button button-primary" href="${pageContext.request.contextPath}/books/new">New book</a>
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">Categories</a>
</div>
</section>
<c:if test="${not empty successMessage}">
@@ -65,6 +68,7 @@
<button class="button button-primary" type="submit">Search</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Clear</a>
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">View catalog</a>
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">Categories</a>
</form>
</section>
@@ -13,6 +13,7 @@
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
<a href="${pageContext.request.contextPath}/librarian/home">Librarian</a>
<a href="${pageContext.request.contextPath}/books">Books</a>
<a href="${pageContext.request.contextPath}/book-categories">Categories</a>
<a href="${pageContext.request.contextPath}/readers">Readers</a>
<a href="${pageContext.request.contextPath}/borrowing">Borrowing</a>
<a href="${pageContext.request.contextPath}/reports">Reports</a>
@@ -53,6 +53,12 @@
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Open</a>
</article>
<article class="workspace-card">
<h2>Category Maintenance</h2>
<p>Maintain catalog categories used by book records and search filters.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">Open</a>
</article>
<article class="workspace-card">
<h2>Reader Management</h2>
<p>Create, update, deactivate, and review reader eligibility records.</p>
@@ -48,6 +48,12 @@
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Manage books</a>
</article>
<article class="workspace-card">
<h2>Category Maintenance</h2>
<p>Create, update, and retire catalog categories used by book records.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">Manage categories</a>
</article>
<article class="workspace-card">
<h2>Reader Management</h2>
<p>Create, update, deactivate, and review eligibility fields for reader records.</p>
+5
View File
@@ -117,6 +117,11 @@
<url-pattern>/books/edit</url-pattern>
<url-pattern>/books/update</url-pattern>
<url-pattern>/books/delete</url-pattern>
<url-pattern>/book-categories</url-pattern>
<url-pattern>/book-categories/new</url-pattern>
<url-pattern>/book-categories/edit</url-pattern>
<url-pattern>/book-categories/update</url-pattern>
<url-pattern>/book-categories/delete</url-pattern>
</servlet-mapping>
<servlet>
+22 -1
View File
@@ -313,6 +313,12 @@ h2 {
margin-top: 0;
}
.hero-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.toolbar-panel,
.table-panel,
.form-panel {
@@ -350,6 +356,8 @@ h2 {
.search-form select,
.book-form input,
.book-form select,
.category-form input,
.category-form textarea,
.reader-form input,
.reader-form select,
.user-form input,
@@ -368,6 +376,8 @@ h2 {
.search-form select:focus,
.book-form input:focus,
.book-form select:focus,
.category-form input:focus,
.category-form textarea:focus,
.reader-form input:focus,
.reader-form select:focus,
.user-form input:focus,
@@ -394,7 +404,8 @@ h2 {
}
.user-table,
.system-log-table {
.system-log-table,
.category-table {
min-width: 980px;
}
@@ -504,6 +515,7 @@ h2 {
}
.book-form,
.category-form,
.reader-form,
.user-form,
.borrow-form {
@@ -522,6 +534,15 @@ h2 {
gap: 6px;
}
.form-field-wide {
grid-column: 1 / -1;
}
.category-form textarea {
min-height: 112px;
resize: vertical;
}
.form-field label {
color: var(--color-muted);
font-size: 14px;