From 6fdefbc86ecf070c91e49573fe73053aa5d38889 Mon Sep 17 00:00:00 2001
From: Alexis POYEN <apoyen@grandlyon.com>
Date: Thu, 4 Jun 2020 16:22:07 +0200
Subject: [PATCH] Resolve "Handle Capturers affectation"

---
 internal/models/capturer.go              |   1 -
 internal/models/capturerDeskRound.go     |  89 ++++++++++
 internal/models/models.go                |   2 +
 internal/rootmux/admin_test.go           |   5 +
 internal/rootmux/capturer_test.go        |   5 +
 internal/rootmux/rootmux_test.go         |   8 +-
 internal/rootmux/visualizer_test.go      |   5 +
 web/components/management/round-desks.js | 207 ++++++++++++++++++++++-
 web/components/management/round.js       |   1 +
 web/style.css                            |   8 +
 10 files changed, 322 insertions(+), 9 deletions(-)
 create mode 100644 internal/models/capturerDeskRound.go

diff --git a/internal/models/capturer.go b/internal/models/capturer.go
index 115b42d..f404ef3 100644
--- a/internal/models/capturer.go
+++ b/internal/models/capturer.go
@@ -82,7 +82,6 @@ func (d *DataHandler) getCapturerCapturer(w http.ResponseWriter, r *http.Request
 			http.Error(w, ErrorIDIsMissing, http.StatusNotFound)
 			return
 		}
-		// fmt.Println(o)
 		if o.UserID != user.UserID {
 			http.Error(w, ErrorCannotAccessRessource, http.StatusForbidden)
 			return
diff --git a/internal/models/capturerDeskRound.go b/internal/models/capturerDeskRound.go
new file mode 100644
index 0000000..eeb51ee
--- /dev/null
+++ b/internal/models/capturerDeskRound.go
@@ -0,0 +1,89 @@
+package models
+
+import (
+	"encoding/json"
+	"net/http"
+	"strconv"
+	"strings"
+
+	"forge.grandlyon.com/apoyen/elections/internal/auth"
+)
+
+type capturerDeskRound struct {
+	CapturerID  uint
+	DeskRoundID uint
+}
+
+// handleCapturer handle API calls on Capturer
+func (d *DataHandler) handleCapturerDeskRound(w http.ResponseWriter, r *http.Request) {
+	id, _ := strconv.Atoi(strings.TrimPrefix(r.URL.Path, "/api/CapturerDeskRound/"))
+	switch method := r.Method; method {
+	case "POST":
+		switch auth.GetLoggedUserTechnical(w, r).Role {
+		case "ADMIN":
+			d.postCapturerDeskRound(w, r)
+		case "CAPTURER", "VISUALIZER":
+			http.Error(w, ErrorNotAuthorizeMethodOnRessource, http.StatusMethodNotAllowed)
+		default:
+			http.Error(w, ErrorRoleOfLoggedUser, http.StatusInternalServerError)
+		}
+	case "DELETE":
+		switch auth.GetLoggedUserTechnical(w, r).Role {
+		case "ADMIN":
+			d.deleteCapturerDeskRound(w, r, id)
+		case "CAPTURER", "VISUALIZER":
+			http.Error(w, ErrorNotAuthorizeMethodOnRessource, http.StatusMethodNotAllowed)
+		default:
+			http.Error(w, ErrorRoleOfLoggedUser, http.StatusInternalServerError)
+		}
+	default:
+		http.Error(w, "method not allowed", 400)
+	}
+}
+
+func (d *DataHandler) postCapturerDeskRound(w http.ResponseWriter, r *http.Request) {
+	var o capturerDeskRound
+	err := json.NewDecoder(r.Body).Decode(&o)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	var capturer Capturer
+	if err := d.db.First(&capturer, o.CapturerID).Error; err != nil {
+		http.Error(w, ErrorIDDoesNotExist, http.StatusNotFound)
+		return
+	}
+
+	var deskRound DeskRound
+	if err := d.db.First(&deskRound, o.DeskRoundID).Error; err != nil {
+		http.Error(w, ErrorIDDoesNotExist, http.StatusNotFound)
+		return
+	}
+
+	d.db.Model(&capturer).Association("DeskRounds").Append(&deskRound)
+	d.db.Preload("DeskRounds").First(&capturer)
+	json.NewEncoder(w).Encode(capturer)
+}
+
+func (d *DataHandler) deleteCapturerDeskRound(w http.ResponseWriter, r *http.Request, id int) {
+	var o capturerDeskRound
+	err := json.NewDecoder(r.Body).Decode(&o)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	var capturer Capturer
+	if err := d.db.First(&capturer, o.CapturerID).Error; err != nil {
+		http.Error(w, ErrorIDDoesNotExist, http.StatusNotFound)
+		return
+	}
+
+	var deskRound DeskRound
+	if err := d.db.First(&deskRound, o.DeskRoundID).Error; err != nil {
+		http.Error(w, ErrorIDDoesNotExist, http.StatusNotFound)
+		return
+	}
+
+	d.db.Model(&capturer).Association("DeskRounds").Delete(&deskRound)
+	json.NewEncoder(w).Encode(capturer)
+}
diff --git a/internal/models/models.go b/internal/models/models.go
index 1939950..2788b42 100644
--- a/internal/models/models.go
+++ b/internal/models/models.go
@@ -232,6 +232,8 @@ func (d *DataHandler) ProcessAPI(w http.ResponseWriter, r *http.Request) {
 		d.handleRound(w, r)
 	case "DeskRound":
 		d.handleDeskRound(w, r)
+	case "CapturerDeskRound":
+		d.handleCapturerDeskRound(w, r)
 	}
 
 }
