adds project overview page

Sun, 24 May 2020 15:30:43 +0200

author
Mike Becker <universe@uap-core.de>
date
Sun, 24 May 2020 15:30:43 +0200
changeset 80
27a25f32048e
parent 79
f64255a88d66
child 81
1a2e7b5d48f7

adds project overview page

src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/VersionDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/IssueStatus.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/VersionStatistics.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/resources/localization/projects.properties file | annotate | diff | comparison | revisions
src/main/resources/localization/projects_de.properties file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issues.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/project-details.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/version-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/version-stats.jsp file | annotate | diff | comparison | revisions
src/main/webapp/lightpit.css file | annotate | diff | comparison | revisions
src/main/webapp/projects.css file | annotate | diff | comparison | revisions
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Sat May 23 14:13:09 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Sun May 24 15:30:43 2020 +0200
@@ -282,6 +282,13 @@
     protected <T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) {
         final String paramValue = req.getParameter(name);
         if (paramValue == null) return Optional.empty();
+        if (clazz.equals(Boolean.class)) {
+            if (paramValue.toLowerCase().equals("false") || paramValue.equals("0")) {
+                return Optional.of((T)Boolean.FALSE);
+            } else {
+                return Optional.of((T)Boolean.TRUE);
+            }
+        }
         if (clazz.equals(String.class)) return Optional.of((T) paramValue);
         if (java.sql.Date.class.isAssignableFrom(clazz)) {
             try {
--- a/src/main/java/de/uapcore/lightpit/dao/VersionDao.java	Sat May 23 14:13:09 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/VersionDao.java	Sun May 24 15:30:43 2020 +0200
@@ -30,6 +30,7 @@
 
 import de.uapcore.lightpit.entities.Project;
 import de.uapcore.lightpit.entities.Version;
+import de.uapcore.lightpit.entities.VersionStatistics;
 
 import java.sql.SQLException;
 import java.util.List;
@@ -44,4 +45,31 @@
      * @throws SQLException on any kind of SQL error
      */
     List<Version> list(Project project) throws SQLException;
+
+    /**
+     * Retrieves statistics about issues that arose in a version.
+     *
+     * @param version the version
+     * @return version statistics
+     * @throws SQLException on any kind of SQL error
+     */
+    VersionStatistics statsOpenedIssues(Version version) throws SQLException;
+
+    /**
+     * Retrieves statistics about issues that are scheduled for a version.
+     *
+     * @param version the version
+     * @return version statistics
+     * @throws SQLException on any kind of SQL error
+     */
+    VersionStatistics statsScheduledIssues(Version version) throws SQLException;
+
+    /**
+     * Retrieves statistics about issues that are resolved in a version.
+     *
+     * @param version the version
+     * @return version statistics
+     * @throws SQLException on any kind of SQL error
+     */
+    VersionStatistics statsResolvedIssues(Version version) throws SQLException;
 }
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Sat May 23 14:13:09 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Sun May 24 15:30:43 2020 +0200
@@ -29,9 +29,7 @@
 package de.uapcore.lightpit.dao.postgres;
 
 import de.uapcore.lightpit.dao.VersionDao;
-import de.uapcore.lightpit.entities.Project;
-import de.uapcore.lightpit.entities.Version;
-import de.uapcore.lightpit.entities.VersionStatus;
+import de.uapcore.lightpit.entities.*;
 
 import java.sql.Connection;
 import java.sql.PreparedStatement;
@@ -44,13 +42,14 @@
 public final class PGVersionDao implements VersionDao {
 
     private final PreparedStatement insert, update, list, find;
+    private final PreparedStatement issuesAffected, issuesScheduled, issuesResolved;
 
     public PGVersionDao(Connection connection) throws SQLException {
         list = connection.prepareStatement(
                 "select versionid, project, name, ordinal, status " +
                         "from lpit_version " +
                         "where project = ? " +
-                        "order by ordinal, lower(name)");
+                        "order by ordinal desc, lower(name) desc");
 
         find = connection.prepareStatement(
                 "select versionid, project, name, ordinal, status " +
@@ -63,6 +62,28 @@
         update = connection.prepareStatement(
                 "update lpit_version set name = ?, ordinal = ?, status = ?::version_status where versionid = ?"
         );
+
+        issuesAffected = connection.prepareStatement(
+                "select category, status, count(*) as issuecount " +
+                        "from lpit_issue_affected_version " +
+                        "join lpit_issue using (issueid) " +
+                        "where versionid = ? " +
+                        "group by category, status"
+        );
+        issuesScheduled = connection.prepareStatement(
+                "select category, status, count(*) as issuecount " +
+                        "from lpit_issue_scheduled_version " +
+                        "join lpit_issue using (issueid) " +
+                        "where versionid = ? " +
+                        "group by category, status"
+        );
+        issuesResolved = connection.prepareStatement(
+                "select category, status, count(*) as issuecount " +
+                        "from lpit_issue_resolved_version " +
+                        "join lpit_issue using (issueid) " +
+                        "where versionid = ? " +
+                        "group by category, status"
+        );
     }
 
     private Version mapColumns(ResultSet result) throws SQLException {
@@ -74,6 +95,20 @@
         return version;
     }
 
+    private VersionStatistics versionStatistics(Version version, PreparedStatement stmt) throws SQLException {
+        stmt.setInt(1, version.getId());
+        final var result = stmt.executeQuery();
+        final var stats = new VersionStatistics(version);
+        while (result.next()) {
+            stats.setIssueCount(
+                    IssueCategory.valueOf(result.getString("category")),
+                    IssueStatus.valueOf(result.getString("status")),
+                    result.getInt("issuecount")
+            );
+        }
+        return stats;
+    }
+
     @Override
     public void save(Version instance) throws SQLException {
         Objects.requireNonNull(instance.getName());
@@ -121,4 +156,19 @@
             }
         }
     }
+
+    @Override
+    public VersionStatistics statsOpenedIssues(Version version) throws SQLException {
+        return versionStatistics(version, issuesAffected);
+    }
+
+    @Override
+    public VersionStatistics statsScheduledIssues(Version version) throws SQLException {
+        return versionStatistics(version, issuesScheduled);
+    }
+
+    @Override
+    public VersionStatistics statsResolvedIssues(Version version) throws SQLException {
+        return versionStatistics(version, issuesResolved);
+    }
 }
