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
     1.1 --- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Sat May 23 14:13:09 2020 +0200
     1.2 +++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Sun May 24 15:30:43 2020 +0200
     1.3 @@ -282,6 +282,13 @@
     1.4      protected <T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) {
     1.5          final String paramValue = req.getParameter(name);
     1.6          if (paramValue == null) return Optional.empty();
     1.7 +        if (clazz.equals(Boolean.class)) {
     1.8 +            if (paramValue.toLowerCase().equals("false") || paramValue.equals("0")) {
     1.9 +                return Optional.of((T)Boolean.FALSE);
    1.10 +            } else {
    1.11 +                return Optional.of((T)Boolean.TRUE);
    1.12 +            }
    1.13 +        }
    1.14          if (clazz.equals(String.class)) return Optional.of((T) paramValue);
    1.15          if (java.sql.Date.class.isAssignableFrom(clazz)) {
    1.16              try {
     2.1 --- a/src/main/java/de/uapcore/lightpit/dao/VersionDao.java	Sat May 23 14:13:09 2020 +0200
     2.2 +++ b/src/main/java/de/uapcore/lightpit/dao/VersionDao.java	Sun May 24 15:30:43 2020 +0200
     2.3 @@ -30,6 +30,7 @@
     2.4  
     2.5  import de.uapcore.lightpit.entities.Project;
     2.6  import de.uapcore.lightpit.entities.Version;
     2.7 +import de.uapcore.lightpit.entities.VersionStatistics;
     2.8  
     2.9  import java.sql.SQLException;
    2.10  import java.util.List;
    2.11 @@ -44,4 +45,31 @@
    2.12       * @throws SQLException on any kind of SQL error
    2.13       */
    2.14      List<Version> list(Project project) throws SQLException;
    2.15 +
    2.16 +    /**
    2.17 +     * Retrieves statistics about issues that arose in a version.
    2.18 +     *
    2.19 +     * @param version the version
    2.20 +     * @return version statistics
    2.21 +     * @throws SQLException on any kind of SQL error
    2.22 +     */
    2.23 +    VersionStatistics statsOpenedIssues(Version version) throws SQLException;
    2.24 +
    2.25 +    /**
    2.26 +     * Retrieves statistics about issues that are scheduled for a version.
    2.27 +     *
    2.28 +     * @param version the version
    2.29 +     * @return version statistics
    2.30 +     * @throws SQLException on any kind of SQL error
    2.31 +     */
    2.32 +    VersionStatistics statsScheduledIssues(Version version) throws SQLException;
    2.33 +
    2.34 +    /**
    2.35 +     * Retrieves statistics about issues that are resolved in a version.
    2.36 +     *
    2.37 +     * @param version the version
    2.38 +     * @return version statistics
    2.39 +     * @throws SQLException on any kind of SQL error
    2.40 +     */
    2.41 +    VersionStatistics statsResolvedIssues(Version version) throws SQLException;
    2.42  }
     3.1 --- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Sat May 23 14:13:09 2020 +0200
     3.2 +++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Sun May 24 15:30:43 2020 +0200
     3.3 @@ -29,9 +29,7 @@
     3.4  package de.uapcore.lightpit.dao.postgres;
     3.5  
     3.6  import de.uapcore.lightpit.dao.VersionDao;
     3.7 -import de.uapcore.lightpit.entities.Project;
     3.8 -import de.uapcore.lightpit.entities.Version;
     3.9 -import de.uapcore.lightpit.entities.VersionStatus;
    3.10 +import de.uapcore.lightpit.entities.*;
    3.11  
    3.12  import java.sql.Connection;
    3.13  import java.sql.PreparedStatement;
    3.14 @@ -44,13 +42,14 @@
    3.15  public final class PGVersionDao implements VersionDao {
    3.16  
    3.17      private final PreparedStatement insert, update, list, find;
    3.18 +    private final PreparedStatement issuesAffected, issuesScheduled, issuesResolved;
    3.19  
    3.20      public PGVersionDao(Connection connection) throws SQLException {
    3.21          list = connection.prepareStatement(
    3.22                  "select versionid, project, name, ordinal, status " +
    3.23                          "from lpit_version " +
    3.24                          "where project = ? " +
    3.25 -                        "order by ordinal, lower(name)");
    3.26 +                        "order by ordinal desc, lower(name) desc");
    3.27  
    3.28          find = connection.prepareStatement(
    3.29                  "select versionid, project, name, ordinal, status " +
    3.30 @@ -63,6 +62,28 @@
    3.31          update = connection.prepareStatement(
    3.32                  "update lpit_version set name = ?, ordinal = ?, status = ?::version_status where versionid = ?"
    3.33          );
    3.34 +
    3.35 +        issuesAffected = connection.prepareStatement(
    3.36 +                "select category, status, count(*) as issuecount " +
    3.37 +                        "from lpit_issue_affected_version " +
    3.38 +                        "join lpit_issue using (issueid) " +
    3.39 +                        "where versionid = ? " +
    3.40 +                        "group by category, status"
    3.41 +        );
    3.42 +        issuesScheduled = connection.prepareStatement(
    3.43 +                "select category, status, count(*) as issuecount " +
    3.44 +                        "from lpit_issue_scheduled_version " +
    3.45 +                        "join lpit_issue using (issueid) " +
    3.46 +                        "where versionid = ? " +
    3.47 +                        "group by category, status"
    3.48 +        );
    3.49 +        issuesResolved = connection.prepareStatement(
    3.50 +                "select category, status, count(*) as issuecount " +
    3.51 +                        "from lpit_issue_resolved_version " +
    3.52 +                        "join lpit_issue using (issueid) " +
    3.53 +                        "where versionid = ? " +
    3.54 +                        "group by category, status"
    3.55 +        );
    3.56      }
    3.57  
    3.58      private Version mapColumns(ResultSet result) throws SQLException {
    3.59 @@ -74,6 +95,20 @@
    3.60          return version;
    3.61      }
    3.62  
    3.63 +    private VersionStatistics versionStatistics(Version version, PreparedStatement stmt) throws SQLException {
    3.64 +        stmt.setInt(1, version.getId());
    3.65 +        final var result = stmt.executeQuery();
    3.66 +        final var stats = new VersionStatistics(version);
    3.67 +        while (result.next()) {
    3.68 +            stats.setIssueCount(
    3.69 +                    IssueCategory.valueOf(result.getString("category")),
    3.70 +                    IssueStatus.valueOf(result.getString("status")),
    3.71 +                    result.getInt("issuecount")
    3.72 +            );
    3.73 +        }
    3.74 +        return stats;
    3.75 +    }
    3.76 +
    3.77      @Override
    3.78      public void save(Version instance) throws SQLException {
    3.79          Objects.requireNonNull(instance.getName());
    3.80 @@ -121,4 +156,19 @@
    3.81              }
    3.82          }
    3.83      }
    3.84 +
    3.85 +    @Override
    3.86 +    public VersionStatistics statsOpenedIssues(Version version) throws SQLException {
    3.87 +        return versionStatistics(version, issuesAffected);
    3.88 +    }
    3.89 +
    3.90 +    @Override
    3.91 +    public VersionStatistics statsScheduledIssues(Version version) throws SQLException {
    3.92 +        return versionStatistics(version, issuesScheduled);
    3.93 +    }
    3.94 +
    3.95 +    @Override
    3.96 +    public VersionStatistics statsResolvedIssues(Version version) throws SQLException {
    3.97 +        return versionStatistics(version, issuesResolved);
    3.98 +    }
    3.99  }
     4.1 --- a/src/main/java/de/uapcore/lightpit/entities/IssueStatus.java	Sat May 23 14:13:09 2020 +0200
     4.2 +++ b/src/main/java/de/uapcore/lightpit/entities/IssueStatus.java	Sun May 24 15:30:43 2020 +0200
     4.3 @@ -29,12 +29,27 @@
     4.4  package de.uapcore.lightpit.entities;
     4.5  
     4.6  public enum IssueStatus {
     4.7 -    InSpecification,
     4.8 -    ToDo,
     4.9 -    Scheduled,
    4.10 -    InProgress,
    4.11 -    InReview,
    4.12 -    Done,
    4.13 -    Rejected,
    4.14 -    Withdrawn
    4.15 +    InSpecification(0),
    4.16 +    ToDo(0),
    4.17 +    Scheduled(1),
    4.18 +    InProgress(1),
    4.19 +    InReview(1),
    4.20 +    Done(2),
    4.21 +    Rejected(2),
    4.22 +    Withdrawn(2),
    4.23 +    Duplicate(2);
    4.24 +
    4.25 +    private int phase;
    4.26 +
    4.27 +    IssueStatus(int phase) {
    4.28 +        this.phase = phase;
    4.29 +    }
    4.30 +
    4.31 +    public int getPhase() {
    4.32 +        return phase;
    4.33 +    }
    4.34 +
    4.35 +    public static int phaseCount() {
    4.36 +        return 3;
    4.37 +    }
    4.38  }
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/src/main/java/de/uapcore/lightpit/entities/VersionStatistics.java	Sun May 24 15:30:43 2020 +0200
     5.3 @@ -0,0 +1,101 @@
     5.4 +/*
     5.5 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     5.6 + *
     5.7 + * Copyright 2018 Mike Becker. All rights reserved.
     5.8 + *
     5.9 + * Redistribution and use in source and binary forms, with or without
    5.10 + * modification, are permitted provided that the following conditions are met:
    5.11 + *
    5.12 + *   1. Redistributions of source code must retain the above copyright
    5.13 + *      notice, this list of conditions and the following disclaimer.
    5.14 + *
    5.15 + *   2. Redistributions in binary form must reproduce the above copyright
    5.16 + *      notice, this list of conditions and the following disclaimer in the
    5.17 + *      documentation and/or other materials provided with the distribution.
    5.18 + *
    5.19 + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    5.20 + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    5.21 + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    5.22 + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    5.23 + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    5.24 + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    5.25 + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    5.26 + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    5.27 + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    5.28 + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    5.29 + * POSSIBILITY OF SUCH DAMAGE.
    5.30 + *
    5.31 + */
    5.32 +package de.uapcore.lightpit.entities;
    5.33 +
    5.34 +public class VersionStatistics {
    5.35 +
    5.36 +    private final Version version;
    5.37 +    private int[][] issueCount;
    5.38 +
    5.39 +    private int[] rowTotals = null;
    5.40 +    private int[] columnTotals = null;
    5.41 +    private int total = -1;
    5.42 +
    5.43 +    public VersionStatistics(Version version) {
    5.44 +        this.version = version;
    5.45 +        issueCount = new int[IssueCategory.values().length][IssueStatus.values().length];
    5.46 +    }
    5.47 +
    5.48 +    public Version getVersion() {
    5.49 +        return version;
    5.50 +    }
    5.51 +
    5.52 +    public void setIssueCount(IssueCategory category, IssueStatus status, int count) {
    5.53 +        issueCount[category.ordinal()][status.ordinal()] = count;
    5.54 +        total = -1;
    5.55 +        rowTotals = columnTotals = null;
    5.56 +    }
    5.57 +
    5.58 +    public int[][] getIssueCount() {
    5.59 +        return issueCount;
    5.60 +    }
    5.61 +
    5.62 +    public int[] getRowTotals() {
    5.63 +        if (rowTotals != null) return rowTotals;
    5.64 +        final int cn = IssueCategory.values().length;
    5.65 +        final int sn = IssueStatus.values().length;
    5.66 +        final var totals = new int[cn];
    5.67 +        for (int i = 0 ; i < cn ; i++) {
    5.68 +            totals[i] = 0;
    5.69 +            for (int j = 0 ; j < sn ; j++) {
    5.70 +                totals[i] += issueCount[i][j];
    5.71 +            }
    5.72 +        }
    5.73 +        return rowTotals = totals;
    5.74 +    }
    5.75 +
    5.76 +    public int[] getColumnTotals() {
    5.77 +        if (columnTotals != null) return columnTotals;
    5.78 +        final int cn = IssueCategory.values().length;
    5.79 +        final int sn = IssueStatus.values().length;
    5.80 +        final var totals = new int[sn];
    5.81 +        for (int i = 0 ; i < sn ; i++) {
    5.82 +            totals[i] = 0;
    5.83 +            for (int j = 0 ; j < cn ; j++) {
    5.84 +                totals[i] += issueCount[j][i];
    5.85 +            }
    5.86 +        }
    5.87 +        return columnTotals = totals;
    5.88 +    }
    5.89 +
    5.90 +    public int getTotal() {
    5.91 +        if (this.total >= 0) {
    5.92 +            return this.total;
    5.93 +        }
    5.94 +        int total = 0;
    5.95 +        final int cn = IssueCategory.values().length;
    5.96 +        final int sn = IssueStatus.values().length;
    5.97 +        for (int i = 0 ; i < sn ; i++) {
    5.98 +            for (int j = 0 ; j < cn ; j++) {
    5.99 +                total += issueCount[j][i];
   5.100 +            }
   5.101 +        }
   5.102 +        return this.total = total;
   5.103 +    }
   5.104 +}
     6.1 --- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sat May 23 14:13:09 2020 +0200
     6.2 +++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sun May 24 15:30:43 2020 +0200
     6.3 @@ -57,9 +57,10 @@
     6.4  
     6.5      private static final Logger LOG = LoggerFactory.getLogger(ProjectsModule.class);
     6.6  
     6.7 -    public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected-project");
     6.8 -    public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected-issue");
     6.9 -    public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected-version");
    6.10 +    public static final String SESSION_ATTR_SELECTED_PROJECT = fqn(ProjectsModule.class, "selected_project");
    6.11 +    public static final String SESSION_ATTR_SELECTED_ISSUE = fqn(ProjectsModule.class, "selected_issue");
    6.12 +    public static final String SESSION_ATTR_SELECTED_VERSION = fqn(ProjectsModule.class, "selected_version");
    6.13 +    public static final String SESSION_ATTR_HIDE_ZEROS = fqn(ProjectsModule.class, "stats_hide_zeros");
    6.14  
    6.15      private class SessionSelection {
    6.16          final HttpSession session;
    6.17 @@ -128,15 +129,40 @@
    6.18          }
    6.19      }
    6.20  
    6.21 +    private void setAttributeHideZeros(HttpServletRequest req) {
    6.22 +        final Boolean value;
    6.23 +        final var param = getParameter(req, Boolean.class, "reduced");
    6.24 +        if (param.isPresent()) {
    6.25 +            value = param.get();
    6.26 +            req.getSession().setAttribute(SESSION_ATTR_HIDE_ZEROS, value);
    6.27 +        } else {
    6.28 +            final var sessionValue = req.getSession().getAttribute(SESSION_ATTR_HIDE_ZEROS);
    6.29 +            if (sessionValue != null) {
    6.30 +                value = (Boolean) sessionValue;
    6.31 +            } else {
    6.32 +                value = false;
    6.33 +                req.getSession().setAttribute(SESSION_ATTR_HIDE_ZEROS, value);
    6.34 +            }
    6.35 +        }
    6.36 +        req.setAttribute("statsHideZeros", value);
    6.37 +    }
    6.38 +
    6.39      @Override
    6.40      protected String getResourceBundleName() {
    6.41          return "localization.projects";
    6.42      }
    6.43  
    6.44 +
    6.45 +    private static final int BREADCRUMB_LEVEL_ROOT = 0;
    6.46 +    private static final int BREADCRUMB_LEVEL_PROJECT = 1;
    6.47 +    private static final int BREADCRUMB_LEVEL_VERSION = 2;
    6.48 +    private static final int BREADCRUMB_LEVEL_ISSUE_LIST = 3;
    6.49 +    private static final int BREADCRUMB_LEVEL_ISSUE = 4;
    6.50 +
    6.51      /**
    6.52       * Creates the breadcrumb menu.
    6.53       *
    6.54 -     * @param level           the current active level (0: root, 1: project, 2: version, 3: issue)
    6.55 +     * @param level           the current active level (0: root, 1: project, 2: version, 3: issue list, 4: issue)
    6.56       * @param sessionSelection the currently selected objects
    6.57       * @return a dynamic breadcrumb menu trying to display as many levels as possible
    6.58       */
    6.59 @@ -147,7 +173,7 @@
    6.60          entry = new MenuEntry(new ResourceKey("localization.lightpit", "menu.projects"),
    6.61                  "projects/");
    6.62          breadcrumbs.add(entry);
    6.63 -        if (level == 0) entry.setActive(true);
    6.64 +        if (level == BREADCRUMB_LEVEL_ROOT) entry.setActive(true);
    6.65  
    6.66          if (sessionSelection.project != null) {
    6.67              if (sessionSelection.project.getId() < 0) {
    6.68 @@ -157,7 +183,7 @@
    6.69                  entry = new MenuEntry(sessionSelection.project.getName(),
    6.70                          "projects/view?pid=" + sessionSelection.project.getId());
    6.71              }
    6.72 -            if (level == 1) entry.setActive(true);
    6.73 +            if (level == BREADCRUMB_LEVEL_PROJECT) entry.setActive(true);
    6.74              breadcrumbs.add(entry);
    6.75          }
    6.76  
    6.77 @@ -170,15 +196,19 @@
    6.78                          // TODO: change link to issue overview for that version
    6.79                          "projects/versions/edit?id=" + sessionSelection.version.getId());
    6.80              }
    6.81 -            if (level == 2) entry.setActive(true);
    6.82 +            if (level == BREADCRUMB_LEVEL_VERSION) entry.setActive(true);
    6.83 +            breadcrumbs.add(entry);
    6.84 +        }
    6.85 +
    6.86 +        if (sessionSelection.project != null) {
    6.87 +            entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"),
    6.88 +                    // TODO: maybe also add selected version
    6.89 +                    "projects/issues/?pid=" + sessionSelection.project.getId());
    6.90 +            if (level == BREADCRUMB_LEVEL_ISSUE_LIST) entry.setActive(true);
    6.91              breadcrumbs.add(entry);
    6.92          }
    6.93  
    6.94          if (sessionSelection.issue != null) {
    6.95 -            entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"),
    6.96 -                    // TODO: change link to a separate issue view (maybe depending on the selected version)
    6.97 -                    "projects/view?pid=" + sessionSelection.issue.getProject().getId());
    6.98 -            breadcrumbs.add(entry);
    6.99              if (sessionSelection.issue.getId() < 0) {
   6.100                  entry = new MenuEntry(new ResourceKey("localization.projects", "button.issue.create"),
   6.101                          "projects/issues/edit");
   6.102 @@ -187,7 +217,7 @@
   6.103                          // TODO: maybe change link to a view rather than directly opening the editor
   6.104                          "projects/issues/edit?id=" + sessionSelection.issue.getId());
   6.105              }
   6.106 -            if (level == 3) entry.setActive(true);
   6.107 +            if (level == BREADCRUMB_LEVEL_ISSUE) entry.setActive(true);
   6.108              breadcrumbs.add(entry);
   6.109          }
   6.110  
   6.111 @@ -202,7 +232,7 @@
   6.112          setContentPage(req, "projects");
   6.113          setStylesheet(req, "projects");
   6.114  
   6.115 -        setBreadcrumbs(req, getBreadcrumbs(0, sessionSelection));
   6.116 +        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ROOT, sessionSelection));
   6.117  
   6.118          return ResponseType.HTML;
   6.119      }
   6.120 @@ -211,7 +241,7 @@
   6.121          req.setAttribute("project", selection.project);
   6.122          req.setAttribute("users", dao.getUserDao().list());
   6.123          setContentPage(req, "project-form");
   6.124 -        setBreadcrumbs(req, getBreadcrumbs(1, selection));
   6.125 +        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection));
   6.126      }
   6.127  
   6.128      @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
   6.129 @@ -253,29 +283,60 @@
   6.130      }
   6.131  
   6.132      @RequestMapping(requestPath = "view", method = HttpMethod.GET)
   6.133 -    public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
   6.134 +    public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
   6.135          final var sessionSelection = new SessionSelection(req, dao);
   6.136 +        if (sessionSelection.project == null) {
   6.137 +            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
   6.138 +            return ResponseType.NONE;
   6.139 +        }
   6.140  
   6.141 -        req.setAttribute("versions", dao.getVersionDao().list(sessionSelection.project));
   6.142 -        req.setAttribute("issues", dao.getIssueDao().list(sessionSelection.project));
   6.143 +        final var versionDao = dao.getVersionDao();
   6.144 +        final var versions = versionDao.list(sessionSelection.project);
   6.145 +        final var statsAffected = new ArrayList<VersionStatistics>();
   6.146 +        final var statsScheduled = new ArrayList<VersionStatistics>();
   6.147 +        final var statsResolved = new ArrayList<VersionStatistics>();
   6.148 +        for (Version version : versions) {
   6.149 +            statsAffected.add(versionDao.statsOpenedIssues(version));
   6.150 +            statsScheduled.add(versionDao.statsScheduledIssues(version));
   6.151 +            statsResolved.add(versionDao.statsResolvedIssues(version));
   6.152 +        }
   6.153  
   6.154 -        setBreadcrumbs(req, getBreadcrumbs(1, sessionSelection));
   6.155 +        setAttributeHideZeros(req);
   6.156 +
   6.157 +        req.setAttribute("versions", versions);
   6.158 +        req.setAttribute("statsAffected", statsAffected);
   6.159 +        req.setAttribute("statsScheduled", statsScheduled);
   6.160 +        req.setAttribute("statsResolved", statsResolved);
   6.161 +
   6.162 +        req.setAttribute("issueStatusEnum", IssueStatus.values());
   6.163 +        req.setAttribute("issueCategoryEnum", IssueCategory.values());
   6.164 +
   6.165 +        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, sessionSelection));
   6.166          setContentPage(req, "project-details");
   6.167 +        setStylesheet(req, "projects");
   6.168  
   6.169          return ResponseType.HTML;
   6.170      }
   6.171  
   6.172      private void configureEditVersionForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
   6.173 +        final var versionDao = dao.getVersionDao();
   6.174          req.setAttribute("projects", dao.getProjectDao().list());
   6.175          req.setAttribute("version", selection.version);
   6.176          req.setAttribute("versionStatusEnum", VersionStatus.values());
   6.177  
   6.178 +        req.setAttribute("issueStatusEnum", IssueStatus.values());
   6.179 +        req.setAttribute("issueCategoryEnum", IssueCategory.values());
   6.180 +        req.setAttribute("statsAffected", versionDao.statsOpenedIssues(selection.version));
   6.181 +        req.setAttribute("statsScheduled", versionDao.statsScheduledIssues(selection.version));
   6.182 +        req.setAttribute("statsResolved", versionDao.statsResolvedIssues(selection.version));
   6.183 +        setAttributeHideZeros(req);
   6.184 +
   6.185          setContentPage(req, "version-form");
   6.186 -        setBreadcrumbs(req, getBreadcrumbs(2, selection));
   6.187 +        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection));
   6.188      }
   6.189  
   6.190      @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET)
   6.191 -    public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
   6.192 +    public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
   6.193          final var sessionSelection = new SessionSelection(req, dao);
   6.194  
   6.195          sessionSelection.selectVersion(findByParameter(req, Integer.class, "id", dao.getVersionDao()::find)
   6.196 @@ -286,7 +347,7 @@
   6.197      }
   6.198  
   6.199      @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST)
   6.200 -    public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
   6.201 +    public ResponseType commitVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
   6.202          final var sessionSelection = new SessionSelection(req, dao);
   6.203  
   6.204          var version = new Version(-1, sessionSelection.project);
   6.205 @@ -320,11 +381,28 @@
   6.206          req.setAttribute("users", dao.getUserDao().list());
   6.207  
   6.208          setContentPage(req, "issue-form");
   6.209 -        setBreadcrumbs(req, getBreadcrumbs(3, selection));
   6.210 +        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE, selection));
   6.211 +    }
   6.212 +
   6.213 +    @RequestMapping(requestPath = "issues/", method = HttpMethod.GET)
   6.214 +    public ResponseType issues(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
   6.215 +        final var sessionSelection = new SessionSelection(req, dao);
   6.216 +        if (sessionSelection.project == null) {
   6.217 +            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
   6.218 +            return ResponseType.NONE;
   6.219 +        }
   6.220 +
   6.221 +        req.setAttribute("issues", dao.getIssueDao().list(sessionSelection.project));
   6.222 +
   6.223 +        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE_LIST, sessionSelection));
   6.224 +        setContentPage(req, "issues");
   6.225 +        setStylesheet(req, "projects");
   6.226 +
   6.227 +        return ResponseType.HTML;
   6.228      }
   6.229  
   6.230      @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET)
   6.231 -    public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
   6.232 +    public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
   6.233          final var sessionSelection = new SessionSelection(req, dao);
   6.234  
   6.235          sessionSelection.selectIssue(findByParameter(req, Integer.class, "id",
   6.236 @@ -335,7 +413,7 @@
   6.237      }
   6.238  
   6.239      @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST)
   6.240 -    public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException, SQLException {
   6.241 +    public ResponseType commitIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
   6.242          final var sessionSelection = new SessionSelection(req, dao);
   6.243  
   6.244          Issue issue = new Issue(-1, sessionSelection.project);
     7.1 --- a/src/main/resources/localization/projects.properties	Sat May 23 14:13:09 2020 +0200
     7.2 +++ b/src/main/resources/localization/projects.properties	Sun May 24 15:30:43 2020 +0200
     7.3 @@ -26,6 +26,10 @@
     7.4  button.create=New Project
     7.5  button.version.create=New Version
     7.6  button.issue.create=New Issue
     7.7 +button.issue.list=Show Issues
     7.8 +
     7.9 +button.stats.hidezeros=Reduced View
    7.10 +button.stats.showzeros=Full View
    7.11  
    7.12  no-projects=Welcome to LightPIT. Start off by creating a new project!
    7.13  
    7.14 @@ -45,12 +49,18 @@
    7.15  placeholder.null-owner=Unassigned
    7.16  placeholder.null-assignee=Unassigned
    7.17  
    7.18 +version.label=Version
    7.19  version.status.Future=Future
    7.20  version.status.Unreleased=Unreleased
    7.21  version.status.Released=Released
    7.22  version.status.LTS=LTS
    7.23  version.status.Deprecated=Deprecated
    7.24  
    7.25 +version.statistics.affected=Affected by Issues
    7.26 +version.statistics.scheduled=Scheduled Issues
    7.27 +version.statistics.resolved=Resolved Issues
    7.28 +version.statistics.total=Total
    7.29 +
    7.30  thead.issue.project=Project
    7.31  thead.issue.subject=Subject
    7.32  thead.issue.description=Description
    7.33 @@ -81,3 +91,4 @@
    7.34  issue.status.Done=Done
    7.35  issue.status.Rejected=Rejected
    7.36  issue.status.Withdrawn=Withdrawn
    7.37 +issue.status.Duplicate=Duplicate
     8.1 --- a/src/main/resources/localization/projects_de.properties	Sat May 23 14:13:09 2020 +0200
     8.2 +++ b/src/main/resources/localization/projects_de.properties	Sun May 24 15:30:43 2020 +0200
     8.3 @@ -26,6 +26,10 @@
     8.4  button.create=Neues Projekt
     8.5  button.version.create=Neue Version
     8.6  button.issue.create=Neuer Vorgang
     8.7 +button.issue.list=Vorg\u00e4nge
     8.8 +
     8.9 +button.stats.hidezeros=Reduzierte Ansicht
    8.10 +button.stats.showzeros=Komplettansicht
    8.11  
    8.12  no-projects=Wilkommen bei LightPIT. Beginnen Sie mit der Erstellung eines Projektes!
    8.13  
    8.14 @@ -45,12 +49,18 @@
    8.15  placeholder.null-owner=Nicht Zugewiesen
    8.16  placeholder.null-assignee=Niemandem
    8.17  
    8.18 +version.label=Version
    8.19  version.status.Future=Geplant
    8.20  version.status.Unreleased=Unver\u00f6ffentlicht
    8.21  version.status.Released=Ver\u00f6ffentlicht
    8.22  version.status.LTS=Langzeitsupport
    8.23  version.status.Deprecated=Veraltet
    8.24  
    8.25 +version.statistics.affected=Betroffen von Vorg\u00e4ngen
    8.26 +version.statistics.scheduled=Geplante Vorg\u00e4nge 
    8.27 +version.statistics.resolved=Gel\u00f6ste Vorg\u00e4nge
    8.28 +version.statistics.total=Summe
    8.29 +
    8.30  thead.issue.project=Projekt
    8.31  thead.issue.subject=Thema
    8.32  thead.issue.description=Beschreibung
    8.33 @@ -81,3 +91,4 @@
    8.34  issue.status.Done=Erledigt
    8.35  issue.status.Rejected=Zur\u00fcckgewiesen
    8.36  issue.status.Withdrawn=Zur\u00fcckgezogen
    8.37 +issue.status.Duplicate=Duplikat
     9.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     9.2 +++ b/src/main/webapp/WEB-INF/jsp/issues.jsp	Sun May 24 15:30:43 2020 +0200
     9.3 @@ -0,0 +1,85 @@
     9.4 +<%--
     9.5 +DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     9.6 +
     9.7 +Copyright 2018 Mike Becker. All rights reserved.
     9.8 +
     9.9 +Redistribution and use in source and binary forms, with or without
    9.10 +modification, are permitted provided that the following conditions are met:
    9.11 +
    9.12 +1. Redistributions of source code must retain the above copyright
    9.13 +notice, this list of conditions and the following disclaimer.
    9.14 +
    9.15 +2. Redistributions in binary form must reproduce the above copyright
    9.16 +notice, this list of conditions and the following disclaimer in the
    9.17 +documentation and/or other materials provided with the distribution.
    9.18 +
    9.19 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    9.20 +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    9.21 +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
    9.22 +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
    9.23 +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
    9.24 +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    9.25 +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    9.26 +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
    9.27 +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    9.28 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    9.29 +--%>
    9.30 +<%@page pageEncoding="UTF-8" %>
    9.31 +<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    9.32 +<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    9.33 +
    9.34 +<jsp:useBean id="issues" type="java.util.List<de.uapcore.lightpit.entities.Issue>" scope="request"/>
    9.35 +
    9.36 +<div id="tool-area">
    9.37 +    <div>
    9.38 +        <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a>
    9.39 +    </div>
    9.40 +</div>
    9.41 +
    9.42 +<table id="issue-list" class="datatable medskip">
    9.43 +    <thead>
    9.44 +    <tr>
    9.45 +        <th><fmt:message key="thead.issue.subject"/></th>
    9.46 +        <th><fmt:message key="thead.issue.assignee"/></th>
    9.47 +        <th><fmt:message key="thead.issue.category"/></th>
    9.48 +        <th><fmt:message key="thead.issue.status"/></th>
    9.49 +        <th><fmt:message key="thead.issue.created"/></th>
    9.50 +        <th><fmt:message key="thead.issue.updated"/></th>
    9.51 +        <th><fmt:message key="thead.issue.eta"/></th>
    9.52 +    </tr>
    9.53 +    </thead>
    9.54 +    <tbody>
    9.55 +    <c:forEach var="issue" items="${issues}">
    9.56 +        <tr>
    9.57 +            <td>
    9.58 +                <a href="./projects/issues/edit?id=${issue.id}">
    9.59 +                    <c:out value="${issue.subject}" />
    9.60 +                </a>
    9.61 +            </td>
    9.62 +            <td>
    9.63 +                <c:if test="${not empty issue.assignee}">
    9.64 +                    <c:out value="${issue.assignee.shortDisplayname}" />
    9.65 +                </c:if>
    9.66 +                <c:if test="${empty issue.assignee}">
    9.67 +                    <fmt:message key="placeholder.null-assignee" />
    9.68 +                </c:if>
    9.69 +            </td>
    9.70 +            <td>
    9.71 +                <fmt:message key="issue.category.${issue.category}" />
    9.72 +            </td>
    9.73 +            <td>
    9.74 +                <fmt:message key="issue.status.${issue.status}" />
    9.75 +            </td>
    9.76 +            <td>
    9.77 +                <fmt:formatDate value="${issue.created}" type="BOTH"/>
    9.78 +            </td>
    9.79 +            <td>
    9.80 +                <fmt:formatDate value="${issue.updated}" type="BOTH"/>
    9.81 +            </td>
    9.82 +            <td>
    9.83 +                <fmt:formatDate value="${issue.eta}" />
    9.84 +            </td>
    9.85 +        </tr>
    9.86 +    </c:forEach>
    9.87 +    </tbody>
    9.88 +</table>
    10.1 --- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sat May 23 14:13:09 2020 +0200
    10.2 +++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sun May 24 15:30:43 2020 +0200
    10.3 @@ -25,87 +25,46 @@
    10.4  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    10.5  --%>
    10.6  <%@page pageEncoding="UTF-8" %>
    10.7 -<%@page import="de.uapcore.lightpit.modules.ProjectsModule" %>
    10.8  <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    10.9  <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
   10.10  
   10.11 -<c:set scope="page" var="selectedProject" value="${sessionScope[ProjectsModule.SESSION_ATTR_SELECTED_PROJECT]}"/>
   10.12 -
   10.13  <jsp:useBean id="versions" type="java.util.List<de.uapcore.lightpit.entities.Version>" scope="request"/>
   10.14 -<jsp:useBean id="issues" type="java.util.List<de.uapcore.lightpit.entities.Issue>" scope="request"/>
   10.15 +<jsp:useBean id="statsAffected" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/>
   10.16 +<jsp:useBean id="statsScheduled" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/>
   10.17 +<jsp:useBean id="statsResolved" type="java.util.List<de.uapcore.lightpit.entities.VersionStatistics>" scope="request"/>
   10.18 +<jsp:useBean id="issueStatusEnum" type="de.uapcore.lightpit.entities.IssueStatus[]" scope="request"/>
   10.19 +<jsp:useBean id="issueCategoryEnum" type="de.uapcore.lightpit.entities.IssueCategory[]" scope="request"/>
   10.20 +<jsp:useBean id="statsHideZeros" type="java.lang.Boolean" scope="request"/>
   10.21  
   10.22  <div id="tool-area">
   10.23      <a href="./projects/versions/edit" class="button"><fmt:message key="button.version.create"/></a>
   10.24      <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a>
   10.25 +    <a href="./projects/issues/" class="button"><fmt:message key="button.issue.list"/></a>
   10.26 +    <c:if test="${not statsHideZeros}">
   10.27 +    <a href="./projects/view?reduced=1" class="button"><fmt:message key="button.stats.hidezeros"/></a>
   10.28 +    </c:if>
   10.29 +    <c:if test="${statsHideZeros}">
   10.30 +    <a href="./projects/view?reduced=0" class="button"><fmt:message key="button.stats.showzeros"/></a>
   10.31 +    </c:if>
   10.32  </div>
   10.33  
   10.34 -<c:if test="${not empty versions}">
   10.35 -    <table id="version-list" class="datatable medskip">
   10.36 -        <thead>
   10.37 -        <tr>
   10.38 -            <th></th>
   10.39 -            <th><fmt:message key="thead.version.name"/></th>
   10.40 -            <th><fmt:message key="thead.version.status"/></th>
   10.41 -        </tr>
   10.42 -        </thead>
   10.43 -        <tbody>
   10.44 -        <c:forEach var="version" items="${versions}">
   10.45 -            <tr class="nowrap">
   10.46 -                <td style="width: 2em;"><a href="./projects/versions/edit?id=${version.id}">&#x270e;</a>
   10.47 -                </td>
   10.48 -                <td><c:out value="${version.name}"/></td>
   10.49 -                <td><fmt:message key="version.status.${version.status}"/></td>
   10.50 -            </tr>
   10.51 -        </c:forEach>
   10.52 -        </tbody>
   10.53 -    </table>
   10.54 -</c:if>
   10.55 +<div id="version-stats">
   10.56 +<c:forEach var="version" items="${versions}" varStatus="iter">
   10.57 +    <h2>
   10.58 +        <fmt:message key="version.label" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/>
   10.59 +        <a href="./projects/versions/edit?id=${version.id}">&#x270e;</a>
   10.60 +    </h2>
   10.61  
   10.62 -<table id="issue-list" class="datatable medskip">
   10.63 -    <thead>
   10.64 -    <tr>
   10.65 -        <th><fmt:message key="thead.issue.subject"/></th>
   10.66 -        <th><fmt:message key="thead.issue.assignee"/></th>
   10.67 -        <th><fmt:message key="thead.issue.category"/></th>
   10.68 -        <th><fmt:message key="thead.issue.status"/></th>
   10.69 -        <th><fmt:message key="thead.issue.created"/></th>
   10.70 -        <th><fmt:message key="thead.issue.updated"/></th>
   10.71 -        <th><fmt:message key="thead.issue.eta"/></th>
   10.72 -        <!-- TODO: add other information -->
   10.73 -    </tr>
   10.74 -    </thead>
   10.75 -    <tbody>
   10.76 -    <c:forEach var="issue" items="${issues}">
   10.77 -        <tr>
   10.78 -            <td>
   10.79 -                <a href="./projects/issues/edit?id=${issue.id}">
   10.80 -                <c:out value="${issue.subject}" />
   10.81 -                </a>
   10.82 -            </td>
   10.83 -            <td>
   10.84 -                <c:if test="${not empty issue.assignee}">
   10.85 -                    <c:out value="${issue.assignee.shortDisplayname}" />
   10.86 -                </c:if>
   10.87 -                <c:if test="${empty issue.assignee}">
   10.88 -                    <fmt:message key="placeholder.null-assignee" />
   10.89 -                </c:if>
   10.90 -            </td>
   10.91 -            <td>
   10.92 -                <fmt:message key="issue.category.${issue.category}" />
   10.93 -            </td>
   10.94 -            <td>
   10.95 -                <fmt:message key="issue.status.${issue.status}" />
   10.96 -            </td>
   10.97 -            <td>
   10.98 -                <fmt:formatDate value="${issue.created}" type="BOTH"/>
   10.99 -            </td>
  10.100 -            <td>
  10.101 -                <fmt:formatDate value="${issue.updated}" type="BOTH"/>
  10.102 -            </td>
  10.103 -            <td>
  10.104 -                <fmt:formatDate value="${issue.eta}" />
  10.105 -            </td>
  10.106 -        </tr>
  10.107 -    </c:forEach>
  10.108 -    </tbody>
  10.109 -</table>
  10.110 +    <h3><fmt:message key="version.statistics.affected" /></h3>
  10.111 +    <c:set var="stats" value="${statsAffected[iter.index]}" />
  10.112 +    <%@include file="../jspf/version-stats.jsp" %>
  10.113 +
  10.114 +    <h3><fmt:message key="version.statistics.scheduled" /></h3>
  10.115 +    <c:set var="stats" value="${statsScheduled[iter.index]}" />
  10.116 +    <%@include file="../jspf/version-stats.jsp" %>
  10.117 +
  10.118 +    <h3><fmt:message key="version.statistics.resolved" /></h3>
  10.119 +    <c:set var="stats" value="${statsResolved[iter.index]}" />
  10.120 +    <%@include file="../jspf/version-stats.jsp" %>
  10.121 +</c:forEach>
  10.122 +</div>
    11.1 --- a/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sat May 23 14:13:09 2020 +0200
    11.2 +++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sun May 24 15:30:43 2020 +0200
    11.3 @@ -32,6 +32,11 @@
    11.4  <jsp:useBean id="version" type="de.uapcore.lightpit.entities.Version" scope="request"/>
    11.5  <jsp:useBean id="versionStatusEnum" type="de.uapcore.lightpit.entities.VersionStatus[]" scope="request"/>
    11.6  
    11.7 +<jsp:useBean id="statsAffected" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/>
    11.8 +<jsp:useBean id="statsScheduled" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/>
    11.9 +<jsp:useBean id="statsResolved" type="de.uapcore.lightpit.entities.VersionStatistics" scope="request"/>
   11.10 +<jsp:useBean id="statsHideZeros" type="java.lang.Boolean" scope="request"/>
   11.11 +
   11.12  <form action="./projects/versions/commit" method="post">
   11.13      <table class="formtable" style="width: 35ch">
   11.14          <colgroup>
   11.15 @@ -95,3 +100,15 @@
   11.16          </tfoot>
   11.17      </table>
   11.18  </form>
   11.19 +
   11.20 +<h3><fmt:message key="version.statistics.affected" /></h3>
   11.21 +<c:set var="stats" value="${statsAffected}" />
   11.22 +<%@include file="../jspf/version-stats.jsp" %>
   11.23 +
   11.24 +<h3><fmt:message key="version.statistics.scheduled" /></h3>
   11.25 +<c:set var="stats" value="${statsScheduled}" />
   11.26 +<%@include file="../jspf/version-stats.jsp" %>
   11.27 +
   11.28 +<h3><fmt:message key="version.statistics.resolved" /></h3>
   11.29 +<c:set var="stats" value="${statsResolved}" />
   11.30 +<%@include file="../jspf/version-stats.jsp" %>
   11.31 \ No newline at end of file
    12.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    12.2 +++ b/src/main/webapp/WEB-INF/jspf/version-stats.jsp	Sun May 24 15:30:43 2020 +0200
    12.3 @@ -0,0 +1,54 @@
    12.4 +<%@ taglib uri = "http://java.sun.com/jsp/jstl/functions" prefix = "fn" %>
    12.5 +
    12.6 +<table class="datatable">
    12.7 +    <c:if test="${statsHideZeros}">
    12.8 +        <c:set var="visibleColumns" value="0"/>
    12.9 +        <c:forEach var="idx" begin="0" end="${fn:length(issueStatusEnum)-1}">
   12.10 +            <c:set var="visibleColumns" value="${visibleColumns + (stats.columnTotals[idx] eq 0 ? 0 :1)}"/>
   12.11 +        </c:forEach>
   12.12 +    </c:if>
   12.13 +    <c:if test="${not statsHideZeros}">
   12.14 +        <c:set var="visibleColumns" value="${fn:length(issueStatusEnum)}" />
   12.15 +    </c:if>
   12.16 +    <c:set var="colwidth"><fmt:formatNumber value="${100/(visibleColumns+2)}" maxFractionDigits="0" /></c:set>
   12.17 +    <colgroup>
   12.18 +        <c:forEach var="idx" begin="1" end="${visibleColumns+2}">
   12.19 +        <col width="${colwidth}%">
   12.20 +        </c:forEach>
   12.21 +    </colgroup>
   12.22 +    <thead>
   12.23 +        <tr>
   12.24 +            <th></th>
   12.25 +            <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter">
   12.26 +                <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}">
   12.27 +                <th class="hcenter"><fmt:message key="issue.status.${issueStatus}"/></th>
   12.28 +                </c:if>
   12.29 +            </c:forEach>
   12.30 +            <th class="hcenter"><fmt:message key="version.statistics.total"/> </th>
   12.31 +        </tr>
   12.32 +    </thead>
   12.33 +    <tbody>
   12.34 +    <c:forEach var="issueCategory" items="${issueCategoryEnum}" varStatus="categoryIter">
   12.35 +        <c:if test="${not statsHideZeros or stats.rowTotals[categoryIter.index] gt 0}">
   12.36 +        <tr>
   12.37 +        <th><fmt:message key="issue.category.${issueCategory}" /></th>
   12.38 +        <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter">
   12.39 +            <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}">
   12.40 +            <td>${stats.issueCount[categoryIter.index][statusIter.index]}</td>
   12.41 +            </c:if>
   12.42 +        </c:forEach>
   12.43 +        <td>${stats.rowTotals[categoryIter.index]}</td>
   12.44 +        </tr>
   12.45 +        </c:if>
   12.46 +    </c:forEach>
   12.47 +    <tr>
   12.48 +        <th><fmt:message key="version.statistics.total"/> </th>
   12.49 +        <c:forEach var="issueStatus" items="${issueStatusEnum}" varStatus="statusIter">
   12.50 +            <c:if test="${not statsHideZeros or stats.columnTotals[statusIter.index] gt 0}">
   12.51 +            <td>${stats.columnTotals[statusIter.index]}</td>
   12.52 +            </c:if>
   12.53 +        </c:forEach>
   12.54 +        <td>${stats.total}</td>
   12.55 +    </tr>
   12.56 +    </tbody>
   12.57 +</table>
   12.58 \ No newline at end of file
    13.1 --- a/src/main/webapp/lightpit.css	Sat May 23 14:13:09 2020 +0200
    13.2 +++ b/src/main/webapp/lightpit.css	Sun May 24 15:30:43 2020 +0200
    13.3 @@ -120,7 +120,7 @@
    13.4      width: auto;
    13.5      border-style: solid;
    13.6      border-width: 1pt;
    13.7 -    border-color: black;
    13.8 +    border-color: silver;
    13.9      border-collapse: collapse;
   13.10  }
   13.11  
   13.12 @@ -178,6 +178,10 @@
   13.13      text-align: center;
   13.14  }
   13.15  
   13.16 +.hright {
   13.17 +    text-align: right;
   13.18 +}
   13.19 +
   13.20  .smalltext {
   13.21      font-size: smaller;
   13.22  }
    14.1 --- a/src/main/webapp/projects.css	Sat May 23 14:13:09 2020 +0200
    14.2 +++ b/src/main/webapp/projects.css	Sun May 24 15:30:43 2020 +0200
    14.3 @@ -29,4 +29,12 @@
    14.4  
    14.5  #issue-list td {
    14.6      white-space: nowrap;
    14.7 +}
    14.8 +
    14.9 +#version-stats h2 {
   14.10 +    white-space: nowrap;
   14.11 +}
   14.12 +
   14.13 +#version-stats td {
   14.14 +    text-align: right;
   14.15  }
   14.16 \ No newline at end of file

mercurial