diff --git a/internal/rootmux/admin_test.go b/internal/rootmux/admin_test.go
index b807921..64b1c73 100644
--- a/internal/rootmux/admin_test.go
+++ b/internal/rootmux/admin_test.go
@@ -87,6 +87,11 @@ func AdminTests(t *testing.T) {
 		// Delete a DeskRound should fail with 400
 		do("DELETE", "/api/DeskRound/1", xsrfHeader, ``, 400, `method not allowed`)
 
+		// Add deskround to capturer
+		do("POST", "/api/CapturerDeskRound", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 200, `{"ID":1,"UserID":2,"Name":"capturer","DeskRounds":[{"ID":1,"RoundID":1,"DeskID":1,"Capturers":null,"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":false,"Votes":null}]}`)
+		// Remove DeskRound to capturer
+		do("DELETE", "/api/CapturerDeskRound/1", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 200, ``)
+
 		// Update a DeskRound should be done after desk completion by capturing votes
 		// do("PUT", "/api/DeskRound/1", xsrfHeader, `{"ID":1,"Validated":true}`, 200, `{"ID":1,"RoundID":1,"DeskID":1,"Capturers":[],"Completed":false,"DateCompletion":"0001-01-01T00:00:00Z","Validated":true,"Votes":[]}`)
 
diff --git a/internal/rootmux/capturer_test.go b/internal/rootmux/capturer_test.go
index 5ccd09e..2de600a 100644
--- a/internal/rootmux/capturer_test.go
+++ b/internal/rootmux/capturer_test.go
@@ -99,6 +99,11 @@ func CapturerTests(t *testing.T) {
 		// Delete a DeskRound should fail with 400
 		do("DELETE", "/api/DeskRound/1", xsrfHeader, ``, 400, `method not allowed`)
 
+		// Add deskround to capturer should fail with 405
+		do("POST", "/api/CapturerDeskRound", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 405, `You're not authorize to execute this method on this ressource.`)
+		// Remove DeskRound to capturer
+		do("DELETE", "/api/CapturerDeskRound/1", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 405, `You're not authorize to execute this method on this ressource.`)
+
 	}
 	// Do a in memory login with an known admin
 	do("POST", "/Login", noH, `{"login": "capturer","password": "password"}`, 200, "")
diff --git a/internal/rootmux/rootmux_test.go b/internal/rootmux/rootmux_test.go
index 4295acf..c8dfb25 100644
--- a/internal/rootmux/rootmux_test.go
+++ b/internal/rootmux/rootmux_test.go
@@ -61,10 +61,10 @@ func TestAll(t *testing.T) {
 	removeRoundRemoveDeskRoundsTest(t)
 	resetData(t)
 	AdminTests(t)
-	// resetDataWithData(t)
-	// CapturerTests(t)
-	// resetDataWithData(t)
-	// VisualizerTests(t)
+	resetDataWithData(t)
+	CapturerTests(t)
+	resetDataWithData(t)
+	VisualizerTests(t)
 
 	os.RemoveAll("./data")
 }
diff --git a/internal/rootmux/visualizer_test.go b/internal/rootmux/visualizer_test.go
index f872760..a36147f 100644
--- a/internal/rootmux/visualizer_test.go
+++ b/internal/rootmux/visualizer_test.go
@@ -86,6 +86,11 @@ func VisualizerTests(t *testing.T) {
 		// Delete a DeskRound should fail with 400
 		do("DELETE", "/api/DeskRound/1", xsrfHeader, ``, 400, `method not allowed`)
 
+		// Add deskround to capturer should fail with 405
+		do("POST", "/api/CapturerDeskRound", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 405, `You're not authorize to execute this method on this ressource.`)
+		// Remove DeskRound to capturer
+		do("DELETE", "/api/CapturerDeskRound/1", xsrfHeader, `{"CapturerID":1,"DeskRoundID":1}`, 405, `You're not authorize to execute this method on this ressource.`)
+
 	}
 	// Do a in memory login with an known admin
 	do("POST", "/Login", noH, `{"login": "visualizer","password": "password"}`, 200, "")
diff --git a/web/components/management/round-desks.js b/web/components/management/round-desks.js
index 28ce765..6d82d70 100644
--- a/web/components/management/round-desks.js
+++ b/web/components/management/round-desks.js
@@ -1,5 +1,6 @@
 // Imports
 import * as Auth from "/services/auth/auth.js";
+import * as Common from "/services/common/common.js";
 import * as Getters from "/services/common/getters.js";
 import * as Messages from "/services/messages/messages.js";
 
@@ -43,6 +44,61 @@ class RoundDesk {
       </div>
     `;
     current_user = await Auth.GetUser();
+    this.mountModal("capturers-modal");
+    this.handleDom();
+  }
+
+  handleDom() {
+    let roundHandler = this;
+    document.getElementById(`round-new`).addEventListener("click", function () {
+      roundHandler.newRound();
+    });
+
+    document
+      .getElementById(`capturers-modal-close`)
+      .addEventListener("click", function () {
+        Common.toggleModal("capturers-modal", "capturers-modal-card");
+      });
+  }
+
+  mountModal(where) {
+    const mountpoint = where;
+    document.getElementById(mountpoint).innerHTML = /* HTML */ `
+      <div class="modal-background"></div>
+      <div class="modal-card" id="capturers-modal-card">
+        <header class="modal-card-head">
+          <p class="modal-card-title">Ajout/supression de saisisseurs</p>
+          <button
+            class="delete"
+            aria-label="close"
+            id="capturers-modal-close"
+          ></button>
+        </header>
+        <section class="modal-card-body">
+          <div class="columns">
+            <div class="column">
+              <header class="card-header card-header-success">
+                <p class="card-header-title">
+                  Saisisseurs actifs
+                </p>
+              </header>
+              <div id="active-capturers">
+                Liste des saisisseurs actif
+              </div>
+            </div>
+            <div class="column">
+              <header class="card-header card-header-danger">
+                <p class="card-header-title">
+                  Saisisseurs disponibles
+                </p>
+              </header>
+
+              <div id="available-capturers""> Liste des saisseurs disponibles
+            </div>
+          </div>
+        </section>
+      </div>
+    `;
   }
 
   deskRoundTemplate(deskRound) {
@@ -54,7 +110,17 @@ class RoundDesk {
               id="deskrounds-deskround-desc-${deskRound.ID}"
               class="level-left"
             ></div>
-            <div class="level-right"></div>
+            <div class="level-right">
+              <a
+                id="deskrounds-deskround-capturers-${deskRound.ID}"
+                class="button is-link is-small"
+                title="Saisisseurs"
+              >
+                <span class="icon is-small">
+                  <i class="fas fa-user"></i>
+                </span>
+              </a>
+            </div>
           </nav>
         </div>
       </div>
@@ -85,7 +151,10 @@ class RoundDesk {
         </div>
         <div class="field">
           <div class="control">
-            <label class="checkbox" ${!this.deskRound.Completed ? 'disabled="true"' : ""}>
+            <label
+              class="checkbox"
+              ${!this.deskRound.Completed ? 'disabled="true"' : ""}
+            >
               <input
                 class="input"
                 type="checkbox"
@@ -109,6 +178,19 @@ class RoundDesk {
     </div>`;
   }
 
+  capturersTemplate(capturer) {
+    return /* HTML */ `<div class="card card-list">
+      <div id="capturers-capturer-${capturer.ID}" class="card-content">
+        <div class="content">
+          <nav class="level">
+            <div class="level-left">${capturer.Name}</div>
+            <div class="level-right"></div>
+          </nav>
+        </div>
+      </div>
+    </div>`;
+  }
+
   async displayDesks() {
     let deskRounds = await this.updateDeskRounds();
     const markup = deskRounds
@@ -128,7 +210,12 @@ class RoundDesk {
           deskRoundHandler.activateDeskRound(deskRound);
           deskRoundHandler.deskRound = deskRound;
           deskRoundHandler.displayDeskRoundsDetails();
-          // TODO open capturers affected
+        });
+      document
+        .getElementById(`deskrounds-deskround-capturers-${deskRound.ID}`)
+        .addEventListener("click", function () {
+          Common.toggleModal("capturers-modal", "capturers-modal-card");
+          deskRoundHandler.refreshCapturers();
         });
     });
   }