--- a/src/main/java/de/uapcore/lightpit/entities/IssueStatus.java	Sat May 23 14:13:09 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/entities/IssueStatus.java	Sun May 24 15:30:43 2020 +0200
@@ -29,12 +29,27 @@
 package de.uapcore.lightpit.entities;
 
 public enum IssueStatus {
-    InSpecification,
-    ToDo,
-    Scheduled,
-    InProgress,
-    InReview,
-    Done,
-    Rejected,
-    Withdrawn
+    InSpecification(0),
+    ToDo(0),
+    Scheduled(1),
+    InProgress(1),
+    InReview(1),
+    Done(2),
+    Rejected(2),
+    Withdrawn(2),
+    Duplicate(2);
+
+    private int phase;
+
+    IssueStatus(int phase) {
+        this.phase = phase;
+    }
+
+    public int getPhase() {
+        return phase;
+    }
+
+    public static int phaseCount() {
+        return 3;
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/entities/VersionStatistics.java	Sun May 24 15:30:43 2020 +0200
@@ -0,0 +1,101 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2018 Mike Becker. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *   1. Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *
+ *   2. Redistributions in binary form must reproduce the above copyright
+ *      notice, this list of conditions and the following disclaimer in the
+ *      documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+package de.uapcore.lightpit.entities;
+
+public class VersionStatistics {
+
+    private final Version version;
+    private int[][] issueCount;
+
+    private int[] rowTotals = null;
+    private int[] columnTotals = null;
+    private int total = -1;
+
+    public VersionStatistics(Version version) {
+        this.version = version;
+        issueCount = new int[IssueCategory.values().length][IssueStatus.values().length];
+    }
+
+    public Version getVersion() {
+        return version;
+    }
+
+    public void setIssueCount(IssueCategory category, IssueStatus status, int count) {
+        issueCount[category.ordinal()][status.ordinal()] = count;
+        total = -1;
+        rowTotals = columnTotals = null;
+    }
+
+    public int[][] getIssueCount() {
+        return issueCount;
+    }
+
+    public int[] getRowTotals() {
+        if (rowTotals != null) return rowTotals;
+        final int cn = IssueCategory.values().length;
+        final int sn = IssueStatus.values().length;
+        final var totals = new int[cn];
+        for (int i = 0 ; i < cn ; i++) {
+            totals[i] = 0;
+            for (int j = 0 ; j < sn ; j++) {
+                totals[i] += issueCount[i][j];
+            }
+        }
+        return rowTotals = totals;
+    }
+
+    public int[] getColumnTotals() {
+        if (columnTotals != null) return columnTotals;
+        final int cn = IssueCategory.values().length;
+        final int sn = IssueStatus.values().length;
+        final var totals = new int[sn];
+        for (int i = 0 ; i < sn ; i++) {
+            totals[i] = 0;
+            for (int j = 0 ; j < cn ; j++) {
+                totals[i] += issueCount[j][i];
+            }
+        }
+        return columnTotals = totals;
+    }
+
+    public int getTotal() {
+        if (this.total >= 0) {
+            return this.total;
+        }
+        int total = 0;
+        final int cn = IssueCategory.values().length;
+        final int sn = IssueStatus.values().length;
+        for (int i = 0 ; i < sn ; i++) {
+            for (int j = 0 ; j < cn ; j++) {
+                total += issueCount[j][i];
+            }
+        }
+        return this.total = total;
+    }
+}
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sat May 23 14:13:09 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sun May 24 15:30:43 2020 +0200
@@ -57,9 +57,10 @@
 
     private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class);
 
-    public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected-project");
-    public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected-issue");
-    public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected-version");
+    public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected_project");
+    public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected_issue");
+    public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected_version");
+    public static final String SESSION_ATTR_HIDE_ZEROS = fqn(ProjectsModule.class, "stats_hide_zeros");
 
     private class SessionSelection {
         final HttpSession session;
@@ -128,15 +129,40 @@
         }
     }
 
