From 9d810354afbdcece8089f0a6eca8b42ead532dc4 Mon Sep 17 00:00:00 2001
From: Alexis POYEN <apoyen@grandlyon.com>
Date: Tue, 30 Jun 2020 10:42:04 +0200
Subject: [PATCH] Resolve "Display news flow"

---
 internal/models/vote.go                       |   1 +
 internal/rootmux/rootmux_test.go              |   4 +-
 .../visualization/results-section.js          | 118 ++++++++++++++++--
 web/services/common/scroller.js               |  48 +++++++
 .../election/calculate-election-generic.js    |  32 +++--
 web/style.css                                 |  30 ++++-
 6 files changed, 210 insertions(+), 23 deletions(-)
 create mode 100644 web/services/common/scroller.js

diff --git a/internal/models/vote.go b/internal/models/vote.go
index 458be68..5f1d800 100644
--- a/internal/models/vote.go
+++ b/internal/models/vote.go
@@ -239,6 +239,7 @@ func (d *DataHandler) deleteVote(w http.ResponseWriter, r *http.Request, id int)
 			return
 		}
 		deskRound.Completed = false
+		deskRound.DateCompletion = time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC)
 		d.db.Save(&deskRound)
 
 		d.db.Delete(&o)
diff --git a/internal/rootmux/rootmux_test.go b/internal/rootmux/rootmux_test.go
index 81e046d..7d19555 100644
--- a/internal/rootmux/rootmux_test.go
+++ b/internal/rootmux/rootmux_test.go
@@ -131,10 +131,10 @@ func appTests(t *testing.T) {
 		// //Check that on Vote deletion, deskRound is updated
 		do("PUT", "/api/DeskRound/1", xsrfHeader, `{"ID":1,"Validated":false}`, 200, `{"ID":1,"RoundID":1,"DeskID":1,"Capturers":[{"ID":1,"UserID":2,"Name":"Capturer","DeskRounds":null}],"Completed":true,"DateCompletion":"20`)
 		do("DELETE", "/api/Vote/1", xsrfHeader, ``, 200, ``)
-		do("GET", "/api/DeskRound/1", xsrfHeader, ``, 200, `{"ID":1,"RoundID":1,"DeskID":1,"Capturers":[{"ID":1,"UserID":2,"Name":"Capturer","DeskRounds":null}],"Completed":false,"DateCompletion":"20`)
+		do("GET", "/api/DeskRound/1", xsrfHeader, ``, 200, `{"ID":1,"RoundID":1,"DeskID":1,"Capturers":[{"ID":1,"UserID":2,"Name":"Capturer","DeskRounds":null}],"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":[{"ID":3,"DeskRoundID":1,"CandidateListID":0,"VoiceNumber":158,"Blank":true,"NullVote":false},{"ID":4,"DeskRoundID":1,"CandidateListID":0,"VoiceNumber":158,"Blank":false,"NullVote":true}]}`)
 
 		// Verify that on Desk deletion deskRounds are deleted
-		do("GET", "/api/Desk/1", xsrfHeader, ``, 200, `{"ID":1,"SectionID":1,"Name":"Desk 1","WitnessDesk":true,"Subscribed":9587,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"20`)
+		do("GET", "/api/Desk/1", xsrfHeader, ``, 200, `{"ID":1,"SectionID":1,"Name":"Desk 1","WitnessDesk":true,"Subscribed":9587,"DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}]}`)
 		do("DELETE", "/api/Desk/1", xsrfHeader, ``, 200, ``)
 		do("GET", "/api/DeskRound/1", xsrfHeader, ``, 404, `id is missing`)
 
diff --git a/web/components/visualization/results-section.js b/web/components/visualization/results-section.js
index b27d99c..cba58a7 100644
--- a/web/components/visualization/results-section.js
+++ b/web/components/visualization/results-section.js
@@ -1,18 +1,24 @@
 // Imports
+import * as Auth from "/services/auth/auth.js";
 import * as results from "/services/election/calculate-election-generic.js";
+import * as PartyModel from "/services/model/party-model.js";
+import * as Scroller from "/services/common/scroller.js";
 
 export async function mount(where, round) {
   const resultComponent = new ResultComponent(round);
   await resultComponent.mount(where);
   await resultComponent.calculateResults();
+  resultComponent.displayResults();
 }
 
 class ResultComponent {
   constructor(round) {
     this.round = round;
+    this.PartyModel = PartyModel.getPartyModel();
   }
 
   async mount(where) {
+    this.PartyModel.current_user = await Auth.GetUser();
     const mountpoint = where;
     document.getElementById(mountpoint).innerHTML = /* HTML */ `
       <div class="tabs is-boxed is-toggle is-fullwidth">
@@ -76,7 +82,7 @@ class ResultComponent {
               </button>
             </header>
             <div class="card-content">
-              <div id="round-list" class="content">
+              <div id="news-flow" class="content">
                 Flux d'actualité
               </div>
             </div>
@@ -100,22 +106,55 @@ class ResultComponent {
         </div>
       </div>
     `;
+    this.calculator = await results.mountCalculator(this.round);
     this.handleDom();
     document.getElementById("areas").click();
-    this.calculator = await results.mountCalculator(this.round);
+  }
+
+  resultFlowTemplate(zone) {
+    return /* HTML */ `<div class="card-list card-no-hover">
+      <div class="card-content">
+        <div id="flow-content-${zone.ID}" class="content">
+          <h5 class="title is-5">${zone.Name}</h5>
+        </div>
+      </div>
+    </div>`;
+  }
+
+  async progressBarTemplate(candidateList) {
+    let party = await this.PartyModel.getParty(candidateList.PartyID);
+    return /* HTML */ `<div class="progressBar">
+      <div
+        class="progressBarValue"
+        style="background-color : ${party.Color}; width : ${candidateList.Percentage}%"
+      >
+        ${candidateList.Name +
+        " (" +
+        candidateList.VoiceNumber +
+        " voix soit : " +
+        candidateList.Percentage +
+        "%)"}
+      </div>
+    </div>`;
   }
 
   handleDom() {
+    let resultHandler = this;
     document.getElementById("areas").addEventListener("click", function () {
+      resultHandler.zone = "areas";
+      resultHandler.calculateResults();
+      resultHandler.displayResults();
       document.getElementById("sections").setAttribute("class", "");
       document.getElementById("areas").setAttribute("class", "is-active");
     });
     document.getElementById("sections").addEventListener("click", function () {
+      resultHandler.zone = "sections";
+      resultHandler.calculateResults();
+      resultHandler.displayResults();
       document.getElementById("areas").setAttribute("class", "");
       document.getElementById("sections").setAttribute("class", "is-active");
     });
 
-    let resultHandler = this;
     document.getElementById("zoom-map").addEventListener("click", function () {
       resultHandler.zoomMap();
     });
@@ -132,8 +171,9 @@ class ResultComponent {
 
     let radioButtons = document.getElementsByName("filter");
     for (var i = 0; i < radioButtons.length; i++) {
-      radioButtons[i].addEventListener("click", (e) => {
-        this.calculateResults();
+      radioButtons[i].addEventListener("click", async (e) => {
+        await this.calculateResults();
+        this.displayResults();
       });
     }
   }
@@ -151,7 +191,7 @@ class ResultComponent {
     let resultHandler = this;
     document.getElementById("news-flow-section").parentElement.className =
       "column is-full";
-    document.getElementById("news-flow-section").style.height = "auto";
+    document.getElementById("news-flow-section").style.height = "70vh";
     document.getElementById("map-section").parentElement.className = "column";
     document.getElementById("map-section").parentElement.style.display = "none";
     document.getElementById("results-section").style.display = "none";
@@ -191,7 +231,69 @@ class ResultComponent {
   }
 
   async calculateResults() {
-    let filter = document.querySelector('input[name="filter"]:checked').value;
-    await this.calculator.calculateResults(filter);
+    this.filter = document.querySelector('input[name="filter"]:checked').value;
+    this.results = await this.calculator.calculateResults(this.filter);
+  }
+
+  displayResults() {
+    console.log(this.results);
+    document.getElementById("news-flow").innerHTML = "";
+    if (this.zone === "areas") {
+      this.displayFlowAreas();
+    } else if (this.zone === "sections") {
+      this.displayFlowSections();
+    }
+    let scroller = Scroller.scrollInit("news-flow");
+  }
+
+  async displayFlowAreas() {
+    let resultHandler = this;
+    this.results.areasResults.sort(function (a, b) {
+      return b.DateCompletion - a.DateCompletion;
+    });
+    for (let j in this.results.areasResults) {
+      let area = this.results.areasResults[j];
+      if (area.status === resultHandler.filter) {
+        document.getElementById(
+          "news-flow"
+        ).innerHTML += this.resultFlowTemplate(area);
+
+        for (let i in area.candidateLists) {
+          document.getElementById(
+            "flow-content-" + area.ID
+          ).innerHTML += await resultHandler.progressBarTemplate(
+            area.candidateLists[i]
+          );
+        }
+      }
+    }
+  }
+
+  async displayFlowSections() {
+    let resultHandler = this;
+    let sections = [];
+    this.results.areasResults.forEach((area) => {
+      sections = sections.concat(area.Sections);
+    });
+    sections.sort(function (a, b) {
+      return b.DateCompletion - a.DateCompletion;
+    });
+
+    for (let j in sections) {
+      let section = sections[j];
+      if (section.status === resultHandler.filter) {
+        document.getElementById(
+          "news-flow"
+        ).innerHTML += this.resultFlowTemplate(section);
+
+        for (let i in section.candidateLists) {
+          document.getElementById(
+            "flow-content-" + section.ID
+          ).innerHTML += await resultHandler.progressBarTemplate(
+            section.candidateLists[i]
+          );
+        }
+      }
+    }
   }
 }
diff --git a/web/services/common/scroller.js b/web/services/common/scroller.js
new file mode 100644
index 0000000..1db6044
--- /dev/null
+++ b/web/services/common/scroller.js
@@ -0,0 +1,48 @@
+export function scrollInit(divName) {
+  let scroller = new Scroller(divName);
+}
+
+class Scroller {
+  constructor(divName) {
+    this.elmnt = document.getElementById(divName);
+    this.reachedMaxScroll = false;
+
+    this.elmnt.scrollTop = 0;
+    this.previousScrollTop = 0;
+
+    this.scrollDiv();
+    this.elmnt.addEventListener("mouseover", () => {
+      this.pauseScroll();
+    });
+    this.elmnt.addEventListener("mouseout", () => {
+      this.resumeScroll();
+    });
+  }
+
+  scrollDiv() {
+    if (!this.reachedMaxScroll) {
+      this.elmnt.scrollBy({ top: 5, left: 0, behavior: "smooth" });
+      this.reachedMaxScroll =
+        this.elmnt.scrollTop >=
+        this.elmnt.scrollHeight - this.elmnt.offsetHeight;
+      this.scrollTimeout = setTimeout(() => {
+        this.scrollDiv();
+      }, 100);
+    } else {
+      this.reachedMaxScroll = this.elmnt.scrollTop == 0 ? false : true;
+      this.elmnt.scrollBy({ top: -5, left: 0, behavior: "smooth" });
+
+      this.scrollTimeout = setTimeout(() => {
+        this.scrollDiv();
+      }, 100);
+    }
+  }
+
+  pauseScroll() {
+    clearInterval(this.scrollTimeout);
+  }
+
+  resumeScroll() {
+    this.scrollDiv();
+  }
+}
diff --git a/web/services/election/calculate-election-generic.js b/web/services/election/calculate-election-generic.js
index 5a6ccb6..f35ff0f 100644
--- a/web/services/election/calculate-election-generic.js
+++ b/web/services/election/calculate-election-generic.js
@@ -98,7 +98,7 @@ class DirectMetropolitanCalculator {
     }
 
     this.areasResults = await this.calculateAreasResults();
-    console.log(this);
+    return this;
   }
 
   async calculateRoundResults() {
@@ -176,6 +176,13 @@ class DirectMetropolitanCalculator {
     }
     area.Sections = sections;
 
+    let lastDate = new Date(area.Sections[0].DateCompletion);
+    area.Sections.forEach((section) => {
+      if (lastDate - new Date(section.DateCompletion) < 0)
+        lastDate = new Date(section.DateCompletion);
+    });
+    area.DateCompletion = lastDate;
+
     area.stats = await this.calculateStats(deskRounds);
 
     let flag = true;
@@ -183,7 +190,7 @@ class DirectMetropolitanCalculator {
       case "partial":
         if (area.stats.VotesExpressed === 0) {
           area.status = "no_results";
-          return;
+          return area;
         }
         area.status = "partial";
         break;
@@ -196,7 +203,7 @@ class DirectMetropolitanCalculator {
           area.status = "completed";
         } else {
           area.status = "incompleted";
-          return;
+          return area;
         }
         break;
       case "validated":
@@ -208,7 +215,7 @@ class DirectMetropolitanCalculator {
           area.status = "validated";
         } else {
           area.status = "not validated";
-          return;
+          return area;
         }
     }
 
@@ -220,7 +227,7 @@ class DirectMetropolitanCalculator {
         0
       );
       candidateList.Percentage =
-        (candidateList.VoiceNumber / area.stats.VotesExpressed) * 100;
+        Number((candidateList.VoiceNumber / area.stats.VotesExpressed) * 100).toFixed(2);
     });
     area.candidateLists = candidateListToKeep;
     area.candidateLists.sort((a, b) => {
@@ -246,13 +253,20 @@ class DirectMetropolitanCalculator {
       }
     }
 
+    let lastDate = new Date(deskRounds[0].DateCompletion);
+    deskRounds.forEach((desk) => {
+      if (lastDate - new Date(desk.DateCompletion) < 0)
+        lastDate = new Date(desk.DateCompletion);
+    });
+    section.DateCompletion = lastDate;
+
     section.stats = await this.calculateStats(deskRounds);
     let flag = true;
     switch (this.filter) {
       case "partial":
         if (section.stats.VotesExpressed === 0) {
           section.status = "no_results";
-          return;
+          return section;
         }
         section.status = "partial";
         break;
@@ -265,7 +279,7 @@ class DirectMetropolitanCalculator {
           section.status = "completed";
         } else {
           section.status = "incompleted";
-          return;
+          return section;
         }
         break;
       case "validated":
@@ -277,7 +291,7 @@ class DirectMetropolitanCalculator {
           section.status = "validated";
         } else {
           section.status = "not validated";
-          return;
+          return section ;
         }
     }
 
@@ -294,7 +308,7 @@ class DirectMetropolitanCalculator {
         0
       );
       candidateList.Percentage =
-        (candidateList.VoiceNumber / section.stats.VotesExpressed) * 100;
+        Number((candidateList.VoiceNumber / section.stats.VotesExpressed) * 100).toFixed(2);
     });
     section.candidateLists = candidateLists;
     section.candidateLists.sort((a, b) => {
diff --git a/web/style.css b/web/style.css
index a17f16f..d7a5bc0 100644
--- a/web/style.css
+++ b/web/style.css
@@ -120,13 +120,21 @@ select {
 }
 
 #round-desks .column,
-#candidate-lists .column {
+#candidate-lists .column,
+#news-flow {
   overflow-y: auto;
 }
 
+#news-flow,
+#news-flow .content {
+  flex: 1;
+}
+
 #round-desks .columns,
-#candidate-lists .columns {
+#candidate-lists .columns,
+#news-flow-section .card-content {
   max-height: 90%;
+  display: flex;
 }
 
 .card-header-success {
@@ -154,11 +162,25 @@ select {
   background-color: #fff;
 }
 
-.filter{
+.filter {
   text-align: center;
 }
 
 #news-flow-section {
   height: 45vh;
-  margin-bottom: 15px;;
+  margin-bottom: 15px;
+}
+
+.progressBar {
+  width: 99%;
+  margin: 3px;
+  background-color: lightgray;
+  border-radius: 5px;
+}
+
+.progressBarValue {
+  height: 30px;
+  white-space: nowrap;
+  padding: 4px;
+  border-radius: 5px;
 }
-- 
GitLab