@@ -182,7 +269,6 @@ class RoundDesk {
   }
 
   async saveDeskRound() {
-    let round;
     try {
       const response = await fetch("/api/DeskRound/" + this.deskRound.ID, {
         method: "PUT",
@@ -208,4 +294,117 @@ class RoundDesk {
     Messages.Show("is-success", "Bureau de vote mis à jour");
     this.displayDesks();
   }
+
+  async refreshCapturers() {
+    let deskRoundHandler = this;
+    let capturersAvailable;
+    let capturersActive;
+    try {
+      const response = await fetch("/api/Capturer/", {
+        method: "GET",
+        headers: new Headers({
+          "XSRF-Token": current_user.xsrftoken,
+        }),
+      });
+      if (response.status !== 200) {
+        throw new Error(
+          `Capturers could not be fetched (status ${response.status})`
+        );
+      }
+      let capturers = await response.json();
+      capturersAvailable = capturers.filter(function (capturer) {
+        let flag = true;
+        capturer.DeskRounds.forEach((deskRound) => {
+          if (deskRound.ID === deskRoundHandler.deskRound.ID) flag = false;
+        });
+        return flag;
+      });
+      capturersActive = capturers.filter(function (capturer) {
+        let flag = false;
+        capturer.DeskRounds.forEach((deskRound) => {
+          if (deskRound.ID === deskRoundHandler.deskRound.ID) flag = true;
+        });
+        return flag;
+      });
+    } catch (e) {
+      Messages.Show("is-warning", e.message);
+      console.error(e);
+    }
+
+    let markup = capturersAvailable
+      .map((capturer) => this.capturersTemplate(capturer))
+      .join("");
+    document.getElementById("available-capturers").innerHTML = markup;
+
+    markup = capturersActive
+      .map((capturer) => this.capturersTemplate(capturer))
+      .join("");
+    document.getElementById("active-capturers").innerHTML = markup;
+
+    capturersAvailable.map(async (capturer) => {
+      document
+        .getElementById(`capturers-capturer-${capturer.ID}`)
+        .addEventListener("click", async function () {
+          await deskRoundHandler.addCapturerToDesk(capturer);
+          await deskRoundHandler.refreshCapturers();
+        });
+    });
+
+    capturersActive.map(async (capturer) => {
+      document
+        .getElementById(`capturers-capturer-${capturer.ID}`)
+        .addEventListener("click", async function () {
+          await deskRoundHandler.removeCapturerFromDesk(capturer);
+          await deskRoundHandler.refreshCapturers();
+        });
+    });
+  }
+
+  async addCapturerToDesk(capturer) {
+    try {
+      const response = await fetch("/api/CapturerDeskRound/", {
+        method: "POST",
+        headers: new Headers({
+          "XSRF-Token": current_user.xsrftoken,
+        }),
+        body: JSON.stringify({
+          CapturerID: capturer.ID,
+          DeskRoundID: this.deskRound.ID,
+        }),
+      });
+      if (response.status !== 200) {
+        throw new Error(
+          `DeskRound could not be added to capturer (status ${response.status})`
+        );
+      }
+      await response.json();
+    } catch (e) {
+      Messages.Show("is-warning", e.message);
+      console.error(e);
+      return;
+    }
+  }
+
+  async removeCapturerFromDesk(capturer) {
+    try {
+      const response = await fetch("/api/CapturerDeskRound/", {
+        method: "delete",
+        headers: new Headers({
+          "XSRF-Token": current_user.xsrftoken,
+        }),
+        body: JSON.stringify({
+          CapturerID: capturer.ID,
+          DeskRoundID: this.deskRound.ID,
+        }),
+      });
+      if (response.status !== 200) {
+        throw new Error(
+          `DeskRound could not be removed from capturer (status ${response.status})`
+        );
+      }
+    } catch (e) {
+      Messages.Show("is-warning", e.message);
+      console.error(e);
+    }
+  }
 }
diff --git a/web/components/management/round.js b/web/components/management/round.js
index a977cea..43b9b9e 100644
--- a/web/components/management/round.js
+++ b/web/components/management/round.js
@@ -37,6 +37,7 @@ class Round {
       </div>
 
       <div class="modal" id="round-modal"></div>
+      <div class="modal" id="capturers-modal"></div>
     `;
     current_user = await Auth.GetUser();
     this.roundsHandler = await RoundsCard.mount("rounds-list", this)
diff --git a/web/style.css b/web/style.css
index 8039cb5..b75e094 100644
--- a/web/style.css
+++ b/web/style.css
@@ -121,3 +121,11 @@ select {
 #round-desks .columns {
   max-height: 90%;
 }
+
+.card-header-success{
+  background-color: rgba(127,186,0,.95);
+}
+
+.card-header-danger{
+  background-color: rgba(242,80,34,.95);
+}
\ No newline at end of file
-- 
GitLab