+    private void setAttributeHideZeros(HttpServletRequest req) {
+        final Boolean value;
+        final var param = getParameter(req, Boolean.class, "reduced");
+        if (param.isPresent()) {
+            value = param.get();
+            req.getSession().setAttribute(SESSION_ATTR_HIDE_ZEROS, value);
+        } else {
+            final var sessionValue = req.getSession().getAttribute(SESSION_ATTR_HIDE_ZEROS);
+            if (sessionValue != null) {
+                value = (Boolean) sessionValue;
+            } else {
+                value = false;
+                req.getSession().setAttribute(SESSION_ATTR_HIDE_ZEROS, value);
+            }
+        }
+        req.setAttribute("statsHideZeros", value);
+    }
+
     @Override
     protected String getResourceBundleName() {
         return "localization.projects";
     }
 
+
+    private static final int BREADCRUMB_LEVEL_ROOT = 0;
+    private static final int BREADCRUMB_LEVEL_PROJECT = 1;
+    private static final int BREADCRUMB_LEVEL_VERSION = 2;
+    private static final int BREADCRUMB_LEVEL_ISSUE_LIST = 3;
+    private static final int BREADCRUMB_LEVEL_ISSUE = 4;
+
     /**
      * Creates the breadcrumb menu.
      *
-     * @param level           the current active level (0: root, 1: project, 2: version, 3: issue)
+     * @param level           the current active level (0: root, 1: project, 2: version, 3: issue list, 4: issue)
      * @param sessionSelection the currently selected objects
      * @return a dynamic breadcrumb menu trying to display as many levels as possible
      */
@@ -147,7 +173,7 @@
         entry = new MenuEntry(new ResourceKey("localization.lightpit", "menu.projects"),
                 "projects/");
         breadcrumbs.add(entry);
-        if (level == 0) entry.setActive(true);
+        if (level == BREADCRUMB_LEVEL_ROOT) entry.setActive(true);
 
         if (sessionSelection.project != null) {
             if (sessionSelection.project.getId() < 0) {
@@ -157,7 +183,7 @@
                 entry = new MenuEntry(sessionSelection.project.getName(),
                         "projects/view?pid=" + sessionSelection.project.getId());
             }
-            if (level == 1) entry.setActive(true);
+            if (level == BREADCRUMB_LEVEL_PROJECT) entry.setActive(true);
             breadcrumbs.add(entry);
         }
 
@@ -170,15 +196,19 @@
                         // TODO: change link to issue overview for that version
                         "projects/versions/edit?id=" + sessionSelection.version.getId());
             }
