gong at timer start - add a few TODOs
[westcastle.git] / js / index.js
index f58a403..d9f4576 100644 (file)
@@ -6,7 +6,7 @@ new Vue({
        },
        components: {
                'my-players': {
-                       props: ['players'],
+                       props: ['players','initPlayers'],
                        template: `
                                <div id="players">
                                        <div class="left">
@@ -27,6 +27,12 @@ new Vue({
                                                        </tr>
                                                </table>
                                        </div>
+                                       <div class="clear">
+                                               <input class="hide" id="upload" type="file" @change="upload"/>
+                                               <button class="btn block cancel" @click="uploadTrigger()" title="Charge la liste des joueurs, en principe en début de tournoi">
+                                                       (Ré)initialiser
+                                               </button>
+                                       </div>
                                </div>
                        `,
                        computed: {
@@ -41,12 +47,22 @@ new Vue({
                        methods: {
                                toggleAvailability: function(i) {
                                        this.players[i].available = 1 - this.players[i].available;
-                                       this.$forceUpdate(); //TODO (Vue.set... ?!)
+                               },
+                               uploadTrigger: function() {
+                                       document.getElementById("upload").click();
+                               },
+                               upload: function(e) {
+                                       let file = (e.target.files || e.dataTransfer.files)[0];
+                                       var reader = new FileReader();
+                                       reader.onloadend = ev => {
+                                               this.initPlayers(ev.currentTarget.result);
+                                       };
+                                       reader.readAsText(file);
                                },
                        },
                },
                'my-pairings': {
-                       props: ['players','writeScoreToDb'],
+                       props: ['players','commitScores'],
                        data: function() {
                                return {
                                        unpaired: [],
@@ -59,7 +75,14 @@ new Vue({
                        template: `
                                <div id="pairings">
                                        <div v-show="currentIndex < 0">
-                                               <button id="runPairing" class="block btn" @click="doPairings()">Nouvelle ronde</button>
+                                               <div class="button-container-horizontal">
+                                                       <button class="btn validate" :class="{hide: tables.length==0}" @click="commitScores()" title="Valide l'état actuel des scores (cf. rubrique Classement) en mémoire. Cette action est nécessaire au moins une fois en fin de tournoi après toutes les parties, et est recommandée après chaque ronde">
+                                                               Valider
+                                                       </button>
+                                                       <button id="doPairings" class="btn" :class="{cancel: tables.length>0}" @click="doPairings()" title="Répartit les joueurs actifs aléatoirement sur les tables">
+                                                               Nouvelle ronde
+                                                       </button>
+                                               </div>
                                                <div class="pairing" v-for="(table,index) in tables" :class="{scored: scored[index]}"
                                                                @click="showScoreForm(table,index)">
                                                        <p>Table {{ index+1 }}</p>
@@ -83,23 +106,24 @@ new Vue({
                                                                <td :class="{toto: players[tables[currentIndex][i]].prenom=='Toto'}">
                                                                        {{ players[tables[currentIndex][i]].prenom }} {{ players[tables[currentIndex][i]].nom }}
                                                                </td>
-                                                               <td><input type="text" v-model="sessions[currentIndex][i]"/></td>
+                                                               <td><input type="text" v-model="sessions[currentIndex][i]" :disabled="scored[currentIndex]"/></td>
                                                        </tr>
                                                </table>
                                                <div class="button-container-horizontal">
-                                                       <button class="btn validate" @click="setScore()">Enregistrer</button>
-                                                       <button class="btn" @click="currentIndex = -1">Fermer</button>
-                                               </div>
-                                               <div v-if="scored[currentIndex]" class="warning">
-                                                       Attention: un score a déjà été enregistré.
-                                                       Les points indiqués ici s'ajouteront : il faut d'abord
-                                                       <span class="link" @click="clickRestore()">restaurer l'état précédent.</span>
-                                                       Si c'est déjà fait, ignorer ce message :)
+                                                       <button :class="{hide:scored[currentIndex]}" class="btn validate" @click="setScore()" title="Enregistre le score dans la base (la rubrique Classement est mise à jour)">
+                                                               Enregistrer
+                                                       </button>
+                                                       <button :class="{hide:!scored[currentIndex]}" class="btn cancel" @click="resetScore()" title="Annule le score précédemment enregistré : l'action est visible dans la rubrique Classement">
+                                                               Annuler
+                                                       </button>
+                                                       <button class="btn" @click="closeScoreForm()">Fermer</button>
                                                </div>
                                        </div>
                                </div>
                        `,
                        methods: {
+                               // TODO: clic sur "Valider" télécharge la ronde courante
+                               // TODO: mémoriser les appariements passés pour éviter que les mêmes joueurs se rencontrent plusieurs fois
                                doPairings: function() {
                                        // Simple case first: 4 by 4
                                        let tables = [];
@@ -152,36 +176,57 @@ new Vue({
                                                this.sessions[index] = _.times(table.length, _.constant(0));
                                        this.currentIndex = index;
                                },
-                               setScore: function() {
+                               closeScoreForm: function() {
+                                       if (!this.scored[this.currentIndex])
+                                               this.sessions[this.currentIndex] = [];
+                                       this.currentIndex = -1;
+                               },
+                               getPdts: function() {
                                        let sortedSessions = this.sessions[this.currentIndex]
                                                .map( (s,i) => { return {value:parseInt(s), index:i}; })
                                                .sort( (a,b) => { return b.value - a.value; });
-                                       let pdts = [4, 2, 1, 0];
+                                       const ref_pdts = [4, 2, 1, 0];
                                        // NOTE: take care of ex-aequos (spread points subtotal)
                                        let curSum = 0, curCount = 0, start = 0;
+                                       let sortedPdts = [];
                                        for (let i=0; i<4; i++)
                                        {
-                                               // Update pdts:
-                                               curSum += pdts[i];
+                                               curSum += ref_pdts[i];
                                                curCount++;
                                                if (i==3 || sortedSessions[i].value > sortedSessions[i+1].value)
                                                {
                                                        let pdt = curSum / curCount;
                                                        for (let j=start; j<=i; j++)
-                                                               this.players[this.tables[this.currentIndex][sortedSessions[j].index]].pdt += pdt;
+                                                               sortedPdts.push(pdt);
                                                        curSum = 0;
                                                        curCount = 0;
                                                        start = i+1;
                                                }
-                                               // Update sessions:
+                                       }
+                                       // Re-order pdts to match table order
+                                       let pdts = [0, 0, 0, 0];
+                                       for (let i=0; i<4; i++)
+                                               pdts[sortedSessions[i].index] = sortedPdts[i];
+                                       return pdts;
+                               },
+                               setScore: function() {
+                                       let pdts = this.getPdts();
+                                       for (let i=0; i<4; i++)
+                                       {
+                                               this.players[this.tables[this.currentIndex][i]].pdt += pdts[i];
                                                this.players[this.tables[this.currentIndex][i]].session += parseInt(this.sessions[this.currentIndex][i]);
                                        }
-                                       this.scored[this.currentIndex] = true;
+                                       Vue.set(this.scored, this.currentIndex, true);
                                        this.currentIndex = -1;
-                                       this.writeScoreToDb();
                                },
-                               clickRestore: function() {
-                                       document.getElementById('restoreBtn').click();
+                               resetScore: function() {
+                                       let pdts = this.getPdts();
+                                       for (let i=0; i<4; i++)
+                                       {
+                                               this.players[this.tables[this.currentIndex][i]].pdt -= pdts[i];
+                                               this.players[this.tables[this.currentIndex][i]].session -= parseInt(this.sessions[this.currentIndex][i]);
+                                       }
+                                       Vue.set(this.scored, this.currentIndex, false);
                                },
                        },
                },
@@ -190,6 +235,7 @@ new Vue({
                                return {
                                        time: 0, //remaining time, in seconds
                                        running: false,
+                                       initialTime: 5400, //1h30
                                };
                        },
                        template: `
@@ -226,7 +272,7 @@ new Vue({
                                },
                                reset: function(e) {
                                        this.running = false;
-                                       this.time = 5400; //1:30
+                                       this.time = this.initialTime;
                                },
                                start: function() {
                                        if (!this.running)
@@ -237,6 +283,8 @@ new Vue({
                                                this.running = false;
                                                return;
                                        }
+                                       if (this.time == this.initialTime)
+                                               new Audio("sounds/gong.mp3").play(); //gong at the beginning
                                        setTimeout(() => {
                                                if (this.running)
                                                        this.time--;
@@ -249,7 +297,7 @@ new Vue({
                        },
                },
                'my-ranking': {
-                       props: ['players','sortByScore','writeScoreToDb'],
+                       props: ['players','sortByScore','commitScores'],
                        template: `
                                <div id="ranking">
                                        <table class="ranking">
@@ -267,8 +315,11 @@ new Vue({
                                                </tr>
                                        </table>
                                        <div class="button-container-vertical" style="width:200px">
-                                               <button class="btn cancel" @click="resetPlayers()">Réinitialiser</button>
-                                               <button id="restoreBtn" class="btn" @click="restoreLast()">Restaurer</button>
+                                               <a id="download" href="#"></a>
+                                               <button class="btn" @click="download()" title="Télécharge le classement courant au format CSV">Télécharger</button>
+                                               <button class="btn cancel" @click="resetPlayers()" title="Réinitialise les scores à zéro. ATTENTION: action irréversible">
+                                                       Réinitialiser
+                                               </button>
                                        </div>
                                </div>
                        `,
@@ -294,86 +345,83 @@ new Vue({
                                                .sort(this.sortByScore);
                                },
                                resetPlayers: function() {
+                                       if (confirm('Êtes-vous sûr ?'))
+                                       {
+                                               this.players
+                                                       .slice(1) //discard Toto
+                                                       .forEach( p => {
+                                                               p.pdt = 0;
+                                                               p.session = 0;
+                                                               p.available = 1;
+                                                       });
+                                               this.commitScores();
+                                               document.getElementById("doPairings").click();
+                                       }
+                               },
+                               download: function() {
+                                       // Prepare file content
+                                       let content = "prénom,nom,pdt,session\n";
                                        this.players
                                                .slice(1) //discard Toto
+                                               .sort(this.sortByScore)
                                                .forEach( p => {
-                                                       p.pdt = 0;
-                                                       p.session = 0;
-                                                       p.available = 1;
+                                                       content += p.prenom + "," + p.nom + "," + p.pdt + "," + p.session + "\n";
                                                });
-                                       this.writeScoreToDb();
-                                       document.getElementById("runPairing").click();
-                               },
-                               restoreLast: function() {
-                                       let xhr = new XMLHttpRequest();
-                                       let self = this;
-                                       xhr.onreadystatechange = function() {
-                                               if (this.readyState == 4 && this.status == 200)
-                                               {
-                                                       let players = JSON.parse(xhr.responseText);
-                                                       if (players.length > 0)
-                                                       {
-                                                               players.unshift({ //add ghost 4th player for 3-players tables
-                                                                       prenom: "Toto",
-                                                                       nom: "",
-                                                                       pdt: 0,
-                                                                       session: 0,
-                                                                       available: 0,
-                                                               });
-                                                               // NOTE: Vue warning "do not mutate property" if direct self.players = players
-                                                               for (let i=1; i<players.length; i++)
-                                                               {
-                                                                       players[i].pdt = parseFloat(players[i].pdt);
-                                                                       players[i].session = parseInt(players[i].session);
-                                                                       Vue.set(self.players, i, players[i]);
-                                                               }
-                                                       }
-                                               }
-                                       };
-                                       xhr.open("GET", "scripts/rw_players.php?restore=1", true);
-                                       xhr.send(null);
+                                       // Prepare and trigger download link
+                                       let downloadAnchor = document.getElementById("download");
+                                       downloadAnchor.setAttribute("download", "classement.csv");
+                                       downloadAnchor.href = "data:text/plain;charset=utf-8," + encodeURIComponent(content);
+                                       downloadAnchor.click();
                                },
                        },
                },
        },
        created: function() {
-               let xhr = new XMLHttpRequest();
-               let self = this;
-               xhr.onreadystatechange = function() {
-                       if (this.readyState == 4 && this.status == 200)
-                       {
-                               let players = JSON.parse(xhr.responseText);
-                               players.forEach( p => {
-                                       p.pdt = !!p.pdt ? parseFloat(p.pdt) : 0;
-                                       p.session = !!p.session ? parseInt(p.session) : 0;
-                                       p.available = !!p.available ? p.available : 1; //use integer for fputcsv PHP func
-                               });
-                               players.unshift({ //add ghost 4th player for 3-players tables
-                                       prenom: "Toto",
-                                       nom: "",
-                                       pdt: 0,
-                                       session: 0,
-                                       available: 0,
-                               });
-                               self.players = players;
-                       }
-               };
-               xhr.open("GET", "scripts/rw_players.php", true);
-               xhr.send(null);
+               let players = JSON.parse(localStorage.getItem("players"));
+               if (players !== null)
+               {
+                       this.addToto(players);
+                       this.players = players;
+               }
        },
        methods: {
+               addToto: function(array) {
+                       array.unshift({ //add ghost 4th player for 3-players tables
+                               prenom: "Toto",
+                               nom: "",
+                               pdt: 0,
+                               session: 0,
+                               available: 0,
+                       });
+               },
                // Used both in ranking and pairings:
                sortByScore: function(a,b) {
                        return b.pdt - a.pdt + (Math.atan(b.session - a.session) / (Math.PI/2)) / 2;
                },
-               writeScoreToDb: function() {
-                       let xhr = new XMLHttpRequest();
-                       xhr.open("POST", "scripts/rw_players.php");
-                       xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
-                       let orderedPlayers = this.players
-                               .slice(1) //discard Toto
-                               .sort(this.sortByScore);
-                       xhr.send("players="+encodeURIComponent(JSON.stringify(orderedPlayers)));
+               commitScores: function() {
+                       localStorage.setItem(
+                               "players",
+                               JSON.stringify(this.players.slice(1)) //discard Toto
+                       );
+               },
+               // Used in players, reinit players array
+               initPlayers: function(csv) {
+                       const allLines = csv
+                               .split(/\r\n|\n|\r/) //line breaks
+                               .splice(1); //discard header
+                       let players = allLines
+                               .filter( line => { return line.length > 0; }) //remove empty lines
+                               .map( line => {
+                                       let parts = line.split(",");
+                                       let p = { prenom: parts[0], nom: parts[1] };
+                                       p.pdt = parts.length > 2 ? parseFloat(parts[2]) : 0;
+                                       p.session = parts.length > 3 ? parseInt(parts[3]) : 0;
+                                       p.available = parts.length > 4 ? parts[4] : 1;
+                                       return p;
+                               });
+                       this.addToto(players);
+                       this.players = players;
+                       this.commitScores(); //save players in memory
                },
        },
 });