improves issue overview and adds progress information

Mon, 01 Jun 2020 14:46:58 +0200

author
Mike Becker <universe@uap-core.de>
date
Mon, 01 Jun 2020 14:46:58 +0200
changeset 86
0a658e53177c
parent 85
3d16ad54b3dc
child 87
501addad452b

improves issue overview and adds progress information

pom.xml file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/Constants.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/IssueDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/ProjectDao.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/PGIssueDao.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.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/Issue.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/IssueSummary.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Project.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/entities/Version.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/LanguageModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/modules/UsersModule.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/IssuesView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/LanguageView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectIndexView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectInfo.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/UsersEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/UsersView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionInfo.java file | annotate | diff | comparison | revisions
src/main/java/de/uapcore/lightpit/viewmodel/VersionView.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/issue-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/issues.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/language.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/projects.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/site.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/user-form.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jsp/users.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/jsp/version.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/issue-list.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/issue-summary.jsp file | annotate | diff | comparison | revisions
src/main/webapp/WEB-INF/jspf/project-header.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/pom.xml	Sat May 30 18:12:38 2020 +0200
+++ b/pom.xml	Mon Jun 01 14:46:58 2020 +0200
@@ -4,7 +4,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>de.uapcore</groupId>
     <artifactId>lightpit</artifactId>
-    <version>0.1-SNAPSHOT</version>
+    <version>0.2</version>
     <packaging>war</packaging>
     <properties>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
--- a/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/AbstractLightPITServlet.java	Mon Jun 01 14:46:58 2020 +0200
@@ -88,6 +88,7 @@
 
     /**
      * Returns the name of the resource bundle associated with this servlet.
+     *
      * @return the resource bundle base name
      */
     protected abstract String getResourceBundleName();
@@ -266,6 +267,17 @@
     }
 
     /**
+     * Sets the view model object.
+     * The type must match the expected type in the JSP file.
+     *
+     * @param req       the servlet request object
+     * @param viewModel the view model object
+     */
+    public void setViewModel(HttpServletRequest req, Object viewModel) {
+        req.setAttribute(Constants.REQ_ATTR_VIEWMODEL, viewModel);
+    }
+
+    /**
      * Obtains a request parameter of the specified type.
      * The specified type must have a single-argument constructor accepting a string to perform conversion.
      * The constructor of the specified type may throw an exception on conversion failures.
@@ -281,7 +293,7 @@
             final String[] paramValues = req.getParameterValues(name);
             int len = paramValues == null ? 0 : paramValues.length;
             final var array = (T) Array.newInstance(clazz.getComponentType(), len);
-            for (int i = 0 ; i < len ; i++) {
+            for (int i = 0; i < len; i++) {
                 try {
                     final Constructor<?> ctor = clazz.getComponentType().getConstructor(String.class);
                     Array.set(array, i, ctor.newInstance(paramValues[i]));
--- a/src/main/java/de/uapcore/lightpit/Constants.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/Constants.java	Mon Jun 01 14:46:58 2020 +0200
@@ -92,6 +92,11 @@
     public static final String REQ_ATTR_CONTENT_PAGE = fqn(AbstractLightPITServlet.class, "content-page");
 
     /**
+     * Key for the view model object (the type depends on the rendered site).
+     */
+    public static final String REQ_ATTR_VIEWMODEL = "viewmodel";
+
+    /**
      * Key for the name of the additional stylesheet used by a module.
      */
     public static final String REQ_ATTR_STYLESHEET = fqn(AbstractLightPITServlet.class, "extraCss");
--- a/src/main/java/de/uapcore/lightpit/dao/IssueDao.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/IssueDao.java	Mon Jun 01 14:46:58 2020 +0200
@@ -30,6 +30,7 @@
 
 import de.uapcore.lightpit.entities.Issue;
 import de.uapcore.lightpit.entities.Project;
+import de.uapcore.lightpit.entities.Version;
 
 import java.sql.SQLException;
 import java.util.List;
@@ -48,6 +49,15 @@
     List<Issue> list(Project project) throws SQLException;
 
     /**
+     * Lists all issues that are somehow related to the specified version.
+     *
+     * @param version the version
+     * @return a list of issues
+     * @throws SQLException on any kind of SQL error
+     */
+    List<Issue> list(Version version) throws SQLException;
+
+    /**
      * Saves an instances to the database.
      * Implementations of this DAO must guarantee that the generated ID is stored in the instance.
      *
--- a/src/main/java/de/uapcore/lightpit/dao/ProjectDao.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/ProjectDao.java	Mon Jun 01 14:46:58 2020 +0200
@@ -28,6 +28,7 @@
  */
 package de.uapcore.lightpit.dao;
 
+import de.uapcore.lightpit.entities.IssueSummary;
 import de.uapcore.lightpit.entities.Project;
 
 import java.sql.SQLException;
@@ -35,4 +36,6 @@
 
 public interface ProjectDao extends GenericDao<Project> {
     List<Project> list() throws SQLException;
+
+    IssueSummary getIssueSummary(Project project) throws SQLException;
 }
--- a/src/main/java/de/uapcore/lightpit/dao/VersionDao.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/VersionDao.java	Mon Jun 01 14:46:58 2020 +0200
@@ -30,7 +30,6 @@
 
 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;
@@ -45,31 +44,4 @@
      * @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/PGIssueDao.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGIssueDao.java	Mon Jun 01 14:46:58 2020 +0200