-            if (level == 2) entry.setActive(true);
+            if (level == BREADCRUMB_LEVEL_VERSION) entry.setActive(true);
+            breadcrumbs.add(entry);
+        }
+
+        if (sessionSelection.project != null) {
+            entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"),
+                    // TODO: maybe also add selected version
+                    "projects/issues/?pid=" + sessionSelection.project.getId());
+            if (level == BREADCRUMB_LEVEL_ISSUE_LIST) entry.setActive(true);
             breadcrumbs.add(entry);
         }
 
         if (sessionSelection.issue != null) {
-            entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"),
-                    // TODO: change link to a separate issue view (maybe depending on the selected version)
-                    "projects/view?pid=" + sessionSelection.issue.getProject().getId());
-            breadcrumbs.add(entry);
             if (sessionSelection.issue.getId() < 0) {
                 entry = new MenuEntry(new ResourceKey("localization.projects", "button.issue.create"),
                         "projects/issues/edit");
@@ -187,7 +217,7 @@
                         // TODO: maybe change link to a view rather than directly opening the editor
                         "projects/issues/edit?id=" + sessionSelection.issue.getId());
             }
-            if (level == 3) entry.setActive(true);
+            if (level == BREADCRUMB_LEVEL_ISSUE) entry.setActive(true);
             breadcrumbs.add(entry);
         }
 
@@ -202,7 +232,7 @@
         setContentPage(req, "projects");
         setStylesheet(req, "projects");
 
-        setBreadcrumbs(req, getBreadcrumbs(0, sessionSelection));
+        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ROOT, sessionSelection));
 
         return ResponseType.HTML;
     }
@@ -211,7 +241,7 @@
         req.setAttribute("project", selection.project);
         req.setAttribute("users", dao.getUserDao().list());
         setContentPage(req, "project-form");
-        setBreadcrumbs(req, getBreadcrumbs(1, selection));
+        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection));
     }
 
     @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
@@ -253,29 +283,60 @@
     }
 
     @RequestMapping(requestPath = "view", method = HttpMethod.GET)
-    public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
+    public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
         final var sessionSelection = new SessionSelection(req, dao);
+        if (sessionSelection.project == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
+            return ResponseType.NONE;
+        }
 
-        req.setAttribute("versions", dao.getVersionDao().list(sessionSelection.project));
-        req.setAttribute("issues", dao.getIssueDao().list(sessionSelection.project));
+        final var versionDao = dao.getVersionDao();
+        final var versions = versionDao.list(sessionSelection.project);
+        final var statsAffected = new ArrayList<VersionStatistics>();
+        final var statsScheduled = new ArrayList<VersionStatistics>();
+        final var statsResolved = new ArrayList<VersionStatistics>();
+        for (Version version : versions) {
+            statsAffected.add(versionDao.statsOpenedIssues(version));
+            statsScheduled.add(versionDao.statsScheduledIssues(version));
+            statsResolved.add(versionDao.statsResolvedIssues(version));
+        }
 
-        setBreadcrumbs(req, getBreadcrumbs(1, sessionSelection));
+        setAttributeHideZeros(req);
+
+        req.setAttribute("versions", versions);
+        req.setAttribute("statsAffected", statsAffected);
+        req.setAttribute("statsScheduled", statsScheduled);
+        req.setAttribute("statsResolved", statsResolved);
+
+        req.setAttribute("issueStatusEnum", IssueStatus.values());
+        req.setAttribute("issueCategoryEnum", IssueCategory.values());
+
+        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, sessionSelection));
         setContentPage(req, "project-details");
+        setStylesheet(req, "projects");
 
         return ResponseType.HTML;
     }
 
     private void configureEditVersionForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
+        final var versionDao = dao.getVersionDao();
         req.setAttribute("projects", dao.getProjectDao().list());
         req.setAttribute("version", selection.version);
         req.setAttribute("versionStatusEnum", VersionStatus.values());
 
+        req.setAttribute("issueStatusEnum", IssueStatus.values());
+        req.setAttribute("issueCategoryEnum", IssueCategory.values());
+        req.setAttribute("statsAffected", versionDao.statsOpenedIssues(selection.version));
+        req.setAttribute("statsScheduled", versionDao.statsScheduledIssues(selection.version));
+        req.setAttribute("statsResolved", versionDao.statsResolvedIssues(selection.version));
+        setAttributeHideZeros(req);
+
         setContentPage(req, "version-form");
