},
components: {
'my-players': {
- props: ['players'],
+ props: ['players','initPlayers'],
template: `
<div id="players">
- <div id="active">
+ <div class="left">
<p>Présents</p>
<table class="list">
<tr v-for="p in sortedPlayers" v-if="p.available" @click="toggleAvailability(p.index)">
</tr>
</table>
</div>
- <div id="inactive">
+ <div id="inactive" class="right">
<p>Absents</p>
<table class="list">
<tr v-for="p in sortedPlayers" v-if="!p.available && p.nom!=''" @click="toggleAvailability(p.index)">
</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: {
methods: {
toggleAvailability: function(i) {
this.players[i].available = 1 - this.players[i].available;
- this.$forceUpdate(); //TODO (Vue.set... ?!)
},
- },
- },
- 'my-ranking': {
- props: ['players','sortByScore','rankPeople'],
- template: `
- <div id="ranking">
- <table class="ranking">
- <tr class="title">
- <th>Rang</th>
- <th>Joueur</th>
- <th>Points</th>
- <th>Mini-pts</th>
- </tr>
- <tr v-for="p in sortedPlayers">
- <td>{{ p.rank }}</td>
- <td>{{ p.prenom }} {{ p.nom }}</td>
- <td>{{ p.pdt }}</td>
- <td>{{ p.session }}</td>
- </tr>
- </table>
- </div>
- `,
- computed: {
- sortedPlayers: function() {
- let res = this.rankPeople();
- // Add rank information (taking care of ex-aequos)
- let rank = 1;
- for (let i=0; i<res.length; i++)
- {
- if (i==0 || this.sortByScore(res[i],res[i-1]) == 0)
- res[i].rank = rank;
- else //strictly lower scoring
- res[i].rank = ++rank;
- }
- return res;
+ 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','sortByScore'],
+ props: ['players','commitScores'],
data: function() {
return {
unpaired: [],
tables: [], //array of arrays of players indices
- pdts: [], //"points de table" for each table
sessions: [], //"mini-points" for each table
currentIndex: -1, //table index for scoring
+ scored: [], //boolean for each table index
};
},
template: `
<div id="pairings">
<div v-show="currentIndex < 0">
- <button class="block btn" @click="shuffle()">Appariement</button>
- <div class="pairing" v-for="(table,index) in tables" :class="{scored: pdts[index].length > 0}"
+ <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>
<table>
<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]" value="0"/></td>
+ <td><input type="text" v-model="sessions[currentIndex][i]" :disabled="scored[currentIndex]"/></td>
</tr>
</table>
- <div class="button-container">
- <button class="btn" @click="setScore()">Enregistrer</button>
- <button class="btn cancel" @click="resetScore()">Annuler</button>
+ <div class="button-container-horizontal">
+ <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>
t.push(0); //index of "Toto", ghost player
});
this.tables = tables;
- this.pdts = tables.map( t => { return []; }); //empty pdts
this.sessions = tables.map( t => { return []; }); //empty sessions
- },
- shuffle: function() {
- this.doPairings();
+ this.scored = tables.map( t => { return false; }); //nothing scored yet
+ this.currentIndex = -1; //required if reset while scoring
},
showScoreForm: function(table,index) {
- if (this.pdts[index].length > 0)
- return; //already scored
- this.pdts[index] = _.times(table.length, _.constant(0));
- this.sessions[index] = _.times(table.length, _.constant(0));
+ if (this.sessions[index].length == 0)
+ 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:s, index:i}; })
- .sort( (a,b) => { return parseInt(b.value) - parseInt(a.value); });
- let pdts = [4, 2, 1, 0]; //TODO: ex-aequos ?!
- for (let i=0; i<this.tables[this.currentIndex].length; i++)
+ .map( (s,i) => { return {value:parseInt(s), index:i}; })
+ .sort( (a,b) => { return b.value - a.value; });
+ 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++)
+ {
+ 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++)
+ sortedPdts.push(pdt);
+ curSum = 0;
+ curCount = 0;
+ start = i+1;
+ }
+ }
+ // 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][sortedSessions[i].index]].pdt += pdts[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, true);
this.currentIndex = -1;
- this.writeScoreToDb();
},
resetScore: function() {
- this.pdts[this.currentIndex] = [];
- this.sessions[this.currentIndex] = [];
- this.currentIndex = -1;
+ 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);
+ },
+ },
+ },
+ 'my-timer': {
+ data: function() {
+ return {
+ time: 0, //remaining time, in seconds
+ running: false,
+ };
+ },
+ template: `
+ <div id="timer" :style="{lineHeight: divHeight + 'px', fontSize: 0.66*divHeight + 'px', width: divWidth + 'px', height: divHeight + 'px'}">
+ <div @click.left="pauseResume()" @click.right.prevent="reset()" :class="{timeout:time==0}">
+ {{ formattedTime }}
+ </div>
+ <img class="close-cross" src="img/cross.svg" @click="$emit('clockover')"/>
+ </div>
+ `,
+ computed: {
+ formattedTime: function() {
+ let seconds = this.time % 60;
+ let minutes = Math.floor(this.time / 60);
+ return this.padToZero(minutes) + ":" + this.padToZero(seconds);
},
- 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
+ divHeight: function() {
+ return screen.height;
+ },
+ divWidth: function() {
+ return screen.width;
+ },
+ },
+ methods: {
+ padToZero: function(a) {
+ if (a < 10)
+ return "0" + a;
+ return a;
+ },
+ pauseResume: function() {
+ this.running = !this.running;
+ if (this.running)
+ this.start();
+ },
+ reset: function(e) {
+ this.running = false;
+ this.time = 5400; //1:30
+ },
+ start: function() {
+ if (!this.running)
+ return;
+ if (this.time == 0)
+ {
+ new Audio("sounds/gong.mp3").play();
+ this.running = false;
+ return;
+ }
+ setTimeout(() => {
+ if (this.running)
+ this.time--;
+ this.start();
+ }, 1000);
+ },
+ },
+ created: function() {
+ this.reset();
+ },
+ },
+ 'my-ranking': {
+ props: ['players','sortByScore','commitScores'],
+ template: `
+ <div id="ranking">
+ <table class="ranking">
+ <tr class="title">
+ <th>Rang</th>
+ <th>Joueur</th>
+ <th>Points</th>
+ <th>Mini-pts</th>
+ </tr>
+ <tr v-for="p in sortedPlayers">
+ <td>{{ p.rank }}</td>
+ <td>{{ p.prenom }} {{ p.nom }}</td>
+ <td>{{ p.pdt }}</td>
+ <td>{{ p.session }}</td>
+ </tr>
+ </table>
+ <div class="button-container-vertical" style="width:200px">
+ <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>
+ `,
+ computed: {
+ sortedPlayers: function() {
+ let res = this.rankPeople();
+ // Add rank information (taking care of ex-aequos)
+ let rank = 1;
+ for (let i=0; i<res.length; i++)
+ {
+ if (i==0 || this.sortByScore(res[i],res[i-1]) == 0)
+ res[i].rank = rank;
+ else //strictly lower scoring
+ res[i].rank = ++rank;
+ }
+ return res;
+ },
+ },
+ methods: {
+ rankPeople: function() {
+ return this.players
.slice(1) //discard Toto
- .map( p => { return Object.assign({}, p); }) //deep (enough) copy
.sort(this.sortByScore);
- xhr.send("players="+encodeURIComponent(JSON.stringify(orderedPlayers)));
+ },
+ 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 => {
+ content += p.prenom + "," + p.nom + "," + p.pdt + "," + p.session + "\n";
+ });
+ // 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 ? parseInt(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: {
- rankPeople: function() {
- return this.players
- .slice(1) //discard Toto
- .map( p => { return Object.assign({}, p); }) //to not alter original array
- .sort(this.sortByScore);
+ 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;
},
+ 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
+ },
},
});