@@ -43,7 +43,7 @@
 
 public final class PGIssueDao implements IssueDao {
 
-    private final PreparedStatement insert, update, list, find;
+    private final PreparedStatement insert, update, list, listForVersion, find;
     private final PreparedStatement affectedVersions, scheduledVersions, resolvedVersions;
     private final PreparedStatement clearAffected, clearScheduled, clearResolved;
     private final PreparedStatement insertAffected, insertScheduled, insertResolved;
@@ -56,7 +56,24 @@
                         "from lpit_issue i " +
                         "left join lpit_project p on project = projectid " +
                         "left join lpit_user on userid = assignee " +
-                        "where project = ? ");
+                        "where project = ? "+
+                        "order by eta asc, updated desc");
+
+        listForVersion = connection.prepareStatement(
+                "with issue_version as ( "+
+                        "select issueid, versionid from lpit_issue_affected_version union "+
+                        "select issueid, versionid from lpit_issue_scheduled_version union "+
+                        "select issueid, versionid from lpit_issue_resolved_version) "+
+                        "select issueid, project, p.name as projectname, status, category, subject, i.description, " +
+                        "userid, username, givenname, lastname, mail, " +
+                        "created, updated, eta " +
+                        "from lpit_issue i " +
+                        "join issue_version using (issueid) "+
+                        "left join lpit_project p on project = projectid " +
+                        "left join lpit_user on userid = assignee " +
+                        "where versionid = ? "+
+                        "order by eta asc, updated desc"
+        );
 
         find = connection.prepareStatement(
                 "select issueid, project, p.name as projectname, status, category, subject, i.description, " +
@@ -121,7 +138,8 @@
     private Issue mapColumns(ResultSet result) throws SQLException {
         final var project = new Project(result.getInt("project"));
         project.setName(result.getString("projectname"));
-        final var issue = new Issue(result.getInt("issueid"), project);
+        final var issue = new Issue(result.getInt("issueid"));
+        issue.setProject(project);
         issue.setStatus(IssueStatus.valueOf(result.getString("status")));
         issue.setCategory(IssueCategory.valueOf(result.getString("category")));
         issue.setSubject(result.getString("subject"));
@@ -133,8 +151,8 @@
         return issue;
     }
 
-    private Version mapVersion(ResultSet result, Project project) throws SQLException {
-        final var version = new Version(result.getInt("versionid"), project);
+    private Version mapVersion(ResultSet result) throws SQLException {
+        final var version = new Version(result.getInt("versionid"));
         version.setName(result.getString("name"));
         version.setOrdinal(result.getInt("ordinal"));
         version.setStatus(VersionStatus.valueOf(result.getString("status")));
@@ -203,11 +221,10 @@
         }
     }
 
-    @Override
-    public List<Issue> list(Project project) throws SQLException {
-        list.setInt(1, project.getId());
+    private List<Issue> list(PreparedStatement query, int arg) throws SQLException {
+        query.setInt(1, arg);
         List<Issue> issues = new ArrayList<>();
-        try (var result = list.executeQuery()) {
+        try (var result = query.executeQuery()) {
             while (result.next()) {
                 issues.add(mapColumns(result));
             }
@@ -216,6 +233,16 @@
     }
 
     @Override
+    public List<Issue> list(Project project) throws SQLException {
+        return list(list, project.getId());
+    }
+
+    @Override
+    public List<Issue> list(Version version) throws SQLException {
+        return list(listForVersion, version.getId());
+    }
+
+    @Override
     public Issue find(int id) throws SQLException {
         find.setInt(1, id);
         try (var result = find.executeQuery()) {
@@ -232,7 +259,7 @@
         List<Version> versions = new ArrayList<>();
         try (var result = stmt.executeQuery()) {
             while (result.next()) {
-                versions.add(mapVersion(result, issue.getProject()));
+                versions.add(mapVersion(result));
             }
         }
         return versions;
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGProjectDao.java	Mon Jun 01 14:46:58 2020 +0200
@@ -29,6 +29,7 @@
 package de.uapcore.lightpit.dao.postgres;
 
 import de.uapcore.lightpit.dao.ProjectDao;
+import de.uapcore.lightpit.entities.IssueSummary;
 import de.uapcore.lightpit.entities.Project;
 import de.uapcore.lightpit.entities.User;
 
@@ -98,24 +99,26 @@
         return proj;
     }
 
-    private void mapIssueSummary(Project proj) throws SQLException {
-        issue_summary.setInt(1, proj.getId());
+    public IssueSummary getIssueSummary(Project project) throws SQLException {
+        issue_summary.setInt(1, project.getId());
         final var result = issue_summary.executeQuery();
+        final var summary = new IssueSummary();
         while (result.next()) {
             final var phase = result.getInt("phase");
             final var total = result.getInt("total");
             switch(phase) {
                 case 0:
-                    proj.setOpenIssues(total);
+                    summary.setOpen(total);
                     break;
                 case 1:
-                    proj.setActiveIssues(total);
+                    summary.setActive(total);
                     break;
                 case 2:
-                    proj.setDoneIssues(total);
+                    summary.setDone(total);
                     break;
             }
         }
+        return summary;
     }
 
     @Override
@@ -146,7 +149,6 @@
         try (var result = list.executeQuery()) {
             while (result.next()) {
                 final var project = mapColumns(result);
-                mapIssueSummary(project);
                 projects.add(project);
             }
         }
@@ -159,7 +161,6 @@
         try (var result = find.executeQuery()) {
             if (result.next()) {
                 final var project = mapColumns(result);
-                mapIssueSummary(project);
                 return project;
             } else {
                 return null;
--- a/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/dao/postgres/PGVersionDao.java	Mon Jun 01 14:46:58 2020 +0200
@@ -29,7 +29,9 @@
 package de.uapcore.lightpit.dao.postgres;
 
 import de.uapcore.lightpit.dao.VersionDao;
-import de.uapcore.lightpit.entities.*;
+import de.uapcore.lightpit.entities.Project;
+import de.uapcore.lightpit.entities.Version;
+import de.uapcore.lightpit.entities.VersionStatus;
 
 import java.sql.Connection;
 import java.sql.PreparedStatement;
@@ -42,7 +44,6 @@
 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(
@@ -64,54 +65,19 @@
         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 {
         final var project = new Project(result.getInt("project"));
         project.setName(result.getString("projectname"));
-        final var version = new Version(result.getInt("versionid"), project);
+        final var version = new Version(result.getInt("versionid"));
+        version.setProject(project);
         version.setName(result.getString("name"));
         version.setOrdinal(result.getInt("ordinal"));
         version.setStatus(VersionStatus.valueOf(result.getString("status")));
         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());
@@ -159,19 +125,4 @@
             }
         }
     }
-
-    @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/Issue.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/entities/Issue.java	Mon Jun 01 14:46:58 2020 +0200
@@ -38,7 +38,7 @@
 public final class Issue {
 
     private int id;
-    private final Project project;
+    private Project project;
 
     private IssueStatus status;
     private IssueCategory category;
@@ -55,9 +55,8 @@
     private Timestamp updated = Timestamp.from(Instant.now());
     private Date eta;
 
-    public Issue(int id, Project project) {
+    public Issue(int id) {
         this.id = id;
-        this.project = project;
     }
 
     public int getId() {
@@ -72,6 +71,10 @@
         this.id = id;
     }
 
+    public void setProject(Project project) {
+        this.project = project;
+    }
+
     public Project getProject() {
         return project;
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/entities/IssueSummary.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,67 @@
+package de.uapcore.lightpit.entities;
+
+public class IssueSummary {
+    private int open = 0;
+    private int active = 0;
+    private int done = 0;
+
+    public int getOpen() {
+        return open;
+    }
+
+    public void setOpen(int open) {
+        this.open = open;
+    }
+
+    public int getActive() {
+        return active;
+    }
+
+    public void setActive(int active) {
+        this.active = active;
+    }
+
+    public int getDone() {
+        return done;
+    }
+
+    public void setDone(int done) {
+        this.done = done;
+    }
+
+    public int getTotal() {
+        return open+active+done;
+    }
+
+    public int getOpenPercent() {
+        return 100-getActivePercent()-getDonePercent();
+    }
+
+    public int getActivePercent() {
+        int total = getTotal();
+        return total > 0 ? 100*active/total : 0;
+    }
+
+    public int getDonePercent() {
+        int total = getTotal();
+        return total > 0 ? 100*done/total : 0;
+    }
+
+    /**
+     * Adds the specified issue to the summary by increming the respective counter.
+     * @param issue the issue
+     */
+    public void add(Issue issue) {
+        switch (issue.getStatus().getPhase()) {
+            case 0:
+                open++;
+                break;
+            case 1:
+                active++;
+                break;
+            case 2:
+                done++;
+                break;
+        }
+    }
+}
--- a/src/main/java/de/uapcore/lightpit/entities/Project.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/entities/Project.java	Mon Jun 01 14:46:58 2020 +0200
@@ -38,10 +38,6 @@
     private String repoUrl;
     private User owner;
 
-    private int openIssues;
-    private int activeIssues;
-    private int doneIssues;
-
     public Project(int id) {
         this.id = id;
     }
@@ -82,30 +78,6 @@
         this.owner = owner;
     }
 
-    public int getOpenIssues() {
-        return openIssues;
-    }
-
-    public void setOpenIssues(int openIssues) {
-        this.openIssues = openIssues;
-    }
-
-    public int getActiveIssues() {
-        return activeIssues;
-    }
-
-    public void setActiveIssues(int activeIssues) {
-        this.activeIssues = activeIssues;
-    }
-
-    public int getDoneIssues() {
-        return doneIssues;
-    }
-
-    public void setDoneIssues(int doneIssues) {
-        this.doneIssues = doneIssues;
-    }
-
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
--- a/src/main/java/de/uapcore/lightpit/entities/Version.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/entities/Version.java	Mon Jun 01 14:46:58 2020 +0200
@@ -41,9 +41,8 @@
     private int ordinal = 0;
     private VersionStatus status = VersionStatus.Future;
 
-    public Version(int id, Project project) {
+    public Version(int id) {
         this.id = id;
-        this.project = project;
     }
 
     public int getId() {
--- a/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/modules/LanguageModule.java	Mon Jun 01 14:46:58 2020 +0200
@@ -29,6 +29,7 @@
 package de.uapcore.lightpit.modules;
 
 import de.uapcore.lightpit.*;
+import de.uapcore.lightpit.viewmodel.LanguageView;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -86,9 +87,12 @@
     @RequestMapping(method = HttpMethod.GET)
     public ResponseType handle(HttpServletRequest req) {
 
-        req.setAttribute("languages", languages);
-        req.setAttribute("browserLanguage", req.getLocale());
+        final var viewModel = new LanguageView();
+        viewModel.setLanguages(languages);
+        viewModel.setBrowserLanguage(req.getLocale());
+        viewModel.setCurrentLanguage((Locale)req.getSession().getAttribute(Constants.SESSION_ATTR_LANGUAGE));
 
+        setViewModel(req, viewModel);
         setStylesheet(req, "language");
         setContentPage(req, "language");
         return ResponseType.HTML;
--- a/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/modules/ProjectsModule.java	Mon Jun 01 14:46:58 2020 +0200
@@ -32,6 +32,7 @@
 import de.uapcore.lightpit.*;
 import de.uapcore.lightpit.dao.DataAccessObjects;
 import de.uapcore.lightpit.entities.*;
+import de.uapcore.lightpit.viewmodel.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -42,7 +43,10 @@
 import java.io.IOException;
 import java.sql.Date;
 import java.sql.SQLException;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Objects;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -59,41 +63,81 @@
     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;
+        final HttpServletRequest req;
+        final DataAccessObjects dao;
         Project project;
         Version version;
         Issue issue;
 
-        SessionSelection(HttpServletRequest req, Project project) {
-            this.session = req.getSession();
-            this.project = project;
+        SessionSelection(HttpServletRequest req, DataAccessObjects dao) {
+            this.req = req;
+            this.dao = dao;
+            session = req.getSession();
+        }
+
+        void newProject() {
+            project = null;
             version = null;
             issue = null;
             updateAttributes();
+            project = new Project(-1);
+            updateAttributes();
+        }
+
+        void newVersion() throws SQLException {
+            project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
+            syncProject();
+            version = null;
+            issue = null;
+            updateAttributes();
+            version = new Version(-1);
+            version.setProject(project);
+            updateAttributes();
         }
 
-        SessionSelection(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
-            this.session = req.getSession();
-            final var issueDao = dao.getIssueDao();
-            final var projectDao = dao.getProjectDao();
-            final var issueSelection = getParameter(req, Integer.class, "issue");
-            if (issueSelection.isPresent()) {
-                issue = issueDao.find(issueSelection.get());
-            } else {
-                final var issue = (Issue) session.getAttribute(SESSION_ATTR_SELECTED_ISSUE);
-                this.issue = issue == null ? null : issueDao.find(issue.getId());
+        void newIssue() throws SQLException {
+            project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
+            syncProject();
+            version = null;
+            issue = null;
+            updateAttributes();
+            issue = new Issue(-1);
+            issue.setProject(project);
+            updateAttributes();
+        }
+
+        void selectVersion(Version selectedVersion) throws SQLException {
+            issue = null;
+            version = selectedVersion;
+            if (!version.getProject().equals(project)) {
+                project = dao.getProjectDao().find(version.getProject().getId());
             }
-            if (issue != null) {
-                version = null; // show the issue globally
-                project = projectDao.find(issue.getProject().getId());
+            // our object contains more details
+            version.setProject(project);
+            updateAttributes();
+        }
+
+        void selectIssue(Issue selectedIssue) throws SQLException {
+            issue = selectedIssue;
+            if (!issue.getProject().equals(project)) {
+                project = dao.getProjectDao().find(issue.getProject().getId());
             }
+            // our object contains more details
+            issue.setProject(project);
+            if (!issue.getResolvedVersions().contains(version) && !issue.getScheduledVersions().contains(version)
+                    && !issue.getAffectedVersions().contains(version)) {
+                version = null;
+            }
+            updateAttributes();
+        }
 
+        void syncProject() throws SQLException {
             final var projectSelection = getParameter(req, Integer.class, "pid");
             if (projectSelection.isPresent()) {
-                final var selectedProject = projectDao.find(projectSelection.get());
+                final var selectedProject = dao.getProjectDao().find(projectSelection.get());
                 if (!Objects.equals(selectedProject, project)) {
                     // reset version and issue if project changed
                     version = null;
@@ -101,51 +145,57 @@
                 }
                 project = selectedProject;
             } else {
-                final var sessionProject = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
-                project = sessionProject == null ? null : projectDao.find(sessionProject.getId());
+                project = project == null ? null : dao.getProjectDao().find(project.getId());
+            }
+        }
+
+        void syncVersion() throws SQLException {
+            final var versionSelection = getParameter(req, Integer.class, "vid");
+            if (versionSelection.isPresent()) {
+                if (versionSelection.get() < 0) {
+                    version = null;
+                } else {
+                    final var selectedVersion = dao.getVersionDao().find(versionSelection.get());
+                    if (!Objects.equals(selectedVersion, version)) {
+                        issue = null;
+                    }
+                    selectVersion(selectedVersion);
+                }
+            } else {
+                version = version == null ? null : dao.getVersionDao().find(version.getId());
             }
+        }
+
+        void syncIssue() throws SQLException {
+            final var issueSelection = getParameter(req, Integer.class, "issue");
+            if (issueSelection.isPresent()) {
+                final var selectedIssue = dao.getIssueDao().find(issueSelection.get());
+                dao.getIssueDao().joinVersionInformation(selectedIssue);
+                selectIssue(selectedIssue);
+            } else {
+                issue = issue == null ? null : dao.getIssueDao().find(issue.getId());
+            }
+        }
+
+        void sync() throws SQLException {
+            project = (Project) session.getAttribute(SESSION_ATTR_SELECTED_PROJECT);
+            version = (Version) session.getAttribute(SESSION_ATTR_SELECTED_VERSION);
+            issue = (Issue) session.getAttribute(SESSION_ATTR_SELECTED_ISSUE);
+
+            syncProject();
+            syncVersion();
+            syncIssue();
+
             updateAttributes();
         }
 
-        void selectVersion(Version version) {
-            this.project = version.getProject();
-            this.version = version;
-            this.issue = null;
-            updateAttributes();
-        }
-
-        void selectIssue(Issue issue) {
-            this.project = issue.getProject();
-            this.issue = issue;
-            this.version = null;
-            updateAttributes();
-        }
-
-        void updateAttributes() {
+        private void updateAttributes() {
             session.setAttribute(SESSION_ATTR_SELECTED_PROJECT, project);
             session.setAttribute(SESSION_ATTR_SELECTED_VERSION, version);
             session.setAttribute(SESSION_ATTR_SELECTED_ISSUE, issue);
         }
     }
 
-    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";
@@ -162,10 +212,10 @@
      * Creates the breadcrumb menu.
      *
      * @param level           the current active level (0: root, 1: project, 2: version, 3: issue list, 4: issue)
-     * @param sessionSelection the currently selected objects
+     * @param selection the currently selected objects
      * @return a dynamic breadcrumb menu trying to display as many levels as possible
      */
-    private List<MenuEntry> getBreadcrumbs(int level, SessionSelection sessionSelection) {
+    private List<MenuEntry> getBreadcrumbs(int level, SessionSelection selection) {
         MenuEntry entry;
 
         final var breadcrumbs = new ArrayList<MenuEntry>();
@@ -174,47 +224,49 @@
         breadcrumbs.add(entry);
         if (level == BREADCRUMB_LEVEL_ROOT) entry.setActive(true);
 
-        if (sessionSelection.project != null) {
-            if (sessionSelection.project.getId() < 0) {
+        if (selection.project != null) {
+            if (selection.project.getId() < 0) {
                 entry = new MenuEntry(new ResourceKey("localization.projects", "button.create"),
                         "projects/edit");
             } else {
-                entry = new MenuEntry(sessionSelection.project.getName(),
-                        "projects/view?pid=" + sessionSelection.project.getId());
+                entry = new MenuEntry(selection.project.getName(),
+                        "projects/view?pid=" + selection.project.getId());
             }
             if (level == BREADCRUMB_LEVEL_PROJECT) entry.setActive(true);
             breadcrumbs.add(entry);
         }
 
-        if (sessionSelection.version != null) {
-            if (sessionSelection.version.getId() < 0) {
+        if (selection.version != null) {
+            if (selection.version.getId() < 0) {
                 entry = new MenuEntry(new ResourceKey("localization.projects", "button.version.create"),
                         "projects/versions/edit");
             } else {
-                entry = new MenuEntry(sessionSelection.version.getName(),
-                        // TODO: change link to issue overview for that version
-                        "projects/versions/edit?id=" + sessionSelection.version.getId());
+                entry = new MenuEntry(selection.version.getName(),
+                        "projects/versions/view?vid=" + selection.version.getId());
             }
             if (level == BREADCRUMB_LEVEL_VERSION) entry.setActive(true);
             breadcrumbs.add(entry);
         }
 
-        if (sessionSelection.project != null) {
+        if (selection.project != null) {
+            String path = "projects/issues/?pid=" + selection.project.getId();
+            if (selection.version != null) {
+                path += "&vid="+selection.version.getId();
+            }
             entry = new MenuEntry(new ResourceKey("localization.projects", "menu.issues"),
-                    // TODO: maybe also add selected version
-                    "projects/issues/?pid=" + sessionSelection.project.getId());
+                    path);
             if (level == BREADCRUMB_LEVEL_ISSUE_LIST) entry.setActive(true);
             breadcrumbs.add(entry);
         }
 
-        if (sessionSelection.issue != null) {
-            if (sessionSelection.issue.getId() < 0) {
+        if (selection.issue != null) {
+            if (selection.issue.getId() < 0) {
                 entry = new MenuEntry(new ResourceKey("localization.projects", "button.issue.create"),
                         "projects/issues/edit");
             } else {
-                entry = new MenuEntry("#" + sessionSelection.issue.getId(),
+                entry = new MenuEntry("#" + selection.issue.getId(),
                         // TODO: maybe change link to a view rather than directly opening the editor
-                        "projects/issues/edit?id=" + sessionSelection.issue.getId());
+                        "projects/issues/edit?issue=" + selection.issue.getId());
             }
             if (level == BREADCRUMB_LEVEL_ISSUE) entry.setActive(true);
             breadcrumbs.add(entry);
@@ -226,8 +278,22 @@
     @RequestMapping(method = HttpMethod.GET)
     public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
         final var sessionSelection = new SessionSelection(req, dao);
-        final var projectList = dao.getProjectDao().list();
-        req.setAttribute("projects", projectList);
+        sessionSelection.sync();
+
+        final var projectDao = dao.getProjectDao();
+        final var versionDao = dao.getVersionDao();
+
+        final var projectList = projectDao.list();
+
+        final var viewModel = new ProjectIndexView();
+        for (var project : projectList) {
+            final var info = new ProjectInfo(project);
+            info.setVersions(versionDao.list(project));
+            info.setIssueSummary(projectDao.getIssueSummary(project));
+            viewModel.getProjects().add(info);
+        }
+
+        setViewModel(req, viewModel);
         setContentPage(req, "projects");
         setStylesheet(req, "projects");
 
@@ -236,17 +302,24 @@
         return ResponseType.HTML;
     }
 
-    private void configureEditForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
-        req.setAttribute("project", selection.project);
-        req.setAttribute("users", dao.getUserDao().list());
+    private ProjectEditView configureEditForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
+        final var viewModel = new ProjectEditView();
+        viewModel.setProject(selection.project);
+        viewModel.setUsers(dao.getUserDao().list());
+        setViewModel(req, viewModel);
         setContentPage(req, "project-form");
         setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection));
+        return viewModel;
     }
 
     @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
     public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
-        final var selection = new SessionSelection(req, findByParameter(req, Integer.class, "id",
-                dao.getProjectDao()::find).orElse(new Project(-1)));
+        final var selection = new SessionSelection(req, dao);
+        if (getParameter(req, Integer.class, "pid").isEmpty()) {
+            selection.newProject();
+        } else {
+            selection.sync();
+        }
 
         configureEditForm(req, dao, selection);
 
@@ -272,10 +345,12 @@
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
             LOG.debug("Successfully updated project {}", project.getName());
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
-            // TODO: set request attribute with error text
             LOG.warn("Form validation failure: {}", ex.getMessage());
             LOG.debug("Details:", ex);
-            configureEditForm(req, dao, new SessionSelection(req, project));
+            final var selection = new SessionSelection(req, dao);
+            selection.project = project;
+            final var vm = configureEditForm(req, dao, selection);
+            vm.setErrorText(ex.getMessage()); // TODO: error text
         }
 
         return ResponseType.HTML;
@@ -283,127 +358,143 @@
 
     @RequestMapping(requestPath = "view", method = HttpMethod.GET)
     public ResponseType view(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
-        final var sessionSelection = new SessionSelection(req, dao);
-        if (sessionSelection.project == null) {
+        final var selection = new SessionSelection(req, dao);
+        selection.sync();
+
+        if (selection.project == null) {
             resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
             return ResponseType.NONE;
         }
 
         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));
-        }
+        final var issueDao = dao.getIssueDao();
 
-        setAttributeHideZeros(req);
+        final var viewModel = new ProjectView(selection.project);
+        final var issues = issueDao.list(selection.project);
+        for (var issue : issues) issueDao.joinVersionInformation(issue);
+        viewModel.setIssues(issues);
+        viewModel.setVersions(versionDao.list(selection.project));
+        viewModel.updateVersionInfo();
+        setViewModel(req, viewModel);
 
-        req.setAttribute("project", sessionSelection.project);
-        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));
+        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_PROJECT, selection));
         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());
+    @RequestMapping(requestPath = "versions/view", method = HttpMethod.GET)
+    public ResponseType viewVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException, IOException {
+        final var selection = new SessionSelection(req, dao);
+        selection.sync();
+        if (selection.version == null) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return ResponseType.NONE;
+        }
 
-        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);
+        final var issueDao = dao.getIssueDao();
+        final var viewModel = new VersionView(selection.version);
+        final var issues = issueDao.list(selection.version);
+        for (var issue : issues) issueDao.joinVersionInformation(issue);
+        viewModel.setIssues(issues);
+        setViewModel(req, viewModel);
 
+        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection));
+        setContentPage(req, "version");
+        setStylesheet(req, "projects");
+
+        return ResponseType.HTML;
+    }
+
+    private VersionEditView configureEditVersionForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
+        final var viewModel = new VersionEditView(selection.version);
+        if (selection.version.getProject() == null) {
+            viewModel.setProjects(dao.getProjectDao().list());
+        }
+        setViewModel(req, viewModel);
         setContentPage(req, "version-form");
         setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_VERSION, selection));
+        return viewModel;
     }
 
     @RequestMapping(requestPath = "versions/edit", method = HttpMethod.GET)
-    public ResponseType editVersion(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
-        final var sessionSelection = new SessionSelection(req, dao);
+    public ResponseType editVersion(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
+        final var selection = new SessionSelection(req, dao);
+        if (getParameter(req, Integer.class, "vid").isEmpty()) {
+            selection.newVersion();
+        } else {
+            selection.sync();
+        }
 
-        sessionSelection.selectVersion(findByParameter(req, Integer.class, "id", dao.getVersionDao()::find)
-                .orElse(new Version(-1, sessionSelection.project)));
-        configureEditVersionForm(req, dao, sessionSelection);
+        configureEditVersionForm(req, dao, selection);
 
         return ResponseType.HTML;
     }
 
     @RequestMapping(requestPath = "versions/commit", method = HttpMethod.POST)
     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);
+        var version = new Version(-1);
         try {
-            version = new Version(getParameter(req, Integer.class, "id").orElseThrow(), sessionSelection.project);
+            version = new Version(getParameter(req, Integer.class, "id").orElseThrow());
+            version.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
             version.setName(getParameter(req, String.class, "name").orElseThrow());
             getParameter(req, Integer.class, "ordinal").ifPresent(version::setOrdinal);
             version.setStatus(VersionStatus.valueOf(getParameter(req, String.class, "status").orElseThrow()));
             dao.getVersionDao().saveOrUpdate(version);
 
             // specifying the pid parameter will purposely reset the session selected version!
-            setRedirectLocation(req, "./projects/view?pid="+sessionSelection.project.getId());
+            setRedirectLocation(req, "./projects/view?pid="+version.getProject().getId());
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
-            LOG.debug("Successfully updated version {} for project {}", version.getName(), sessionSelection.project.getName());
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
-            // TODO: set request attribute with error text
             LOG.warn("Form validation failure: {}", ex.getMessage());
             LOG.debug("Details:", ex);
-            sessionSelection.selectVersion(version);
-            configureEditVersionForm(req, dao, sessionSelection);
+            final var selection = new SessionSelection(req, dao);
+            selection.selectVersion(version);
+            final var viewModel = configureEditVersionForm(req, dao, selection);
+            // TODO: set Error Text
         }
 
         return ResponseType.HTML;
     }
 
-    private void configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
+    private IssueEditView configureEditIssueForm(HttpServletRequest req, DataAccessObjects dao, SessionSelection selection) throws SQLException {
+        final var viewModel = new IssueEditView(selection.issue);
 
-        if (selection.issue.getProject() == null || selection.issue.getProject().getId() < 0) {
-            req.setAttribute("projects", dao.getProjectDao().list());
-            req.setAttribute("versions", Collections.<Version>emptyList());
+        if (selection.issue.getProject() == null) {
+            viewModel.setProjects(dao.getProjectDao().list());
         } else {
-            req.setAttribute("projects", Collections.<Project>emptyList());
-            req.setAttribute("versions", dao.getVersionDao().list(selection.issue.getProject()));
+            viewModel.setVersions(dao.getVersionDao().list(selection.issue.getProject()));
         }
-
-        dao.getIssueDao().joinVersionInformation(selection.issue);
-        req.setAttribute("issue", selection.issue);
-        req.setAttribute("issueStatusEnum", IssueStatus.values());
-        req.setAttribute("issueCategoryEnum", IssueCategory.values());
-        req.setAttribute("users", dao.getUserDao().list());
+        viewModel.setUsers(dao.getUserDao().list());
+        setViewModel(req, viewModel);
 
         setContentPage(req, "issue-form");
         setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE, selection));
+        return viewModel;
     }
 
     @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) {
+        final var selection = new SessionSelection(req, dao);
+        selection.sync();
+        if (selection.project == null) {
             resp.sendError(HttpServletResponse.SC_NOT_FOUND, "No project selected.");
             return ResponseType.NONE;
         }
 
-        req.setAttribute("issues", dao.getIssueDao().list(sessionSelection.project));
+        final var viewModel = new IssuesView();
+        viewModel.setProject(selection.project);
+        if (selection.version == null) {
+            viewModel.setIssues(dao.getIssueDao().list(selection.project));
+        } else {
+            viewModel.setVersion(selection.version);
+            viewModel.setIssues(dao.getIssueDao().list(selection.version));
+        }
+        setViewModel(req, viewModel);
 
-        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE_LIST, sessionSelection));
+        setBreadcrumbs(req, getBreadcrumbs(BREADCRUMB_LEVEL_ISSUE_LIST, selection));
         setContentPage(req, "issues");
         setStylesheet(req, "projects");
 
@@ -412,22 +503,25 @@
 
     @RequestMapping(requestPath = "issues/edit", method = HttpMethod.GET)
     public ResponseType editIssue(HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws SQLException {
-        final var sessionSelection = new SessionSelection(req, dao);
+        final var selection = new SessionSelection(req, dao);
+        if (getParameter(req, Integer.class, "issue").isEmpty()) {
+            selection.newIssue();
+        } else {
+            selection.sync();
+        }
 
-        sessionSelection.selectIssue(findByParameter(req, Integer.class, "id",
-                dao.getIssueDao()::find).orElse(new Issue(-1, sessionSelection.project)));
-        configureEditIssueForm(req, dao, sessionSelection);
+        configureEditIssueForm(req, dao, selection);
 
         return ResponseType.HTML;
     }
 
     @RequestMapping(requestPath = "issues/commit", method = HttpMethod.POST)
     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);
+        Issue issue = new Issue(-1);
         try {
-            issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow(), sessionSelection.project);
+            issue = new Issue(getParameter(req, Integer.class, "id").orElseThrow());
+            issue.setProject(new Project(getParameter(req, Integer.class, "pid").orElseThrow()));
             getParameter(req, String.class, "category").map(IssueCategory::valueOf).ifPresent(issue::setCategory);
             getParameter(req, String.class, "status").map(IssueStatus::valueOf).ifPresent(issue::setStatus);
             issue.setSubject(getParameter(req, String.class, "subject").orElseThrow());
@@ -440,17 +534,17 @@
             getParameter(req, Integer[].class, "affected")
                     .map(Stream::of)
                     .map(stream ->
-                        stream.map(id -> new Version(id, sessionSelection.project)).collect(Collectors.toList())
+                        stream.map(Version::new).collect(Collectors.toList())
                     ).ifPresent(issue::setAffectedVersions);
             getParameter(req, Integer[].class, "scheduled")
                     .map(Stream::of)
                     .map(stream ->
-                            stream.map(id -> new Version(id, sessionSelection.project)).collect(Collectors.toList())
+                            stream.map(Version::new).collect(Collectors.toList())
                     ).ifPresent(issue::setScheduledVersions);
             getParameter(req, Integer[].class, "resolved")
                     .map(Stream::of)
                     .map(stream ->
-                            stream.map(id -> new Version(id, sessionSelection.project)).collect(Collectors.toList())
+                            stream.map(Version::new).collect(Collectors.toList())
                     ).ifPresent(issue::setResolvedVersions);
 
             dao.getIssueDao().saveOrUpdate(issue);
@@ -458,13 +552,14 @@
             // specifying the issue parameter keeps the edited issue as breadcrumb
             setRedirectLocation(req, "./projects/issues/?issue="+issue.getId());
             setContentPage(req, Constants.JSP_COMMIT_SUCCESSFUL);
-            LOG.debug("Successfully updated issue {} for project {}", issue.getId(), sessionSelection.project.getName());
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
             // TODO: set request attribute with error text
             LOG.warn("Form validation failure: {}", ex.getMessage());
             LOG.debug("Details:", ex);
-            sessionSelection.selectIssue(issue);
-            configureEditIssueForm(req, dao, sessionSelection);
+            final var selection = new SessionSelection(req, dao);
+            selection.selectIssue(issue);
+            final var viewModel = configureEditIssueForm(req, dao, selection);
+            // TODO: set Error Text
         }
 
         return ResponseType.HTML;
--- a/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/java/de/uapcore/lightpit/modules/UsersModule.java	Mon Jun 01 14:46:58 2020 +0200
@@ -32,6 +32,8 @@
 import de.uapcore.lightpit.*;
 import de.uapcore.lightpit.dao.DataAccessObjects;
 import de.uapcore.lightpit.entities.User;
+import de.uapcore.lightpit.viewmodel.UsersEditView;
+import de.uapcore.lightpit.viewmodel.UsersView;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -57,7 +59,9 @@
     public ResponseType index(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
         final var userDao = dao.getUserDao();
 
-        req.setAttribute("users", userDao.list());
+        final var viewModel = new UsersView();
+        viewModel.setUsers(userDao.list());
+        setViewModel(req, viewModel);
         setContentPage(req, "users");
 
         return ResponseType.HTML;
@@ -66,9 +70,11 @@
     @RequestMapping(requestPath = "edit", method = HttpMethod.GET)
     public ResponseType edit(HttpServletRequest req, DataAccessObjects dao) throws SQLException {
 
-        req.setAttribute("user", findByParameter(req, Integer.class, "id",
+        final var viewModel = new UsersEditView();
+        viewModel.setUser(findByParameter(req, Integer.class, "id",
                 dao.getUserDao()::find).orElse(new User(-1)));
 
+        setViewModel(req, viewModel);
         setContentPage(req, "user-form");
 
         return ResponseType.HTML;
@@ -92,8 +98,10 @@
 
             LOG.debug("Successfully updated user {}", user.getUsername());
         } catch (NoSuchElementException | IllegalArgumentException | SQLException ex) {
-            // TODO: set request attribute with error text
-            req.setAttribute("user", user);
+            final var viewModel = new UsersEditView();
+            viewModel.setUser(user);
+            // TODO: viewModel.setErrorText()
+            setViewModel(req, viewModel);
             setContentPage(req, "user-form");
             LOG.warn("Form validation failure: {}", ex.getMessage());
             LOG.debug("Details:", ex);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/IssueEditView.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,54 @@
+package de.uapcore.lightpit.viewmodel;
+
+import de.uapcore.lightpit.entities.*;
+
+import java.util.Collections;
+import java.util.List;
+
+public class IssueEditView {
+    private final Issue issue;
+
+    private List<Project> projects = Collections.emptyList();
+    private List<Version> versions = Collections.emptyList();
+    private List<User> users;
+
+    public IssueEditView(Issue issue) {
+        this.issue = issue;
+    }
+
+    public Issue getIssue() {
+        return issue;
+    }
+
+    public List<Project> getProjects() {
+        return projects;
+    }
+
+    public void setProjects(List<Project> projects) {
+        this.projects = projects;
+    }
+
+    public List<Version> getVersions() {
+        return versions;
+    }
+
+    public void setVersions(List<Version> versions) {
+        this.versions = versions;
+    }
+
+    public List<User> getUsers() {
+        return users;
+    }
+
+    public void setUsers(List<User> users) {
+        this.users = users;
+    }
+
+    public IssueStatus[] getIssueStatus() {
+        return IssueStatus.values();
+    }
+
+    public IssueCategory[] getIssueCategory() {
+        return IssueCategory.values();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/IssuesView.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,37 @@
+package de.uapcore.lightpit.viewmodel;
+
+import de.uapcore.lightpit.entities.Issue;
+import de.uapcore.lightpit.entities.Project;
+import de.uapcore.lightpit.entities.Version;
+
+import java.util.List;
+
+public class IssuesView {
+    private List<Issue> issues;
+    private Project project;
+    private Version version;
+
+    public List<Issue> getIssues() {
+        return issues;
+    }
+
+    public void setIssues(List<Issue> issues) {
+        this.issues = issues;
+    }
+
+    public Version getVersion() {
+        return version;
+    }
+
+    public void setVersion(Version version) {
+        this.version = version;
+    }
+
+    public Project getProject() {
+        return project;
+    }
+
+    public void setProject(Project project) {
+        this.project = project;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/LanguageView.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,36 @@
+package de.uapcore.lightpit.viewmodel;
+
+import java.util.List;
+import java.util.Locale;
+
+public class LanguageView {
+
+    private List<Locale> languages;
+    private Locale browserLanguage;
+    private Locale currentLanguage;
+
+
+    public List<Locale> getLanguages() {
+        return languages;
+    }
+
+    public void setLanguages(List<Locale> languages) {
+        this.languages = languages;
+    }
+
+    public Locale getBrowserLanguage() {
+        return browserLanguage;
+    }
+
+    public void setBrowserLanguage(Locale browserLanguage) {
+        this.browserLanguage = browserLanguage;
+    }
+
+    public Locale getCurrentLanguage() {
+        return currentLanguage;
+    }
+
+    public void setCurrentLanguage(Locale currentLanguage) {
+        this.currentLanguage = currentLanguage;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectEditView.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,37 @@
+package de.uapcore.lightpit.viewmodel;
+
+import de.uapcore.lightpit.entities.Project;
+import de.uapcore.lightpit.entities.User;
+
+import java.util.List;
+
+public class ProjectEditView {
+
+    private Project project;
+    private List<User> users;
+    private String errorText;
+
+    public Project getProject() {
+        return project;
+    }
+
+    public void setProject(Project project) {
+        this.project = project;
+    }
+
+    public List<User> getUsers() {
+        return users;
+    }
+
+    public void setUsers(List<User> users) {
+        this.users = users;
+    }
+
+    public String getErrorText() {
+        return errorText;
+    }
+
+    public void setErrorText(String errorText) {
+        this.errorText = errorText;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectIndexView.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,17 @@
+package de.uapcore.lightpit.viewmodel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ProjectIndexView {
+
+    private List<ProjectInfo> projects = new ArrayList<>();
+
+    public List<ProjectInfo> getProjects() {
+        return projects;
+    }
+
+    public void setProjects(List<ProjectInfo> projects) {
+        this.projects = projects;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectInfo.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,58 @@
+package de.uapcore.lightpit.viewmodel;
+
+import de.uapcore.lightpit.entities.IssueSummary;
+import de.uapcore.lightpit.entities.Project;
+import de.uapcore.lightpit.entities.Version;
+import de.uapcore.lightpit.entities.VersionStatus;
+
+import java.util.Collections;
+import java.util.List;
+
+public class ProjectInfo {
+
+    private final Project project;
+    private List<Version> versions = Collections.emptyList();
+    private IssueSummary issueSummary = new IssueSummary();
+
+    public ProjectInfo(Project project) {
+        this.project = project;
+    }
+
+    public Project getProject() {
+        return project;
+    }
+
+    public List<Version> getVersions() {
+        return versions;
+    }
+
+    public void setVersions(List<Version> versions) {
+        this.versions = versions;
+    }
+
+    public Version getLatestVersion() {
+        for (var v : versions) {
+            if (v.getStatus().ordinal() >= VersionStatus.Released.ordinal())
+                return v;
+        }
+        return null;
+    }
+
+    public Version getNextVersion() {
+        Version next = null;
+        for (var v : versions) {
+            if (v.getStatus().ordinal() >= VersionStatus.Released.ordinal())
+                break;
+            next = v;
+        }
+        return next;
+    }
+
+    public IssueSummary getIssueSummary() {
+        return issueSummary;
+    }
+
+    public void setIssueSummary(IssueSummary issueSummary) {
+        this.issueSummary = issueSummary;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/ProjectView.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,81 @@
+package de.uapcore.lightpit.viewmodel;
+
+import de.uapcore.lightpit.entities.Issue;
+import de.uapcore.lightpit.entities.IssueSummary;
+import de.uapcore.lightpit.entities.Project;
+import de.uapcore.lightpit.entities.Version;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ProjectView {
+
+    private final Project project;
+    private List<Version> versions = Collections.emptyList();
+    private List<Issue> issues = Collections.emptyList();
+
+    private IssueSummary issuesTotal;
+    private List<Issue> issuesWithoutVersion;
+    private IssueSummary issuesWithoutVersionTotal;
+    private List<VersionInfo> versionInfos = Collections.emptyList();
+
+    public ProjectView(Project project) {
+        this.project = project;
+    }
+
+    public Project getProject() {
+        return project;
+    }
+
+    public List<Issue> getIssues() {
+        return issues;
+    }
+
+    public void setIssues(List<Issue> issues) {
+        this.issues = issues;
+        issuesTotal = new IssueSummary();
+        issuesWithoutVersion = new ArrayList<>();
+        issuesWithoutVersionTotal = new IssueSummary();
+        for (Issue issue : issues) {
+            issuesTotal.add(issue);
+            if (issue.getResolvedVersions().isEmpty() && issue.getScheduledVersions().isEmpty() && issue.getResolvedVersions().isEmpty()) {
+                issuesWithoutVersion.add(issue);
+                issuesWithoutVersionTotal.add(issue);
+            }
+        }
+    }
+
+    public List<Version> getVersions() {
+        return versions;
+    }
+
+    public void setVersions(List<Version> versions) {
+        this.versions = versions;
+    }
+
+    public void updateVersionInfo() {
+        versionInfos = new ArrayList<>();
+        for (Version version : versions) {
+            final var info = new VersionInfo(version);
+            info.collectIssues(issues);
+            versionInfos.add(info);
+        }
+    }
+
+    public IssueSummary getIssuesTotal() {
+        return issuesTotal;
+    }
+
+    public List<Issue> getIssuesWithoutVersion() {
+        return issuesWithoutVersion;
+    }
+
+    public IssueSummary getIssuesWithoutVersionTotal() {
+        return issuesWithoutVersionTotal;
+    }
+
+    public List<VersionInfo> getVersionInfos() {
+        return versionInfos;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/UsersEditView.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,24 @@
+package de.uapcore.lightpit.viewmodel;
+
+import de.uapcore.lightpit.entities.User;
+
+public class UsersEditView {
+    private User user;
+    private String errorText;
+
+    public User getUser() {
+        return user;
+    }
+
+    public void setUser(User user) {
+        this.user = user;
+    }
+
+    public String getErrorText() {
+        return errorText;
+    }
+
+    public void setErrorText(String errorText) {
+        this.errorText = errorText;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/UsersView.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,17 @@
+package de.uapcore.lightpit.viewmodel;
+
+import de.uapcore.lightpit.entities.User;
+
+import java.util.List;
+
+public class UsersView {
+    private List<User> users;
+
+    public List<User> getUsers() {
+        return users;
+    }
+
+    public void setUsers(List<User> users) {
+        this.users = users;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionEditView.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,42 @@
+package de.uapcore.lightpit.viewmodel;
+
+import de.uapcore.lightpit.entities.Project;
+import de.uapcore.lightpit.entities.Version;
+import de.uapcore.lightpit.entities.VersionStatus;
+
+import java.util.Collections;
+import java.util.List;
+
+public class VersionEditView {
+    private final Version version;
+    private List<Project> projects = Collections.emptyList();
+    private String errorText;
+
+    public VersionEditView(Version version) {
+        this.version = version;
+    }
+
+    public Version getVersion() {
+        return version;
+    }
+
+    public List<Project> getProjects() {
+        return projects;
+    }
+
+    public void setProjects(List<Project> projects) {
+        this.projects = projects;
+    }
+
+    public VersionStatus[] getVersionStatus() {
+        return VersionStatus.values();
+    }
+
+    public String getErrorText() {
+        return errorText;
+    }
+
+    public void setErrorText(String errorText) {
+        this.errorText = errorText;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionInfo.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,82 @@
+package de.uapcore.lightpit.viewmodel;
+
+import de.uapcore.lightpit.entities.Issue;
+import de.uapcore.lightpit.entities.IssueSummary;
+import de.uapcore.lightpit.entities.Version;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class VersionInfo {
+
+    private final Version version;
+
+    private final IssueSummary reportedTotal = new IssueSummary();
+    private final IssueSummary scheduledTotal = new IssueSummary();
+    private final IssueSummary resolvedTotal = new IssueSummary();
+
+    private final List<Issue> reported = new ArrayList<>();
+    private final List<Issue> scheduled = new ArrayList<>();
+    private final List<Issue> resolved = new ArrayList<>();
+
+    public VersionInfo(Version version) {
+        this.version = version;
+    }
+
+    public Version getVersion() {
+        return version;
+    }
+
+    public void addReported(Issue issue) {
+        reportedTotal.add(issue);
+        reported.add(issue);
+    }
+
+    public void addScheduled(Issue issue) {
+        scheduledTotal.add(issue);
+        scheduled.add(issue);
+    }
+
+    public void addResolved(Issue issue) {
+        resolvedTotal.add(issue);
+        resolved.add(issue);
+    }
+
+    public IssueSummary getReportedTotal() {
+        return reportedTotal;
+    }
+
+    public IssueSummary getScheduledTotal() {
+        return scheduledTotal;
+    }
+
+    public IssueSummary getResolvedTotal() {
+        return resolvedTotal;
+    }
+
+    public List<Issue> getReported() {
+        return reported;
+    }
+
+    public List<Issue> getScheduled() {
+        return scheduled;
+    }
+
+    public List<Issue> getResolved() {
+        return resolved;
+    }
+
+    public void collectIssues(List<Issue> issues) {
+        for (Issue issue : issues) {
+            if (issue.getAffectedVersions().contains(version)) {
+                addReported(issue);
+            }
+            if (issue.getScheduledVersions().contains(version)) {
+                addScheduled(issue);
+            }
+            if (issue.getResolvedVersions().contains(version)) {
+                addResolved(issue);
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/java/de/uapcore/lightpit/viewmodel/VersionView.java	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,23 @@
+package de.uapcore.lightpit.viewmodel;
+
+import de.uapcore.lightpit.entities.Issue;
+import de.uapcore.lightpit.entities.Version;
+
+import java.util.List;
+
+public class VersionView {
+
+    private final VersionInfo versionInfo;
+
+    public VersionView(Version version) {
+        this.versionInfo = new VersionInfo(version);
+    }
+
+    public VersionInfo getVersionInfo() {
+        return versionInfo;
+    }
+
+    public void setIssues(List<Issue> issues) {
+        versionInfo.collectIssues(issues);
+    }
+}
--- a/src/main/resources/localization/projects.properties	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/resources/localization/projects.properties	Mon Jun 01 14:46:58 2020 +0200
@@ -26,7 +26,7 @@
 button.create=New Project
 button.version.create=New Version
 button.issue.create=New Issue
-button.issue.list=Show Issues
+button.issue.all=All Issues
 
 button.stats.hidezeros=Reduced View
 button.stats.showzeros=Full View
@@ -39,9 +39,18 @@
 description=Description
 repoUrl=Repository
 owner=Project Lead
+version.latest=Latest Version
+version.next=Next Version
+
+progress=Overall Progress
+
 issues.open=Open
 issues.active=In Progress
 issues.done=Done
+issues.total=Total
+issues.reported=Reported Issues
+issues.scheduled=Scheduled Issues
+issues.resolved=Resolved Issues
 
 version.project=Project
 version.name=Version
@@ -59,11 +68,8 @@
 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
 
+issue.without-version=Issues w/o Assigned Version
 issue.project=Project
 issue.subject=Subject
 issue.description=Description
--- a/src/main/resources/localization/projects_de.properties	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/resources/localization/projects_de.properties	Mon Jun 01 14:46:58 2020 +0200
@@ -26,7 +26,7 @@
 button.create=Neues Projekt
 button.version.create=Neue Version
 button.issue.create=Neuer Vorgang
-button.issue.list=Vorg\u00e4nge
+button.issue.all=Alle Vorg\u00e4nge
 
 button.stats.hidezeros=Reduzierte Ansicht
 button.stats.showzeros=Komplettansicht
@@ -39,9 +39,18 @@
 description=Beschreibung
 repoUrl=Repository
 owner=Projektleitung
+version.latest=Neuste Version
+version.next=N\u00e4chste Version
+
+progress=Gesamtfortschritt
+
 issues.open=Offen
 issues.active=In Arbeit
 issues.done=Erledigt
+issues.reported=Er\u00f6ffnete Vorg\u00e4nge
+issues.scheduled=Geplante Vorg\u00e4nge 
+issues.resolved=Gel\u00f6ste Vorg\u00e4nge
+issues.total=Summe
 
 version.project=Projekt
 version.name=Version
@@ -59,11 +68,7 @@
 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
-
+issue.without-version=Vorg\u00e4nge ohne Version
 issue.project=Projekt
 issue.subject=Thema
 issue.description=Beschreibung
--- a/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/issue-form.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -28,12 +28,9 @@
 <%@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="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request" />
-<jsp:useBean id="versions" type="java.util.List<de.uapcore.lightpit.entities.Version>" scope="request" />
-<jsp:useBean id="issue" type="de.uapcore.lightpit.entities.Issue" 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="users" type="java.util.List<de.uapcore.lightpit.entities.User>" scope="request"/>
+<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssueEditView" scope="request"/>
+<c:set var="issue" scope="page" value="${viewmodel.issue}" />
+<c:set var="versions" value="${viewmodel.versions}" />
 
 <form action="./projects/issues/commit" method="post">
     <table class="formtable">
@@ -45,26 +42,28 @@
         <tr>
             <th><fmt:message key="issue.project"/></th>
             <td>
-                <c:if test="${issue.project.id ge 0}">
-                    <c:out value="${issue.project.name}" />
-                    <input type="hidden" name="pid" value="${issue.project.id}" />
-                </c:if>
-                <c:if test="${empty issue.project or issue.project.id lt 0}">
-                <select name="pid" required>
-                    <c:forEach var="project" items="${projects}">
-                        <option value="${project.id}">
-                            <c:out value="${project.name}" />
-                        </option>
-                    </c:forEach>
-                </select>
-                </c:if>
+                <c:choose>
+                    <c:when test="${not empty issue.project}">
+                        <c:out value="${issue.project.name}" />
+                        <input type="hidden" name="pid" value="${issue.project.id}" />
+                    </c:when>
+                    <c:otherwise>
+                        <select name="pid" required>
+                            <c:forEach var="project" items="${viewmodel.projects}">
+                                <option value="${project.id}">
+                                    <c:out value="${project.name}" />
+                                </option>
+                            </c:forEach>
+                        </select>
+                    </c:otherwise>
+                </c:choose>
             </td>
         </tr>
         <tr>
             <th><fmt:message key="issue.category"/></th>
             <td>
                 <select name="category">
-                    <c:forEach var="category" items="${issueCategoryEnum}">
+                    <c:forEach var="category" items="${viewmodel.issueCategory}">
                         <option
                                 <c:if test="${category eq issue.category}">selected</c:if>
                                 value="${category}">
@@ -78,7 +77,7 @@
             <th><fmt:message key="issue.status"/></th>
             <td>
                 <select name="status">
-                    <c:forEach var="status" items="${issueStatusEnum}">
+                    <c:forEach var="status" items="${viewmodel.issueStatus}">
                         <option
                                 <c:if test="${status eq issue.status}">selected</c:if>
                                 value="${status}">
@@ -95,7 +94,7 @@
         <tr>
             <th class="vtop"><fmt:message key="issue.description"/></th>
             <td>
-                <textarea name="description"><c:out value="${issue.description}"/></textarea>
+                <textarea name="description" rows="10"><c:out value="${issue.description}"/></textarea>
             </td>
         </tr>
         <tr>
@@ -103,7 +102,7 @@
             <td>
                 <select name="assignee">
                     <option value="-1"><fmt:message key="placeholder.null-assignee"/></option>
-                    <c:forEach var="user" items="${users}">
+                    <c:forEach var="user" items="${viewmodel.users}">
                         <option
                                 <c:if test="${not empty issue.assignee and user eq issue.assignee}">selected</c:if>
                                 value="${user.id}"><c:out value="${user.displayname}"/></option>
@@ -155,7 +154,7 @@
             <td colspan="2">
                 <input type="hidden" name="id" value="${issue.id}"/>
                 <c:choose>
-                    <c:when test="${not empty issue.project and issue.project.id ge 0}">
+                    <c:when test="${not empty issue.project}">
                         <c:set var="cancelUrl">./projects/issues/?pid=${issue.project.id}</c:set>
                     </c:when>
                     <c:otherwise>
--- a/src/main/webapp/WEB-INF/jsp/issues.jsp	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/issues.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -28,60 +28,26 @@
 <%@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"/>
+<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.IssuesView" scope="request"/>
+<c:set var="project" scope="page" value="${viewmodel.project}"/>
+<c:set var="version" scope="page" value="${viewmodel.version}"/>
+<%@include file="../jspf/project-header.jsp"%>
+
+<c:if test="${not empty version}">
+    <h2>
+        <fmt:message key="version.label" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/>
+        <a href="./projects/versions/edit?vid=${version.id}">&#x270e;</a>
+    </h2>
+</c:if>
 
 <div id="tool-area">
     <div>
         <a href="./projects/issues/edit" class="button"><fmt:message key="button.issue.create"/></a>
+        <c:if test="${not empty version}">
+            <a href="./projects/issues/?pid=${project.id}&vid=-1" class="button"><fmt:message key="button.issue.all"/></a>
+        </c:if>
     </div>
 </div>
 
-<table id="issue-list" class="datatable medskip">
-    <thead>
-    <tr>
-        <th><fmt:message key="issue.subject"/></th>
-        <th><fmt:message key="issue.assignee"/></th>
-        <th><fmt:message key="issue.category"/></th>
-        <th><fmt:message key="issue.status"/></th>
-        <th><fmt:message key="issue.created"/></th>
-        <th><fmt:message key="issue.updated"/></th>
-        <th><fmt:message key="issue.eta"/></th>
-    </tr>
-    </thead>
-    <tbody>
-    <c:forEach var="issue" items="${issues}">
-        <tr>
-            <td>
-                <span class="phase-${issue.status.phase}">
-                    <a href="./projects/issues/edit?id=${issue.id}">
-                        <c:out value="${issue.subject}" />
-                    </a>
-                </span>
-            </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>
+<c:set var="issues" value="${viewmodel.issues}"/>
+<%@include file="../jspf/issue-list.jsp"%>
\ No newline at end of file
--- a/src/main/webapp/WEB-INF/jsp/language.jsp	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/language.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -25,23 +25,19 @@
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 --%>
 <%@page pageEncoding="UTF-8" %>
-<%@page import="de.uapcore.lightpit.Constants" %>
 <%@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="languages" type="java.util.List<java.util.Locale>" scope="request"/>
-<jsp:useBean id="browserLanguage" type="java.util.Locale" scope="request"/>
-
-<c:set scope="page" var="currentLanguage" value="${sessionScope[Constants.SESSION_ATTR_LANGUAGE]}"/>
+<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.LanguageView" scope="request"/>
 
 <form method="POST" id="lang-selector">
-    <c:forEach items="${languages}" var="l">
+    <c:forEach items="${viewmodel.languages}" var="l">
         <label>
             <input type="radio" name="language" value="${l.language}"
-                   <c:if test="${l.language eq currentLanguage.language}">checked</c:if>/>
+                   <c:if test="${l.language eq viewmodel.currentLanguage.language}">checked</c:if>/>
                 ${l.displayLanguage}
-            (${l.getDisplayLanguage(currentLanguage)}
-            <c:if test="${not empty browserLanguage and l.language eq browserLanguage.language}"><c:set
+            (${l.getDisplayLanguage(viewmodel.currentLanguage)}
+            <c:if test="${not empty viewmodel.browserLanguage and l.language eq viewmodel.browserLanguage.language}"><c:set
                     var="browserLanguagePresent" value="true"/>&nbsp;-&nbsp;<fmt:message key="browserLanguage"/></c:if>)
         </label>
     </c:forEach>
--- a/src/main/webapp/WEB-INF/jsp/project-details.jsp	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/project-details.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -28,66 +28,46 @@
 <%@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="project" type="de.uapcore.lightpit.entities.Project" scope="request" />
-<jsp:useBean id="versions" type="java.util.List<de.uapcore.lightpit.entities.Version>" 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"/>
+<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectView" scope="request" />
 
-<div id="project-attributes">
-    <div class="row">
-        <div class="caption"><fmt:message key="name"/>:</div>
-        <div><c:out value="${project.name}"/></div>
-        <div class="caption"><fmt:message key="description"/>:</div>
-        <div><c:out value="${project.description}"/></div>
-    </div>
-    <div class="row">
-        <div class="caption"><fmt:message key="owner"/>:</div>
-        <div>
-            <c:if test="${not empty project.owner}"><c:out value="${project.owner.displayname}"/></c:if>
-        </div>
-        <div class="caption"><fmt:message key="repoUrl"/>:</div>
-        <div>
-            <c:if test="${not empty project.repoUrl}">
-                <a target="_blank" href="<c:out value="${project.repoUrl}"/>"><c:out
-                        value="${project.repoUrl}"/></a>
-            </c:if>
-        </div>
-    </div>
-</div>
+<c:set var="project" scope="page" value="${viewmodel.project}"/>
+<%@include file="../jspf/project-header.jsp"%>
 
 <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>
+    <a href="./projects/issues/edit?pid=${project.id}" class="button"><fmt:message key="button.issue.create"/></a>
 </div>
 
-<div id="version-stats">
-<c:forEach var="version" items="${versions}" varStatus="iter">
+<h2><fmt:message key="progress" /></h2>
+
+<c:set var="summary" value="${viewmodel.issuesTotal}" />
+<%@include file="../jspf/issue-summary.jsp"%>
+
+<h2><fmt:message key="issue.without-version" /></h2>
+
+<c:set var="issues" value="${viewmodel.issuesWithoutVersion}"/>
+<c:set var="summary" value="${viewmodel.issuesWithoutVersionTotal}" />
+<%@include file="../jspf/issue-summary.jsp"%>
+<%@include file="../jspf/issue-list.jsp"%>
+
+<c:forEach var="versionInfo" items="${viewmodel.versionInfos}">
     <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>
+        <fmt:message key="version.label" /> <c:out value="${versionInfo.version.name}" /> - <fmt:message key="version.status.${versionInfo.version.status}"/>
+        (<a href="./projects/versions/view?vid=${versionInfo.version.id}">open</a>)
     </h2>
 
-    <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="issues.reported"/> </h3>
+    <c:set var="summary" value="${versionInfo.reportedTotal}"/>
+    <c:set var="issues" value="${versionInfo.reported}"/>
+    <%@include file="../jspf/issue-summary.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="issues.scheduled"/> </h3>
+    <c:set var="summary" value="${versionInfo.scheduledTotal}"/>
+    <c:set var="issues" value="${versionInfo.scheduled}"/>
+    <%@include file="../jspf/issue-summary.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>
+    <h3><fmt:message key="issues.resolved"/> </h3>
+    <c:set var="summary" value="${versionInfo.resolvedTotal}"/>
+    <c:set var="issues" value="${versionInfo.resolved}"/>
+    <%@include file="../jspf/issue-summary.jsp"%>
+</c:forEach>
\ No newline at end of file
--- a/src/main/webapp/WEB-INF/jsp/projects.jsp	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/projects.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -25,15 +25,12 @@
 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="viewmodel" type="de.uapcore.lightpit.viewmodel.ProjectIndexView" scope="request"/>
 
-<jsp:useBean id="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request"/>
-
-<c:if test="${empty projects}">
+<c:if test="${empty viewmodel.projects}">
     <div class="info-box">
         <fmt:message key="no-projects"/>
     </div>
@@ -43,30 +40,35 @@
     <a href="./projects/edit" class="button"><fmt:message key="button.create"/></a>
 </div>
 
-<c:if test="${not empty projects}">
+<c:if test="${not empty viewmodel.projects}">
     <table id="project-list" class="datatable medskip">
         <colgroup>
             <col>
             <col width="20%">
             <col width="50%">
-            <col width="10%">
-            <col width="10%">
-            <col width="10%">
+            <col width="6%">
+            <col width="6%">
+            <col width="6%">
+            <col width="6%">
+            <col width="6%">
         </colgroup>
         <thead>
         <tr>
             <th></th>
             <th><fmt:message key="name"/></th>
             <th><fmt:message key="repoUrl"/></th>
-            <th><fmt:message key="issues.open"/></th>
-            <th><fmt:message key="issues.active"/></th>
-            <th><fmt:message key="issues.done"/></th>
+            <th class="hcenter"><fmt:message key="version.latest"/></th>
+            <th class="hcenter"><fmt:message key="version.next"/></th>
+            <th class="hcenter"><fmt:message key="issues.open"/></th>
+            <th class="hcenter"><fmt:message key="issues.active"/></th>
+            <th class="hcenter"><fmt:message key="issues.done"/></th>
         </tr>
         </thead>
         <tbody>
-        <c:forEach var="project" items="${projects}">
+        <c:forEach var="projectInfo" items="${viewmodel.projects}">
+            <c:set var="project" scope="page" value="${projectInfo.project}"/>
             <tr class="nowrap">
-                <td style="width: 2em;"><a href="./projects/edit?id=${project.id}">&#x270e;</a></td>
+                <td style="width: 2em;"><a href="./projects/edit?pid=${project.id}">&#x270e;</a></td>
                 <td><a href="./projects/view?pid=${project.id}"><c:out value="${project.name}"/></a>
                 </td>
                 <td>
@@ -75,9 +77,19 @@
                                 value="${project.repoUrl}"/></a>
                     </c:if>
                 </td>
-                <td>${project.openIssues}</td>
-                <td>${project.activeIssues}</td>
-                <td>${project.doneIssues}</td>
+                <td class="hright">
+                    <c:if test="${not empty projectInfo.latestVersion}">
+                        <a href="./projects/versions/view?vid=${projectInfo.latestVersion.id}"><c:out value="${projectInfo.latestVersion.name}"/></a>
+                    </c:if>
+                </td>
+                <td class="hright">
+                    <c:if test="${not empty projectInfo.nextVersion}">
+                        <a href="./projects/versions/view?vid=${projectInfo.nextVersion.id}"><c:out value="${projectInfo.nextVersion.name}"/></a>
+                    </c:if>
+                </td>
+                <td class="hright">${projectInfo.issueSummary.open}</td>
+                <td class="hright">${projectInfo.issueSummary.active}</td>
+                <td class="hright">${projectInfo.issueSummary.done}</td>
             </tr>
         </c:forEach>
         </tbody>
--- a/src/main/webapp/WEB-INF/jsp/site.jsp	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/site.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -26,7 +26,6 @@
 --%>
 <%@page contentType="text/html" pageEncoding="UTF-8" trimDirectiveWhitespaces="true" %>
 <%@page import="de.uapcore.lightpit.Constants" %>
-<%@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" %>
 <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
@@ -60,9 +59,6 @@
     <fmt:setLocale scope="request" value="${sessionScope[Constants.SESSION_ATTR_LANGUAGE]}"/>
 </c:if>
 
-<%-- Selected project, if any --%>
-<c:set scope="page" var="selectedProject" value="${sessionScope[ProjectsModule.SESSION_ATTR_SELECTED_PROJECT]}"/>
-
 <%-- Load resource bundles --%>
 <fmt:setBundle scope="request" basename="${bundleName}"/>
 <fmt:setBundle scope="request" var="lightpit_bundle" basename="localization.lightpit"/>
--- a/src/main/webapp/WEB-INF/jsp/user-form.jsp	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/user-form.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -28,7 +28,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="user" type="de.uapcore.lightpit.entities.User" scope="request"/>
+<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.UsersEditView" scope="request"/>
+<c:set var="user" scope="page" value="${viewmodel.user}" />
 
 <form action="./teams/commit" method="post">
     <table class="formtable">
--- a/src/main/webapp/WEB-INF/jsp/users.jsp	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/users.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -28,9 +28,9 @@
 <%@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="users" type="java.util.List<de.uapcore.lightpit.entities.User>" scope="request"/>
+<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.UsersView" scope="request"/>
 
-<c:if test="${empty users}">
+<c:if test="${empty viewmodel.users}">
     <div class="info-box">
         <fmt:message key="no-users"/>
     </div>
@@ -40,7 +40,7 @@
     <a href="./teams/edit" class="button"><fmt:message key="button.create"/></a>
 </div>
 
-<c:if test="${not empty users}">
+<c:if test="${not empty viewmodel.users}">
     <table class="datatable medskip">
         <thead>
         <tr>
@@ -49,7 +49,7 @@
         </tr>
         </thead>
         <tbody>
-        <c:forEach var="user" items="${users}">
+        <c:forEach var="user" items="${viewmodel.users}">
             <tr>
                 <td><a href="./teams/edit?id=${user.id}">&#x270e;</a></td>
                 <td><c:out value="${user.displayname}"/></td>
--- a/src/main/webapp/WEB-INF/jsp/version-form.jsp	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/webapp/WEB-INF/jsp/version-form.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -28,14 +28,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="projects" type="java.util.List<de.uapcore.lightpit.entities.Project>" scope="request" />
-<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"/>
+<jsp:useBean id="viewmodel" type="de.uapcore.lightpit.viewmodel.VersionEditView" scope="request" />
+<c:set var="version" scope="page" value="${viewmodel.version}"/>
 
 <form action="./projects/versions/commit" method="post">
     <table class="formtable" style="width: 35ch">
@@ -47,19 +41,21 @@
         <tr>
             <th><fmt:message key="version.project"/></th>
             <td>
-                <c:if test="${version.project.id ge 0}">
-                    <c:out value="${version.project.name}" />
-                    <input type="hidden" name="pid" value="${version.project.id}" />
-                </c:if>
-                <c:if test="${empty version.project or version.project.id lt 0}">
-                    <select name="pid" required>
-                        <c:forEach var="project" items="${projects}">
-                            <option value="${project.id}">
-                                <c:out value="${project.name}" />
-                            </option>
-                        </c:forEach>
-                    </select>
-                </c:if>
+                <c:choose>
+                    <c:when test="${not empty version.project}">
+                        <c:out value="${version.project.name}" />
+                        <input type="hidden" name="pid" value="${version.project.id}" />
+                    </c:when>
+                    <c:otherwise>
+                        <select name="pid" required>
+                            <c:forEach var="project" items="${viewmodel.projects}">
+                                <option value="${project.id}">
+                                    <c:out value="${project.name}" />
+                                </option>
+                            </c:forEach>
+                        </select>
+                    </c:otherwise>
+                </c:choose>
             </td>
         </tr>
         <tr>
@@ -70,7 +66,7 @@
             <th><fmt:message key="version.status"/></th>
             <td>
                 <select name="status" required>
-                    <c:forEach var="elem" items="${versionStatusEnum}">
+                    <c:forEach var="elem" items="${viewmodel.versionStatus}">
                         <option
                                 <c:if test="${elem eq version.status}">selected</c:if> value="${elem}"><fmt:message
                                 key="version.status.${elem}"/></option>
@@ -90,7 +86,7 @@
             <td colspan="2">
                 <input type="hidden" name="id" value="${version.id}"/>
                 <c:choose>
-                    <c:when test="${not empty version.project and version.project.id ge 0}">
+                    <c:when test="${not empty version.project}">
                         <c:set var="cancelUrl">./projects/view?pid=${version.project.id}</c:set>
                     </c:when>
                     <c:otherwise>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/jsp/version.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,65 @@
+<%--
+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="viewmodel" type="de.uapcore.lightpit.viewmodel.VersionView" scope="request"/>
+<c:set var="version" scope="page" value="${viewmodel.versionInfo.version}"/>
+
+<c:set var="project" scope="page" value="${version.project}"/>
+<%@include file="../jspf/project-header.jsp"%>
+
+
+<div id="tool-area">
+    <a href="./projects/issues/edit?pid=${project.id}" class="button"><fmt:message key="button.issue.create"/></a>
+</div>
+
+<h2>
+<fmt:message key="version.label" /> <c:out value="${version.name}" /> - <fmt:message key="version.status.${version.status}"/>
+<a href="./projects/versions/edit?vid=${version.id}">&#x270e;</a>
+</h2>
+
+<h3><fmt:message key="issues.reported"/> </h3>
+
+<c:set var="summary" value="${viewmodel.versionInfo.reportedTotal}"/>
+<c:set var="issues" value="${viewmodel.versionInfo.reported}"/>
+<%@include file="../jspf/issue-summary.jsp"%>
+<%@include file="../jspf/issue-list.jsp"%>
+
+<h3><fmt:message key="issues.scheduled"/> </h3>
+<c:set var="summary" value="${viewmodel.versionInfo.scheduledTotal}"/>
+<c:set var="issues" value="${viewmodel.versionInfo.scheduled}"/>
+<%@include file="../jspf/issue-summary.jsp"%>
+<%@include file="../jspf/issue-list.jsp"%>
+
+<h3><fmt:message key="issues.resolved"/> </h3>
+<c:set var="summary" value="${viewmodel.versionInfo.resolvedTotal}"/>
+<c:set var="issues" value="${viewmodel.versionInfo.resolved}"/>
+<%@include file="../jspf/issue-summary.jsp"%>
+<%@include file="../jspf/issue-list.jsp"%>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/jspf/issue-list.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,53 @@
+<%--
+issues: List<Issue>
+--%>
+
+<table class="fullwidth datatable medskip">
+    <thead>
+    <tr>
+        <th><fmt:message key="issue.subject"/></th>
+        <th><fmt:message key="issue.assignee"/></th>
+        <th><fmt:message key="issue.category"/></th>
+        <th><fmt:message key="issue.status"/></th>
+        <th><fmt:message key="issue.created"/></th>
+        <th><fmt:message key="issue.updated"/></th>
+        <th><fmt:message key="issue.eta"/></th>
+    </tr>
+    </thead>
+    <tbody>
+    <c:forEach var="issue" items="${issues}">
+        <tr>
+            <td>
+                <span class="phase-${issue.status.phase}">
+                    <a href="./projects/issues/edit?issue=${issue.id}">
+                        <c:out value="${issue.subject}" />
+                    </a>
+                </span>
+            </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>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/jspf/issue-summary.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,20 @@
+<%--
+summary: IssueSummary
+--%>
+
+<div class="issue-summary">
+    <div class="row">
+        <div class="caption"><fmt:message key="issues.open"/>:</div>
+        <div><c:out value="${summary.open}"/></div>
+        <div class="caption"><fmt:message key="issues.active"/>:</div>
+        <div><c:out value="${summary.active}"/></div>
+        <div class="caption"><fmt:message key="issues.done"/>:</div>
+        <div><c:out value="${summary.done}"/></div>
+    </div>
+</div>
+
+<div class="issue-progress-bar">
+    <div class="open" style="width: ${summary.openPercent}%"></div>
+    <div class="active" style="width: ${summary.activePercent}%"></div>
+    <div class="done" style="width: ${summary.donePercent}%"></div>
+</div>
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/WEB-INF/jspf/project-header.jsp	Mon Jun 01 14:46:58 2020 +0200
@@ -0,0 +1,24 @@
+<%--
+project: Project
+--%>
+<div class="project-attributes">
+    <div class="row">
+        <div class="caption"><fmt:message key="name"/>:</div>
+        <div><c:out value="${project.name}"/></div>
+        <div class="caption"><fmt:message key="description"/>:</div>
+        <div><c:out value="${project.description}"/></div>
+    </div>
+    <div class="row">
+        <div class="caption"><fmt:message key="owner"/>:</div>
+        <div>
+            <c:if test="${not empty project.owner}"><c:out value="${project.owner.displayname}"/></c:if>
+        </div>
+        <div class="caption"><fmt:message key="repoUrl"/>:</div>
+        <div>
+            <c:if test="${not empty project.repoUrl}">
+                <a target="_blank" href="<c:out value="${project.repoUrl}"/>"><c:out
+                        value="${project.repoUrl}"/></a>
+            </c:if>
+        </div>
+    </div>
+</div>
--- a/src/main/webapp/WEB-INF/jspf/version-stats.jsp	Sat May 30 18:12:38 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-<%@ 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 30 18:12:38 2020 +0200
+++ b/src/main/webapp/lightpit.css	Mon Jun 01 14:46:58 2020 +0200
@@ -117,7 +117,6 @@
 }
 
 table.datatable {
-    width: auto;
     border-style: solid;
     border-width: 1pt;
     border-color: silver;
--- a/src/main/webapp/projects.css	Sat May 30 18:12:38 2020 +0200
+++ b/src/main/webapp/projects.css	Mon Jun 01 14:46:58 2020 +0200
@@ -27,21 +27,12 @@
  *
  */
 
-#issue-list td {
-    white-space: nowrap;
-}
-
-#version-stats h2 {
-    white-space: nowrap;
+.project-attributes, .issue-summary {
+    display: table;
 }
 
-#version-stats td {
-    text-align: right;
-}
-
-#project-attributes {
+.project-attributes {
     margin-bottom: 2em;
-    display: table;
 }
 
 .row {
@@ -63,3 +54,28 @@
 span.phase-2 {
     text-decoration: line-through;
 }
+
+.issue-progress-bar {
+    display: flex;
+    position: relative;
+    width: 100ex;
+    height: 2em;
+    border-style: inset;
+    border-width: 2pt;
+    border-color: #6060cc;
+}
+
+.issue-progress-bar .open {
+    height: 100%;
+    background: steelblue;
+}
+
+.issue-progress-bar .active {
+    height: 100%;
+    background: gold;
+}
+
+.issue-progress-bar .done {
+    height: 100%;
+    background: green;
+}

mercurial