-        setBreadcrumbs(req, getBreadcrumbs(2, selection));
+        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection));
     }
 
     @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET)
-    public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
+    public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
         final var sessionSelection = new SessionSelection(req, dao);
 
         sessionSelection.selectVersion(findByParameter(req, Integer.class, "id", dao.getVersionDao()::find)
@@ -286,7 +347,7 @@
     }
 
     @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST)
-    public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
+    public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
         final var sessionSelection = new SessionSelection(req, dao);
 
         var version = new Version(-1, sessionSelection.project);
@@ -320,11 +381,28 @@
         req.setAttribute("users", dao.getUserDao().list());
 
         setContentPage(req, "issue-form");
-        setBreadcrumbs(req, getBreadcrumbs(3, selection));
+        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE, selection));
+    }
+
+    @RequestMapping(requestPath = "issues/", method = HttpMethod.GET)
+    public ResponseType issues(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
+        final var sessionSelection = new SessionSelection(req, dao);
+        if (sessionSelection.project == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
+            return ResponseType.NONE;
+        }
+
+        req.setAttribute("issues", dao.getIssueDao().list(sessionSelection.project));
+
+        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE_LIST, sessionSelection));
+        setContentPage(req, "issues");
+        setStylesheet(req, "projects");
+
+        return ResponseType.HTML;
     }
 
     @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET)
-    public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
+    public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
         final var sessionSelection = new SessionSelection(req, dao);
 
         sessionSelection.selectIssue(findByParameter(req, Integer.class, "id",
@@ -335,7 +413,7 @@
     }
 
     @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST)
-    public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
+    public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
         final var sessionSelection = new SessionSelection(req, dao);
 
         Issue issue = new Issue(-1, sessionSelection.project);
--- a/src/main/resources/localization/projects.properties	Sat May 23 14:13:09 2020 +0200
+++ b/src/main/resources/localization/projects.properties	Sun May 24 15:30:43 2020 +0200
@@ -26,6 +26,10 @@
 button.create=New Project
 button.version.create=New Version
 button.issue.create=New Issue
+button.issue.list=Show Issues
+
+button.stats.hidezeros=Reduced View
+button.stats.showzeros=Full View
 
 no-projects=Welcome to LightPIT. Start off by creating a new project!
 
@@ -45,12 +49,18 @@
 placeholder.null-owner=Unassigned
 placeholder.null-assignee=Unassigned
 
+version.label=Version
 version.status.Future=Future
 version.status.Unreleased=Unreleased
 version.status.Released=Released
 version.status.LTS=LTS
 version.status.Deprecated=Deprecated
 
+version.statistics.affected=Affected by Issues
+version.statistics.scheduled=Scheduled Issues
+version.statistics.resolved=Resolved Issues
+version.statistics.total=Total
+
 thead.issue.project=Project
 thead.issue.subject=Subject
 thead.issue.description=Description
@@ -81,3 +91,4 @@
 issue.status.Done=Done
 issue.status.Rejected=Rejected
 issue.status.Withdrawn=Withdrawn
+issue.status.Duplicate=Duplicate
--- a/src/main/resources/localization/projects_de.properties	Sat May 23 14:13:09 2020 +0200
+++ b/src/main/resources/localization/projects_de.properties	Sun May 24 15:30:43 2020 +0200
@@ -26,6 +26,10 @@
 button.create=Neues Projekt
 button.version.create=Neue Version
 button.issue.create=Neuer Vorgang
+button.issue.list=Vorg\u00e4nge
+
+button.stats.hidezeros=Reduzierte Ansicht
+button.stats.showzeros=Komplettansicht
 
 no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes!
 
@@ -45,12 +49,18 @@
 placeholder.null-owner=Nicht Zugewiesen
 placeholder.null-assignee=Niemandem
 
+version.label=Version
 version.status.Future=Geplant
 version.status.Unreleased=Unver\u00f6ffentlicht
 version.status.Released=Ver\u00f6ffentlicht
 version.status.LTS=Langzeitsupport
 version.status.Deprecated=Veraltet
 
