完成报表中心
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package com.mzh.library.controller;
|
||||
|
||||
import com.mzh.library.dao.impl.JdbcReportDao;
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.ReportCenter;
|
||||
import com.mzh.library.service.ReportService;
|
||||
import com.mzh.library.service.ServiceResult;
|
||||
import com.mzh.library.service.impl.ReportServiceImpl;
|
||||
import com.mzh.library.util.SessionAttributes;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
public class ReportServlet extends HttpServlet {
|
||||
private static final String REPORT_JSP = "/WEB-INF/jsp/reports/dashboard.jsp";
|
||||
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
||||
|
||||
private ReportService reportService;
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
this.reportService = new ReportServiceImpl(new JdbcReportDao());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
ServiceResult<ReportCenter> result = reportService.loadReportCenter(currentUser(request));
|
||||
if (isPermissionDenied(result)) {
|
||||
forwardDenied(request, response, result.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.isSuccessful()) {
|
||||
request.setAttribute("reportCenter", result.getData());
|
||||
} else {
|
||||
request.setAttribute("errorMessage", result.getMessage());
|
||||
}
|
||||
|
||||
request.getRequestDispatcher(REPORT_JSP).forward(request, response);
|
||||
}
|
||||
|
||||
private boolean isPermissionDenied(ServiceResult<?> result) {
|
||||
return !result.isSuccessful()
|
||||
&& "You do not have permission to view reports.".equals(result.getMessage());
|
||||
}
|
||||
|
||||
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
||||
throws ServletException, IOException {
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
request.setAttribute("errorMessage", message);
|
||||
request.getRequestDispatcher(UNAUTHORIZED_JSP).forward(request, response);
|
||||
}
|
||||
|
||||
private AuthenticatedUser currentUser(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
Object value = session == null ? null : session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
|
||||
return value instanceof AuthenticatedUser ? (AuthenticatedUser) value : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.mzh.library.dao;
|
||||
|
||||
import com.mzh.library.entity.BorrowingSummary;
|
||||
import com.mzh.library.entity.InventorySummary;
|
||||
import com.mzh.library.entity.OverdueReportRow;
|
||||
import com.mzh.library.entity.PopularBookReportRow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ReportDao {
|
||||
InventorySummary loadInventorySummary();
|
||||
|
||||
BorrowingSummary loadBorrowingSummary();
|
||||
|
||||
List<OverdueReportRow> findOverdueRows();
|
||||
|
||||
List<PopularBookReportRow> findPopularBooks(int limit);
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.mzh.library.dao.impl;
|
||||
|
||||
import com.mzh.library.dao.ReportDao;
|
||||
import com.mzh.library.entity.BookStatus;
|
||||
import com.mzh.library.entity.BorrowRecordStatus;
|
||||
import com.mzh.library.entity.BorrowingSummary;
|
||||
import com.mzh.library.entity.InventorySummary;
|
||||
import com.mzh.library.entity.OverdueReportRow;
|
||||
import com.mzh.library.entity.PopularBookReportRow;
|
||||
import com.mzh.library.exception.DaoException;
|
||||
import com.mzh.library.util.JdbcUtil;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class JdbcReportDao implements ReportDao {
|
||||
private static final String INVENTORY_SUMMARY = ""
|
||||
+ "SELECT COUNT(*) AS total_titles, "
|
||||
+ "COALESCE(SUM(total_copies), 0) AS total_copies, "
|
||||
+ "COALESCE(SUM(available_copies), 0) AS available_copies, "
|
||||
+ "COALESCE(SUM(CASE WHEN status <> ? OR available_copies <= 0 THEN 1 ELSE 0 END), 0) "
|
||||
+ "AS unavailable_or_empty_titles "
|
||||
+ "FROM books";
|
||||
|
||||
private static final String BORROWING_SUMMARY = ""
|
||||
+ "SELECT "
|
||||
+ "COALESCE(SUM(CASE WHEN status = ? AND returned_at IS NULL THEN 1 ELSE 0 END), 0) AS active_loans, "
|
||||
+ "COALESCE(SUM(CASE WHEN status = ? THEN 1 ELSE 0 END), 0) AS returned_loans, "
|
||||
+ "COALESCE(SUM(CASE WHEN status = ? AND returned_at IS NULL AND due_at < CURRENT_TIMESTAMP "
|
||||
+ "THEN 1 ELSE 0 END), 0) AS overdue_loans "
|
||||
+ "FROM borrow_records";
|
||||
|
||||
private static final String OVERDUE_ROWS = ""
|
||||
+ "SELECT r.reader_identifier, r.full_name AS reader_name, "
|
||||
+ "b.book_identifier, b.title AS book_title, br.due_at, "
|
||||
+ "GREATEST(DATEDIFF(CURRENT_DATE, DATE(br.due_at)), 0) AS overdue_days "
|
||||
+ "FROM borrow_records br "
|
||||
+ "JOIN readers r ON r.id = br.reader_id "
|
||||
+ "JOIN books b ON b.id = br.book_id "
|
||||
+ "WHERE br.status = ? AND br.returned_at IS NULL AND br.due_at < CURRENT_TIMESTAMP "
|
||||
+ "ORDER BY br.due_at, r.reader_identifier, b.book_identifier";
|
||||
|
||||
private static final String POPULAR_BOOKS = ""
|
||||
+ "SELECT b.book_identifier, b.title, b.author, COUNT(br.id) AS borrow_count "
|
||||
+ "FROM books b "
|
||||
+ "JOIN borrow_records br ON br.book_id = b.id "
|
||||
+ "GROUP BY b.id, b.book_identifier, b.title, b.author "
|
||||
+ "ORDER BY borrow_count DESC, b.title, b.book_identifier "
|
||||
+ "LIMIT ?";
|
||||
|
||||
@Override
|
||||
public InventorySummary loadInventorySummary() {
|
||||
try (Connection connection = JdbcUtil.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(INVENTORY_SUMMARY)) {
|
||||
statement.setString(1, BookStatus.AVAILABLE.getCode());
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
if (resultSet.next()) {
|
||||
InventorySummary summary = new InventorySummary();
|
||||
summary.setTotalTitles(resultSet.getInt("total_titles"));
|
||||
summary.setTotalCopies(resultSet.getInt("total_copies"));
|
||||
summary.setAvailableCopies(resultSet.getInt("available_copies"));
|
||||
summary.setUnavailableOrEmptyTitles(resultSet.getInt("unavailable_or_empty_titles"));
|
||||
return summary;
|
||||
}
|
||||
return new InventorySummary();
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new DaoException("Unable to load inventory report summary", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BorrowingSummary loadBorrowingSummary() {
|
||||
try (Connection connection = JdbcUtil.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(BORROWING_SUMMARY)) {
|
||||
statement.setString(1, BorrowRecordStatus.ACTIVE.getCode());
|
||||
statement.setString(2, BorrowRecordStatus.RETURNED.getCode());
|
||||
statement.setString(3, BorrowRecordStatus.ACTIVE.getCode());
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
if (resultSet.next()) {
|
||||
BorrowingSummary summary = new BorrowingSummary();
|
||||
summary.setActiveLoans(resultSet.getInt("active_loans"));
|
||||
summary.setReturnedLoans(resultSet.getInt("returned_loans"));
|
||||
summary.setOverdueLoans(resultSet.getInt("overdue_loans"));
|
||||
return summary;
|
||||
}
|
||||
return new BorrowingSummary();
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new DaoException("Unable to load borrowing report summary", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OverdueReportRow> findOverdueRows() {
|
||||
try (Connection connection = JdbcUtil.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(OVERDUE_ROWS)) {
|
||||
statement.setString(1, BorrowRecordStatus.ACTIVE.getCode());
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
List<OverdueReportRow> rows = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
rows.add(mapOverdueRow(resultSet));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new DaoException("Unable to load overdue report rows", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PopularBookReportRow> findPopularBooks(int limit) {
|
||||
try (Connection connection = JdbcUtil.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(POPULAR_BOOKS)) {
|
||||
statement.setInt(1, limit);
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
List<PopularBookReportRow> rows = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
rows.add(mapPopularBook(resultSet));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
} catch (SQLException ex) {
|
||||
throw new DaoException("Unable to load popular book report rows", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private OverdueReportRow mapOverdueRow(ResultSet resultSet) throws SQLException {
|
||||
OverdueReportRow row = new OverdueReportRow();
|
||||
row.setReaderIdentifier(resultSet.getString("reader_identifier"));
|
||||
row.setReaderName(resultSet.getString("reader_name"));
|
||||
row.setBookIdentifier(resultSet.getString("book_identifier"));
|
||||
row.setBookTitle(resultSet.getString("book_title"));
|
||||
row.setDueAt(toLocalDateTime(resultSet.getTimestamp("due_at")));
|
||||
row.setOverdueDays(resultSet.getLong("overdue_days"));
|
||||
return row;
|
||||
}
|
||||
|
||||
private PopularBookReportRow mapPopularBook(ResultSet resultSet) throws SQLException {
|
||||
PopularBookReportRow row = new PopularBookReportRow();
|
||||
row.setBookIdentifier(resultSet.getString("book_identifier"));
|
||||
row.setTitle(resultSet.getString("title"));
|
||||
row.setAuthor(resultSet.getString("author"));
|
||||
row.setBorrowCount(resultSet.getInt("borrow_count"));
|
||||
return row;
|
||||
}
|
||||
|
||||
private LocalDateTime toLocalDateTime(Timestamp timestamp) {
|
||||
return timestamp == null ? null : timestamp.toLocalDateTime();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.mzh.library.entity;
|
||||
|
||||
public class BorrowingSummary {
|
||||
private int activeLoans;
|
||||
private int returnedLoans;
|
||||
private int overdueLoans;
|
||||
|
||||
public int getActiveLoans() {
|
||||
return activeLoans;
|
||||
}
|
||||
|
||||
public void setActiveLoans(int activeLoans) {
|
||||
this.activeLoans = activeLoans;
|
||||
}
|
||||
|
||||
public int getReturnedLoans() {
|
||||
return returnedLoans;
|
||||
}
|
||||
|
||||
public void setReturnedLoans(int returnedLoans) {
|
||||
this.returnedLoans = returnedLoans;
|
||||
}
|
||||
|
||||
public int getOverdueLoans() {
|
||||
return overdueLoans;
|
||||
}
|
||||
|
||||
public void setOverdueLoans(int overdueLoans) {
|
||||
this.overdueLoans = overdueLoans;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.mzh.library.entity;
|
||||
|
||||
public class InventorySummary {
|
||||
private int totalTitles;
|
||||
private int totalCopies;
|
||||
private int availableCopies;
|
||||
private int unavailableOrEmptyTitles;
|
||||
|
||||
public int getTotalTitles() {
|
||||
return totalTitles;
|
||||
}
|
||||
|
||||
public void setTotalTitles(int totalTitles) {
|
||||
this.totalTitles = totalTitles;
|
||||
}
|
||||
|
||||
public int getTotalCopies() {
|
||||
return totalCopies;
|
||||
}
|
||||
|
||||
public void setTotalCopies(int totalCopies) {
|
||||
this.totalCopies = totalCopies;
|
||||
}
|
||||
|
||||
public int getAvailableCopies() {
|
||||
return availableCopies;
|
||||
}
|
||||
|
||||
public void setAvailableCopies(int availableCopies) {
|
||||
this.availableCopies = availableCopies;
|
||||
}
|
||||
|
||||
public int getUnavailableOrEmptyTitles() {
|
||||
return unavailableOrEmptyTitles;
|
||||
}
|
||||
|
||||
public void setUnavailableOrEmptyTitles(int unavailableOrEmptyTitles) {
|
||||
this.unavailableOrEmptyTitles = unavailableOrEmptyTitles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.mzh.library.entity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class OverdueReportRow {
|
||||
private static final DateTimeFormatter DISPLAY_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||
|
||||
private String readerIdentifier;
|
||||
private String readerName;
|
||||
private String bookIdentifier;
|
||||
private String bookTitle;
|
||||
private LocalDateTime dueAt;
|
||||
private long overdueDays;
|
||||
|
||||
public String getReaderIdentifier() {
|
||||
return readerIdentifier;
|
||||
}
|
||||
|
||||
public void setReaderIdentifier(String readerIdentifier) {
|
||||
this.readerIdentifier = readerIdentifier;
|
||||
}
|
||||
|
||||
public String getReaderName() {
|
||||
return readerName;
|
||||
}
|
||||
|
||||
public void setReaderName(String readerName) {
|
||||
this.readerName = readerName;
|
||||
}
|
||||
|
||||
public String getBookIdentifier() {
|
||||
return bookIdentifier;
|
||||
}
|
||||
|
||||
public void setBookIdentifier(String bookIdentifier) {
|
||||
this.bookIdentifier = bookIdentifier;
|
||||
}
|
||||
|
||||
public String getBookTitle() {
|
||||
return bookTitle;
|
||||
}
|
||||
|
||||
public void setBookTitle(String bookTitle) {
|
||||
this.bookTitle = bookTitle;
|
||||
}
|
||||
|
||||
public LocalDateTime getDueAt() {
|
||||
return dueAt;
|
||||
}
|
||||
|
||||
public void setDueAt(LocalDateTime dueAt) {
|
||||
this.dueAt = dueAt;
|
||||
}
|
||||
|
||||
public long getOverdueDays() {
|
||||
return overdueDays;
|
||||
}
|
||||
|
||||
public void setOverdueDays(long overdueDays) {
|
||||
this.overdueDays = overdueDays;
|
||||
}
|
||||
|
||||
public String getDueAtText() {
|
||||
return dueAt == null ? "" : DISPLAY_FORMAT.format(dueAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.mzh.library.entity;
|
||||
|
||||
public class PopularBookReportRow {
|
||||
private String bookIdentifier;
|
||||
private String title;
|
||||
private String author;
|
||||
private int borrowCount;
|
||||
|
||||
public String getBookIdentifier() {
|
||||
return bookIdentifier;
|
||||
}
|
||||
|
||||
public void setBookIdentifier(String bookIdentifier) {
|
||||
this.bookIdentifier = bookIdentifier;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public int getBorrowCount() {
|
||||
return borrowCount;
|
||||
}
|
||||
|
||||
public void setBorrowCount(int borrowCount) {
|
||||
this.borrowCount = borrowCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.mzh.library.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ReportCenter {
|
||||
private InventorySummary inventorySummary;
|
||||
private BorrowingSummary borrowingSummary;
|
||||
private List<OverdueReportRow> overdueRows = Collections.emptyList();
|
||||
private List<PopularBookReportRow> popularBooks = Collections.emptyList();
|
||||
|
||||
public InventorySummary getInventorySummary() {
|
||||
return inventorySummary;
|
||||
}
|
||||
|
||||
public void setInventorySummary(InventorySummary inventorySummary) {
|
||||
this.inventorySummary = inventorySummary;
|
||||
}
|
||||
|
||||
public BorrowingSummary getBorrowingSummary() {
|
||||
return borrowingSummary;
|
||||
}
|
||||
|
||||
public void setBorrowingSummary(BorrowingSummary borrowingSummary) {
|
||||
this.borrowingSummary = borrowingSummary;
|
||||
}
|
||||
|
||||
public List<OverdueReportRow> getOverdueRows() {
|
||||
return overdueRows;
|
||||
}
|
||||
|
||||
public void setOverdueRows(List<OverdueReportRow> overdueRows) {
|
||||
this.overdueRows = immutableCopy(overdueRows);
|
||||
}
|
||||
|
||||
public List<PopularBookReportRow> getPopularBooks() {
|
||||
return popularBooks;
|
||||
}
|
||||
|
||||
public void setPopularBooks(List<PopularBookReportRow> popularBooks) {
|
||||
this.popularBooks = immutableCopy(popularBooks);
|
||||
}
|
||||
|
||||
private static <T> List<T> immutableCopy(List<T> source) {
|
||||
if (source == null || source.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Collections.unmodifiableList(new ArrayList<>(source));
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public class AuthorizationFilter implements Filter {
|
||||
private static final Logger LOGGER = Logger.getLogger(AuthorizationFilter.class.getName());
|
||||
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
||||
private static final List<PathRule> RULES = Arrays.asList(
|
||||
new PathRule("/reports", Permission.VIEW_REPORTS),
|
||||
new PathRule("/borrowing", Permission.MANAGE_BORROWING),
|
||||
new PathRule("/books", Permission.MANAGE_BOOKS),
|
||||
new PathRule("/readers", Permission.MANAGE_READERS),
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.mzh.library.service;
|
||||
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.ReportCenter;
|
||||
|
||||
public interface ReportService {
|
||||
ServiceResult<ReportCenter> loadReportCenter(AuthenticatedUser actor);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.mzh.library.service.impl;
|
||||
|
||||
import com.mzh.library.dao.ReportDao;
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.Permission;
|
||||
import com.mzh.library.entity.ReportCenter;
|
||||
import com.mzh.library.exception.DaoException;
|
||||
import com.mzh.library.service.PermissionPolicy;
|
||||
import com.mzh.library.service.ReportService;
|
||||
import com.mzh.library.service.ServiceResult;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class ReportServiceImpl implements ReportService {
|
||||
private static final Logger LOGGER = Logger.getLogger(ReportServiceImpl.class.getName());
|
||||
private static final String UNAVAILABLE_MESSAGE =
|
||||
"Report service is temporarily unavailable. Please try again later.";
|
||||
private static final String DENIED_MESSAGE = "You do not have permission to view reports.";
|
||||
private static final int POPULAR_BOOK_LIMIT = 10;
|
||||
|
||||
private final ReportDao reportDao;
|
||||
private final PermissionPolicy permissionPolicy;
|
||||
|
||||
public ReportServiceImpl(ReportDao reportDao) {
|
||||
this(reportDao, new PermissionPolicy());
|
||||
}
|
||||
|
||||
public ReportServiceImpl(ReportDao reportDao, PermissionPolicy permissionPolicy) {
|
||||
this.reportDao = reportDao;
|
||||
this.permissionPolicy = permissionPolicy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceResult<ReportCenter> loadReportCenter(AuthenticatedUser actor) {
|
||||
if (!canViewReports(actor)) {
|
||||
return ServiceResult.failure(DENIED_MESSAGE);
|
||||
}
|
||||
|
||||
try {
|
||||
ReportCenter reportCenter = new ReportCenter();
|
||||
reportCenter.setInventorySummary(reportDao.loadInventorySummary());
|
||||
reportCenter.setBorrowingSummary(reportDao.loadBorrowingSummary());
|
||||
reportCenter.setOverdueRows(reportDao.findOverdueRows());
|
||||
reportCenter.setPopularBooks(reportDao.findPopularBooks(POPULAR_BOOK_LIMIT));
|
||||
return ServiceResult.success(reportCenter);
|
||||
} catch (DaoException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Unable to load report center actorId=" + actor.getId(), ex);
|
||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canViewReports(AuthenticatedUser actor) {
|
||||
return actor != null && permissionPolicy.allows(actor.getRole(), Permission.VIEW_REPORTS);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
<a href="${pageContext.request.contextPath}/books">Books</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>
|
||||
</c:if>
|
||||
<a href="${pageContext.request.contextPath}/reader/home">Reader</a>
|
||||
<c:if test="${sessionScope.userRole == 'reader'}">
|
||||
|
||||
@@ -52,6 +52,12 @@
|
||||
<p>Create loans, process returns, renew active records, and review overdue items.</p>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Open</a>
|
||||
</article>
|
||||
|
||||
<article class="workspace-card">
|
||||
<h2>Report Center</h2>
|
||||
<p>Review inventory health, borrowing counts, overdue records, and popular books.</p>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/reports">Open</a>
|
||||
</article>
|
||||
</c:if>
|
||||
|
||||
<article class="workspace-card">
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<%@ 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>Reports - 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="reports-title">
|
||||
<div>
|
||||
<p class="eyebrow">Reports</p>
|
||||
<h1 id="reports-title">Report center</h1>
|
||||
<p>Review collection inventory, borrowing health, overdue loans, and popular books.</p>
|
||||
</div>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Borrowing records</a>
|
||||
</section>
|
||||
|
||||
<c:if test="${not empty errorMessage}">
|
||||
<div class="message message-error" role="alert">
|
||||
<c:out value="${errorMessage}" />
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<c:if test="${not empty reportCenter}">
|
||||
<section class="report-grid" aria-label="Report summary">
|
||||
<article class="report-card">
|
||||
<p class="eyebrow">Inventory</p>
|
||||
<h2>Total titles</h2>
|
||||
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.totalTitles}" /></p>
|
||||
</article>
|
||||
<article class="report-card">
|
||||
<p class="eyebrow">Inventory</p>
|
||||
<h2>Total copies</h2>
|
||||
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.totalCopies}" /></p>
|
||||
</article>
|
||||
<article class="report-card">
|
||||
<p class="eyebrow">Inventory</p>
|
||||
<h2>Available copies</h2>
|
||||
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.availableCopies}" /></p>
|
||||
</article>
|
||||
<article class="report-card">
|
||||
<p class="eyebrow">Attention</p>
|
||||
<h2>Unavailable or empty</h2>
|
||||
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.unavailableOrEmptyTitles}" /></p>
|
||||
</article>
|
||||
<article class="report-card">
|
||||
<p class="eyebrow">Borrowing</p>
|
||||
<h2>Currently borrowed</h2>
|
||||
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.activeLoans}" /></p>
|
||||
</article>
|
||||
<article class="report-card">
|
||||
<p class="eyebrow">Borrowing</p>
|
||||
<h2>Returned records</h2>
|
||||
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.returnedLoans}" /></p>
|
||||
</article>
|
||||
<article class="report-card report-card-alert">
|
||||
<p class="eyebrow">Borrowing</p>
|
||||
<h2>Overdue loans</h2>
|
||||
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.overdueLoans}" /></p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="table-panel" aria-labelledby="overdue-report-title">
|
||||
<h2 id="overdue-report-title">Overdue list</h2>
|
||||
<c:choose>
|
||||
<c:when test="${empty reportCenter.overdueRows}">
|
||||
<p class="empty-state">No active overdue borrowing records.</p>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Reader</th>
|
||||
<th scope="col">Book</th>
|
||||
<th scope="col">Due date</th>
|
||||
<th scope="col">Overdue days</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:forEach var="row" items="${reportCenter.overdueRows}">
|
||||
<tr>
|
||||
<td>
|
||||
<strong><c:out value="${row.readerIdentifier}" /></strong>
|
||||
<div class="muted-text"><c:out value="${row.readerName}" /></div>
|
||||
</td>
|
||||
<td>
|
||||
<strong><c:out value="${row.bookIdentifier}" /></strong>
|
||||
<div class="muted-text"><c:out value="${row.bookTitle}" /></div>
|
||||
</td>
|
||||
<td><c:out value="${row.dueAtText}" /></td>
|
||||
<td>
|
||||
<span class="status-pill status-overdue">
|
||||
<c:out value="${row.overdueDays}" /> days
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</section>
|
||||
|
||||
<section class="table-panel" aria-labelledby="popular-report-title">
|
||||
<h2 id="popular-report-title">Popular borrowing ranking</h2>
|
||||
<c:choose>
|
||||
<c:when test="${empty reportCenter.popularBooks}">
|
||||
<p class="empty-state">No borrowing records are available for ranking yet.</p>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Book</th>
|
||||
<th scope="col">Author</th>
|
||||
<th scope="col">Borrow records</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:forEach var="row" items="${reportCenter.popularBooks}">
|
||||
<tr>
|
||||
<td>
|
||||
<strong><c:out value="${row.bookIdentifier}" /></strong>
|
||||
<div class="muted-text"><c:out value="${row.title}" /></div>
|
||||
</td>
|
||||
<td><c:out value="${row.author}" /></td>
|
||||
<td><c:out value="${row.borrowCount}" /></td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</section>
|
||||
</c:if>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -45,6 +45,12 @@
|
||||
<p>Create loans, process returns, renew records, and review overdue items.</p>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Manage borrowing</a>
|
||||
</article>
|
||||
|
||||
<article class="workspace-card">
|
||||
<h2>Report Center</h2>
|
||||
<p>Review inventory summaries, borrowing health, overdue lists, and popular books.</p>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/reports">View reports</a>
|
||||
</article>
|
||||
</c:if>
|
||||
|
||||
<c:if test="${sessionScope.userRole == 'reader'}">
|
||||
|
||||
@@ -132,6 +132,15 @@
|
||||
<url-pattern>/reader/loans</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>ReportServlet</servlet-name>
|
||||
<servlet-class>com.mzh.library.controller.ReportServlet</servlet-class>
|
||||
</servlet>
|
||||
<servlet-mapping>
|
||||
<servlet-name>ReportServlet</servlet-name>
|
||||
<url-pattern>/reports</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>UnauthorizedServlet</servlet-name>
|
||||
<servlet-class>com.mzh.library.controller.UnauthorizedServlet</servlet-class>
|
||||
|
||||
@@ -238,6 +238,13 @@ h2 {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.report-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.workspace-card {
|
||||
min-height: 190px;
|
||||
display: flex;
|
||||
@@ -254,6 +261,32 @@ h2 {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
min-height: 150px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
background: var(--color-panel);
|
||||
box-shadow: var(--shadow-panel);
|
||||
}
|
||||
|
||||
.report-card h2 {
|
||||
color: var(--color-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.report-card-alert {
|
||||
border-color: rgba(181, 66, 56, 0.28);
|
||||
}
|
||||
|
||||
.report-metric {
|
||||
margin-bottom: 0;
|
||||
color: var(--color-primary-strong);
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.notice-panel {
|
||||
max-width: 680px;
|
||||
padding: 28px;
|
||||
@@ -491,6 +524,7 @@ h2 {
|
||||
.notice-panel,
|
||||
.dashboard-hero,
|
||||
.workspace-card,
|
||||
.report-card,
|
||||
.toolbar-panel,
|
||||
.table-panel,
|
||||
.form-panel {
|
||||
|
||||
@@ -11,12 +11,15 @@ public final class PermissionPolicyCheck {
|
||||
PermissionPolicy policy = new PermissionPolicy();
|
||||
|
||||
require(policy.allows(Role.ADMINISTRATOR, Permission.MANAGE_USERS), "administrator should manage users");
|
||||
require(policy.allows(Role.ADMINISTRATOR, Permission.VIEW_REPORTS), "administrator should view reports");
|
||||
require(policy.allows(Role.LIBRARIAN, Permission.MANAGE_BORROWING), "librarian should manage borrowing");
|
||||
require(policy.allows(Role.LIBRARIAN, Permission.MANAGE_READERS), "librarian should manage readers");
|
||||
require(policy.allows(Role.LIBRARIAN, Permission.VIEW_REPORTS), "librarian should view reports");
|
||||
require(!policy.allows(Role.LIBRARIAN, Permission.BORROW_BOOKS), "librarian should not borrow as a reader");
|
||||
require(!policy.allows(Role.LIBRARIAN, Permission.MANAGE_USERS), "librarian should not manage users");
|
||||
require(policy.allows(Role.READER, Permission.VIEW_CATALOG), "reader should view catalog");
|
||||
require(policy.allows(Role.READER, Permission.BORROW_BOOKS), "reader should view borrowing capabilities");
|
||||
require(!policy.allows(Role.READER, Permission.VIEW_REPORTS), "reader should not view reports");
|
||||
require(!policy.allows(Role.READER, Permission.MANAGE_BORROWING), "reader should not manage borrowing");
|
||||
require(!policy.allows(Role.READER, Permission.MANAGE_BOOKS), "reader should not manage books");
|
||||
require(!policy.allows(Role.READER, Permission.MANAGE_READERS), "reader should not manage readers");
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
package com.mzh.library.service;
|
||||
|
||||
import com.mzh.library.dao.ReportDao;
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.BorrowingSummary;
|
||||
import com.mzh.library.entity.InventorySummary;
|
||||
import com.mzh.library.entity.OverdueReportRow;
|
||||
import com.mzh.library.entity.Permission;
|
||||
import com.mzh.library.entity.PopularBookReportRow;
|
||||
import com.mzh.library.entity.ReportCenter;
|
||||
import com.mzh.library.entity.Role;
|
||||
import com.mzh.library.exception.DaoException;
|
||||
import com.mzh.library.service.impl.ReportServiceImpl;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public final class ReportServiceCheck {
|
||||
private static final String DENIED_MESSAGE = "You do not have permission to view reports.";
|
||||
private static final String UNAVAILABLE_MESSAGE =
|
||||
"Report service is temporarily unavailable. Please try again later.";
|
||||
|
||||
private ReportServiceCheck() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Logger.getLogger(ReportServiceImpl.class.getName()).setLevel(Level.OFF);
|
||||
|
||||
ReportService service = new ReportServiceImpl(new InMemoryReportDao());
|
||||
AuthenticatedUser administrator = user(5L, Role.ADMINISTRATOR);
|
||||
AuthenticatedUser librarian = user(10L, Role.LIBRARIAN);
|
||||
AuthenticatedUser reader = user(20L, Role.READER);
|
||||
|
||||
ServiceResult<ReportCenter> anonymousDenied = service.loadReportCenter(null);
|
||||
requireDenied(anonymousDenied, "anonymous user should not view reports");
|
||||
|
||||
ServiceResult<ReportCenter> readerDenied = service.loadReportCenter(reader);
|
||||
requireDenied(readerDenied, "reader should not view reports");
|
||||
|
||||
ServiceResult<ReportCenter> adminReport = service.loadReportCenter(administrator);
|
||||
require(adminReport.isSuccessful(), "administrator should load report center");
|
||||
|
||||
ServiceResult<ReportCenter> report = service.loadReportCenter(librarian);
|
||||
require(report.isSuccessful(), "librarian should load report center");
|
||||
require(report.getData().getInventorySummary().getTotalTitles() == 4,
|
||||
"inventory summary should expose total titles");
|
||||
require(report.getData().getInventorySummary().getUnavailableOrEmptyTitles() == 2,
|
||||
"inventory summary should expose unavailable or empty titles");
|
||||
require(report.getData().getBorrowingSummary().getActiveLoans() == 3,
|
||||
"borrowing summary should expose active loans");
|
||||
require(report.getData().getBorrowingSummary().getOverdueLoans() == 1,
|
||||
"borrowing summary should expose overdue loans");
|
||||
require(report.getData().getOverdueRows().size() == 1,
|
||||
"report center should include overdue rows");
|
||||
require(report.getData().getPopularBooks().size() == 2,
|
||||
"report center should include popular book rows");
|
||||
|
||||
ReportService failingService = new ReportServiceImpl(new FailingReportDao());
|
||||
ServiceResult<ReportCenter> failure = failingService.loadReportCenter(librarian);
|
||||
require(!failure.isSuccessful(), "DAO failure should not escape reports");
|
||||
require(UNAVAILABLE_MESSAGE.equals(failure.getMessage()),
|
||||
"DAO failure should map to safe report message");
|
||||
}
|
||||
|
||||
private static void requireDenied(ServiceResult<ReportCenter> result, String message) {
|
||||
require(!result.isSuccessful(), message);
|
||||
require(DENIED_MESSAGE.equals(result.getMessage()),
|
||||
"report denial should use the report permission message");
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(long id, Role role) {
|
||||
return new AuthenticatedUser(id, role.getCode(), role.getDisplayName(), role,
|
||||
role == Role.READER
|
||||
? EnumSet.of(Permission.VIEW_CATALOG, Permission.BORROW_BOOKS)
|
||||
: EnumSet.of(Permission.MANAGE_BORROWING, Permission.VIEW_REPORTS, Permission.VIEW_CATALOG));
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) {
|
||||
throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemoryReportDao implements ReportDao {
|
||||
@Override
|
||||
public InventorySummary loadInventorySummary() {
|
||||
InventorySummary summary = new InventorySummary();
|
||||
summary.setTotalTitles(4);
|
||||
summary.setTotalCopies(12);
|
||||
summary.setAvailableCopies(7);
|
||||
summary.setUnavailableOrEmptyTitles(2);
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BorrowingSummary loadBorrowingSummary() {
|
||||
BorrowingSummary summary = new BorrowingSummary();
|
||||
summary.setActiveLoans(3);
|
||||
summary.setReturnedLoans(5);
|
||||
summary.setOverdueLoans(1);
|
||||
return summary;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OverdueReportRow> findOverdueRows() {
|
||||
OverdueReportRow row = new OverdueReportRow();
|
||||
row.setReaderIdentifier("RD-1000");
|
||||
row.setReaderName("Active Reader");
|
||||
row.setBookIdentifier("BK-1000");
|
||||
row.setBookTitle("Effective Java");
|
||||
row.setDueAt(LocalDateTime.of(2026, 4, 1, 12, 0));
|
||||
row.setOverdueDays(26);
|
||||
return Collections.singletonList(row);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PopularBookReportRow> findPopularBooks(int limit) {
|
||||
PopularBookReportRow first = popularBook("BK-1000", "Effective Java", "Joshua Bloch", 5);
|
||||
PopularBookReportRow second = popularBook("BK-1001", "Clean Code", "Robert C. Martin", 3);
|
||||
return Arrays.asList(first, second);
|
||||
}
|
||||
|
||||
private PopularBookReportRow popularBook(String identifier, String title, String author, int borrowCount) {
|
||||
PopularBookReportRow row = new PopularBookReportRow();
|
||||
row.setBookIdentifier(identifier);
|
||||
row.setTitle(title);
|
||||
row.setAuthor(author);
|
||||
row.setBorrowCount(borrowCount);
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FailingReportDao implements ReportDao {
|
||||
@Override
|
||||
public InventorySummary loadInventorySummary() {
|
||||
throw new DaoException("Simulated inventory report failure", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BorrowingSummary loadBorrowingSummary() {
|
||||
throw new DaoException("Simulated borrowing report failure", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OverdueReportRow> findOverdueRows() {
|
||||
throw new DaoException("Simulated overdue report failure", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PopularBookReportRow> findPopularBooks(int limit) {
|
||||
throw new DaoException("Simulated popular report failure", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user