完成报表中心

This commit is contained in:
Zzzz
2026-04-27 22:07:20 +08:00
parent d503036aeb
commit f9a9c630c2
26 changed files with 1127 additions and 4 deletions
@@ -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'}">
+9
View File
@@ -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>
+34
View File
@@ -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 {