+version.statistics.affected=Betroffen von Vorg\u00e4ngen
+version.statistics.scheduled=Geplante Vorg\u00e4nge 
+version.statistics.resolved=Gel\u00f6ste Vorg\u00e4nge
+version.statistics.total=Summe
+
 thead.issue.project=Projekt
 thead.issue.subject=Thema
 thead.issue.description=Beschreibung
@@ -81,3 +91,4 @@
 issue.status.Done=Erledigt
 issue.status.Rejected=Zur\u00fcckgewiesen
 issue.status.Withdrawn=Zur\u00fcckgezogen
+issue.status.Duplicate=Duplikat
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/jsp/issues.jsp	Sun May 24 15:30:43 2020 +0200
@@ -0,0 +1,85 @@
+<%--
+DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+
+Copyright 2018 Mike Becker. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--%>
+<%@page pageEncoding="UTF-8" %>
+<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+
+<jsp:useBean id="issues" type="java.util.List<de.uapcore.lightpit.entities.Issue>" scope="request"/>
+
+<div id="tool-area">
+    <div>
+        <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a>
+    </div>
+</div>
+
+<table id="issue-list" class="datatable medskip">
+    <thead>
+    <tr>
+        <th><fmt:message key="thead.issue.subject"/></th>
+        <th><fmt:message key="thead.issue.assignee"/></th>
+        <th><fmt:message key="thead.issue.category"/></th>
+        <th><fmt:message key="thead.issue.status"/></th>
+        <th><fmt:message key="thead.issue.created"/></th>
+        <th><fmt:message key="thead.issue.updated"/></th>
+        <th><fmt:message key="thead.issue.eta"/></th>
+    </tr>
+    </thead>
+    <tbody>
+    <c:forEach var="issue" items="${issues}">
+        <tr>
+            <td>
+                <a href="./projects/issues/edit?id=${issue.id}">
+                    <c:out value="${issue.subject}" />
+                </a>
+            </td>
+            <td>
+                <c:if test="${not empty issue.assignee}">
+                    <c:out value="${issue.assignee.shortDisplayname}" />
+                </c:if>
+                <c:if test="${empty issue.assignee}">
+                    <fmt:message key="placeholder.null-assignee" />
+                </c:if>
+            </td>
+            <td>
+                <fmt:message key="issue.category.${issue.category}" />
+            </td>
+            <td>
+                <fmt:message key="issue.status.${issue.status}" />
+            </td>
+            <td>
+                <fmt:formatDate value="${issue.created}" type="BOTH"/>
+            </td>
+            <td>
+                <fmt:formatDate value="${issue.updated}" type="BOTH"/>
+            </td>
+            <td>
+                <fmt:formatDate value="${issue.eta}" />
+            </td>
+        </tr>
+    </c:forEach>
+    </tbody>
+</table>
--- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sat May 23 14:13:09 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sun May 24 15:30:43 2020 +0200
@@ -25,87 +25,46 @@
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 --%>
 <%@page pageEncoding="UTF-8" %>
-<%@page import="de.uapcore.lightpit.modules.ProjectsModule" %>
 <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
 <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
 
-<c:set scope="page" var="selectedProject" value="${sessionScope[ProjectsModule.SESSION_ATTR_SELECTED_PROJECT]}"/>
-
 <jsp:useBean id="versions" type="java.util.List<de.uapcore.lightpit.entities.Version>" scope="request"/>
-<jsp:useBean id="issues" type="java.util.List<de.uapcore.lightpit.entities.Issue>" scope="request"/>
+<jsp:useBean id="statsAffected" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/>
+<jsp:useBean id="statsScheduled" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/>
+<jsp:useBean id="statsResolved" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/>
+<jsp:useBean id="issueStatusEnum" type="de.uapcore.lightpit.entities.IssueStatus[]" scope="request"/>
+<jsp:useBean id="issueCategoryEnum" type="de.uapcore.lightpit.entities.IssueCategory[]" scope="request"/>
+<jsp:useBean id="statsHideZeros" type="java.lang.Boolean" scope="request"/>
 
 <div id="tool-area">
     <a href="./projects/versions/edit" class="button"><fmt:message key="button.version.create"/></a>
     <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a>
+    <a href="./projects/issues/" class="button"><fmt:message key="button.issue.list"/></a>
+    <c:if test="${not statsHideZeros}">
+    <a href="./projects/view?reduced=1" class="button"><fmt:message key="button.stats.hidezeros"/></a>
+    </c:if>
+    <c:if test="${statsHideZeros}">
+    <a href="./projects/view?reduced=0" class="button"><fmt:message key="button.stats.showzeros"/></a>
+    </c:if>
 </div>
 
-<c:if test="${not empty versions}">
-    <table id="version-list" class="datatable medskip">
-        <thead>
-        <tr>
-            <th></th>
-            <th><fmt:message key="thead.version.name"/></th>
-            <th><fmt:message key="thead.version.status"/></th>
-        </tr>
-        </thead>
-        <tbody>
-        <c:forEach var="version" items="${versions}">
-            <tr class="nowrap">
-                <td style="width: 2em;"><a href="./projects/versions/edit?id=${version.id}">&#x270e;</a>
-                </td>
-                <td><c:out value="${version.name}"/></td>
-                <td><fmt:message key="version.status.${version.status}"/></td>
-            </tr>
-        </c:forEach>
-        </tbody>
-    </table>
-</c:if>
+<div id="version-stats">
+<c:forEach var="version" items="${versions}" varStatus="iter">
+    <h2>
+        <fmt:message key="version.label" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/>
+        <a href="./projects/versions/edit?id=${version.id}">&#x270e;</a>
+    </h2>
 
-<table id="issue-list" class="datatable medskip">
-    <thead>
-    <tr>
-        <th><fmt:message key="thead.issue.subject"/></th>
-        <th><fmt:message key="thead.issue.assignee"/></th>
-        <th><fmt:message key="thead.issue.category"/></th>
-        <th><fmt:message key="thead.issue.status"/></th>
-        <th><fmt:message key="thead.issue.created"/></th>
-        <th><fmt:message key="thead.issue.updated"/></th>
-        <th><fmt:message key="thead.issue.eta"/></th>
-        <!-- TODO: add other information -->
-    </tr>
-    </thead>
-    <tbody>
-    <c:forEach var="issue" items="${issues}">
-        <tr>
-            <td>
-                <a href="./projects/issues/edit?id=${issue.id}">
-                <c:out value="${issue.subject}" />
-                </a>
-            </td>
-            <td>
-                <c:if test="${not empty issue.assignee}">
-                    <c:out value="${issue.assignee.shortDisplayname}" />
-                </c:if>
-                <c:if test="${empty issue.assignee}">
-                    <fmt:message key="placeholder.null-assignee" />
-                </c:if>
-            </td>
-            <td>
-                <fmt:message key="issue.category.${issue.category}" />
-            </td>
-            <td>
-                <fmt:message key="issue.status.${issue.status}" />
-            </td>
-            <td>
-                <fmt:formatDate value="${issue.created}" type="BOTH"/>
-            </td>
-            <td>
-                <fmt:formatDate value="${issue.updated}" type="BOTH"/>
-            </td>
-            <td>
-                <fmt:formatDate value="${issue.eta}" />
-            </td>
-        </tr>
-    </c:forEach>
-    </tbody>
-</table>
+    <h3><fmt:message key="version.statistics.affected" /></h3>
+    <c:set var="stats" value="${statsAffected[iter.index]}" />
+    <%@include file="../jspf/version-stats.jsp" %>
+
+    <h3><fmt:message key="version.statistics.scheduled" /></h3>
+    <c:set var="stats" value="${statsScheduled[iter.index]}" />
+    <%@include file="../jspf/version-stats.jsp" %>
+
+    <h3><fmt:message key="version.statistics.resolved" /></h3>
+    <c:set var="stats" value="${statsResolved[iter.index]}" />
+    <%@include file="../jspf/version-stats.jsp" %>
+</c:forEach>
+</div>
--- a/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sat May 23 14:13:09 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sun May 24 15:30:43 2020 +0200
@@ -32,6 +32,11 @@
 <jsp:useBean id="version" type="de.uapcore.lightpit.entities.Version" scope="request"/>
 <jsp:useBean id="versionStatusEnum" type="de.uapcore.lightpit.entities.VersionStatus[]" scope="request"/>
 
+<jsp:useBean id="statsAffected" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/>
+<jsp:useBean id="statsScheduled" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/>
+<jsp:useBean id="statsResolved" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/>
+<jsp:useBean id="statsHideZeros" type="java.lang.Boolean" scope="request"/>
+
 <form action="./projects/versions/commit" method="post">
     <table class="formtable" style="width: 35ch">
         <colgroup>
@@ -95,3 +100,15 @@
         </tfoot>
     </table>
 </form>
+
+<h3><fmt:message key="version.statistics.affected" /></h3>
+<c:set var="stats" value="${statsAffected}" />
+<%@include file="../jspf/version-stats.jsp" %>
+
+<h3><fmt:message key="version.statistics.scheduled" /></h3>
+<c:set var="stats" value="${statsScheduled}" />
+<%@include file="../jspf/version-stats.jsp" %>
+
+<h3><fmt:message key="version.statistics.resolved" /></h3>
+<c:set var="stats" value="${statsResolved}" />
+<%@include file="../jspf/version-stats.jsp" %>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/jspf/version-stats.jsp	Sun May 24 15:30:43 2020 +0200
@@ -0,0 +1,54 @@
+<%@ taglib uri = "http://java.sun.com/jsp/jstl/functions" prefix = "fn" %>
+
+<table class="datatable">
+    <c:if test="${statsHideZeros}">
+        <c:set var="visibleColumns" value="0"/>
+        <c:forEach var="idx" begin="0" end="${fn:length(issueStatusEnum)-1}">
+            <c:set var="visibleColumns" value="${visibleColumns + (stats.columnTotals[idx] eq 0 ? 0 :1)}"/>
+        </c:forEach>
+    </c:if>
+    <c:if test="${not statsHideZeros}">
+        <c:set var="visibleColumns" value="${fn:length(issueStatusEnum)}" />
+    </c:if>
+    <c:set var="colwidth"><fmt:formatNumber value="${100/(visibleColumns+2)}" maxFractionDigits="0" /></c:set>
+    <colgroup>
+        <c:forEach var="idx" begin="1" end="${visibleColumns+2}">
+        <col width="${colwidth}%">
+        </c:forEach>
+    </colgroup>
+    <thead>
+        <tr>
+            <th></th>
+            <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter">
+                <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}">
+                <th class="hcenter"><fmt:message key="issue.status.${issueStatus}"/></th>
+                </c:if>
+            </c:forEach>
+            <th class="hcenter"><fmt:message key="version.statistics.total"/> </th>
+        </tr>
+    </thead>
+    <tbody>
+    <c:forEach var="issueCategory" items="${issueCategoryEnum}" varStatus="categoryIter">
+        <c:if test="${not statsHideZeros or stats.rowTotals[categoryIter.index] gt 0}">
+        <tr>
+        <th><fmt:message key="issue.category.${issueCategory}" /></th>
+        <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter">
+            <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}">
+            <td>${stats.issueCount[categoryIter.index][statusIter.index]}</td>
+            </c:if>
+        </c:forEach>
+        <td>${stats.rowTotals[categoryIter.index]}</td>
+        </tr>
+        </c:if>
+    </c:forEach>
+    <tr>
+        <th><fmt:message key="version.statistics.total"/> </th>
+        <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter">
+            <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}">
+            <td>${stats.columnTotals[statusIter.index]}</td>
+            </c:if>
+        </c:forEach>
+        <td>${stats.total}</td>
+    </tr>
+    </tbody>
+</table>
\ No newline at end of file
--- a/src/main/webapp/lightpit.css	Sat May 23 14:13:09 2020 +0200
+++ b/src/main/webapp/lightpit.css	Sun May 24 15:30:43 2020 +0200
@@ -120,7 +120,7 @@
     width: auto;
     border-style: solid;
     border-width: 1pt;
-    border-color: black;
+    border-color: silver;
     border-collapse: collapse;
 }
 
@@ -178,6 +178,10 @@
     text-align: center;
 }
 
+.hright {
+    text-align: right;
+}
+
 .smalltext {
     font-size: smaller;
 }
--- a/src/main/webapp/projects.css	Sat May 23 14:13:09 2020 +0200
+++ b/src/main/webapp/projects.css	Sun May 24 15:30:43 2020 +0200
@@ -29,4 +29,12 @@
 
 #issue-list td {
     white-space: nowrap;
+}
+
+#version-stats h2 {
+    white-space: nowrap;
+}
+
+#version-stats td {
+    text-align: right;
 }
\ No newline at end of file

mercurial