mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2024-11-27 11:33:02 +00:00
2a298a3ee4
Signed-off-by: Emmy D'Anello <emmy.danello@animath.fr>
853 lines
34 KiB
JavaScript
853 lines
34 KiB
JavaScript
(async () => {
|
|
// check notification permission
|
|
// This is useful to alert people that they should do something
|
|
await Notification.requestPermission()
|
|
})()
|
|
|
|
// TODO ETEAM Mieux paramétriser (5 pour le TFJM², 6 pour l'ETEAM)
|
|
const RECOMMENDED_SOLUTIONS_COUNT = 6
|
|
|
|
const problems_count = JSON.parse(document.getElementById('problems_count').textContent)
|
|
|
|
const tournaments = JSON.parse(document.getElementById('tournaments_list').textContent)
|
|
let socket = null
|
|
|
|
const messages = document.getElementById('messages')
|
|
|
|
/**
|
|
* Request to abort the draw of the given tournament.
|
|
* Only volunteers are allowed to do this.
|
|
* @param tid The tournament id
|
|
*/
|
|
function abortDraw(tid) {
|
|
socket.send(JSON.stringify({'tid': tid, 'type': 'abort'}))
|
|
}
|
|
|
|
/**
|
|
* Request to cancel the last step.
|
|
* Only volunteers are allowed to do this.
|
|
* @param tid The tournament id
|
|
*/
|
|
function cancelLastStep(tid) {
|
|
socket.send(JSON.stringify({'tid': tid, 'type': 'cancel'}))
|
|
}
|
|
|
|
/**
|
|
* Request to launch a dice between 1 and 100, for the two first steps.
|
|
* The parameter `trigram` can be specified (by volunteers) to launch a dice for a specific team.
|
|
* @param tid The tournament id
|
|
* @param trigram The trigram of the team that a volunteer wants to force the dice launch (default: null)
|
|
* @param result The forced value. Null if unused (for regular people)
|
|
*/
|
|
function drawDice(tid, trigram = null, result = null) {
|
|
socket.send(JSON.stringify({'tid': tid, 'type': 'dice', 'trigram': trigram, 'result': result}))
|
|
}
|
|
|
|
/**
|
|
* Fetch the requested dice from the buttons and request to draw it.
|
|
* Only available for debug purposes and for admins.
|
|
* @param tid The tournament id
|
|
*/
|
|
function drawDebugDice(tid) {
|
|
let dice_10 = parseInt(document.querySelector(`input[name="debug-dice-${tid}-10"]:checked`).value)
|
|
let dice_1 = parseInt(document.querySelector(`input[name="debug-dice-${tid}-1"]:checked`).value)
|
|
let result = (dice_10 + dice_1) || 100
|
|
let team_div = document.querySelector(`div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`)
|
|
let team = team_div.getAttribute("data-team")
|
|
drawDice(tid, team, result)
|
|
}
|
|
|
|
/**
|
|
* Request to draw a new problem.
|
|
* @param tid The tournament id
|
|
* @param problem The forced problem. Null if unused (for regular people)
|
|
*/
|
|
function drawProblem(tid, problem = null) {
|
|
socket.send(JSON.stringify({'tid': tid, 'type': 'draw_problem', 'problem': problem}))
|
|
}
|
|
|
|
/**
|
|
* Accept the current proposed problem.
|
|
* @param tid The tournament id
|
|
*/
|
|
function acceptProblem(tid) {
|
|
socket.send(JSON.stringify({'tid': tid, 'type': 'accept'}))
|
|
}
|
|
|
|
/**
|
|
* Reject the current proposed problem.
|
|
* @param tid The tournament id
|
|
*/
|
|
function rejectProblem(tid) {
|
|
socket.send(JSON.stringify({'tid': tid, 'type': 'reject'}))
|
|
}
|
|
|
|
/**
|
|
* Volunteers can export the draw to make it available for notation.
|
|
* @param tid The tournament id
|
|
*/
|
|
function exportDraw(tid) {
|
|
socket.send(JSON.stringify({'tid': tid, 'type': 'export'}))
|
|
}
|
|
|
|
/**
|
|
* Volunteers can make the draw continue for the second round of the final.
|
|
* @param tid The tournament id
|
|
*/
|
|
function continueFinal(tid) {
|
|
socket.send(JSON.stringify({'tid': tid, 'type': 'continue_final'}))
|
|
}
|
|
|
|
/**
|
|
* Display a new notification with the given title and the given body.
|
|
* @param title The title of the notification
|
|
* @param body The body of the notification
|
|
* @param timeout The time (in milliseconds) after that the notification automatically closes. 0 to make indefinite. Default to 5000 ms.
|
|
* @return Notification
|
|
*/
|
|
function showNotification(title, body, timeout = 5000) {
|
|
let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"})
|
|
if (timeout)
|
|
setTimeout(() => notif.close(), timeout)
|
|
return notif
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if (document.location.hash) {
|
|
// Open the tab of the tournament that is present in the hash
|
|
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(elem => {
|
|
if ('#' + elem.innerText.toLowerCase() === document.location.hash.toLowerCase()) {
|
|
elem.click()
|
|
}
|
|
})
|
|
}
|
|
|
|
// When a tab is opened, add the tournament name in the hash
|
|
document.querySelectorAll('button[data-bs-toggle="tab"]').forEach(
|
|
elem => elem.addEventListener(
|
|
'click', () => document.location.hash = '#' + elem.innerText.toLowerCase()))
|
|
|
|
/**
|
|
* Add alert message on the top on the interface.
|
|
* @param message The content of the alert.
|
|
* @param type The alert type, which is a bootstrap color (success, info, warning, danger,…).
|
|
* @param timeout The time (in milliseconds) before the alert is auto-closing. 0 to infinitely, default to 5000 ms.
|
|
*/
|
|
function addMessage(message, type, timeout = 5000) {
|
|
const wrapper = document.createElement('div')
|
|
wrapper.innerHTML = [
|
|
`<div class="alert alert-${type} alert-dismissible" role="alert">`,
|
|
`<div>${message}</div>`,
|
|
'<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
|
|
].join('\n')
|
|
messages.append(wrapper)
|
|
|
|
if (timeout)
|
|
setTimeout(() => wrapper.remove(), timeout)
|
|
}
|
|
|
|
/**
|
|
* Update the information banner.
|
|
* @param tid The tournament id
|
|
* @param info The content to updated
|
|
*/
|
|
function setInfo(tid, info) {
|
|
document.getElementById(`messages-${tid}`).innerHTML = info
|
|
}
|
|
|
|
/**
|
|
* Open the draw interface, given the list of teams.
|
|
* @param tid The tournament id
|
|
* @param teams The list of teams (represented by their trigrams) that are present on this draw.
|
|
*/
|
|
function drawStart(tid, teams) {
|
|
// Hide the not-started-banner
|
|
document.getElementById(`banner-not-started-${tid}`).classList.add('d-none')
|
|
// Display the full draw interface
|
|
document.getElementById(`draw-content-${tid}`).classList.remove('d-none')
|
|
|
|
let dicesDiv = document.getElementById(`dices-${tid}`)
|
|
for (let team of teams) {
|
|
// Add empty dice score badge for each team
|
|
let col = document.createElement('div')
|
|
col.classList.add('col-md-1')
|
|
dicesDiv.append(col)
|
|
|
|
let diceDiv = document.createElement('div')
|
|
diceDiv.id = `dice-${tid}-${team}`
|
|
diceDiv.classList.add('badge', 'rounded-pill', 'text-bg-warning')
|
|
if (document.getElementById(`abort-${tid}`) !== null) {
|
|
// Check if this is a volunteer, who can launch a die for a specific team
|
|
diceDiv.onclick = (_) => drawDice(tid, team)
|
|
}
|
|
diceDiv.textContent = `${team} 🎲 ??`
|
|
col.append(diceDiv)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abort the current draw, and make all invisible, except the not-started-banner.
|
|
* @param tid The tournament id
|
|
*/
|
|
function drawAbort(tid) {
|
|
document.getElementById(`banner-not-started-${tid}`).classList.remove('d-none')
|
|
document.getElementById(`draw-content-${tid}`).classList.add('d-none')
|
|
document.getElementById(`dices-${tid}`).innerHTML = ""
|
|
document.getElementById(`recap-${tid}-round-list`).innerHTML = ""
|
|
document.getElementById(`tables-${tid}`).innerHTML = ""
|
|
updateDiceVisibility(tid, false)
|
|
updateBoxVisibility(tid, false)
|
|
updateButtonsVisibility(tid, false)
|
|
updateExportVisibility(tid, false)
|
|
updateContinueVisibility(tid, false)
|
|
}
|
|
|
|
/**
|
|
* This function is triggered after a new dice result. We update the score of the team.
|
|
* Can be resetted to empty values if the result is null.
|
|
* @param tid The tournament id
|
|
* @param trigram The trigram of the team that launched its dice
|
|
* @param result The result of the dice. null if it is a reset.
|
|
*/
|
|
function updateDiceInfo(tid, trigram, result) {
|
|
let elem = document.getElementById(`dice-${tid}-${trigram}`)
|
|
if (result === null) {
|
|
elem.classList.remove('text-bg-success')
|
|
elem.classList.add('text-bg-warning')
|
|
elem.innerText = `${trigram} 🎲 ??`
|
|
} else {
|
|
elem.classList.remove('text-bg-warning')
|
|
elem.classList.add('text-bg-success')
|
|
elem.innerText = `${trigram} 🎲 ${result}`
|
|
}
|
|
|
|
let nextTeam = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`).getAttribute("data-team")
|
|
if (nextTeam) {
|
|
// If there is one team that does not have launched its dice, then we update the debug section
|
|
let debugSpan = document.getElementById(`debug-dice-${tid}-team`)
|
|
if (debugSpan)
|
|
debugSpan.innerText = nextTeam
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display or hide the dice button.
|
|
* @param tid The tournament id
|
|
* @param visible The visibility status
|
|
*/
|
|
function updateDiceVisibility(tid, visible) {
|
|
let div = document.getElementById(`launch-dice-${tid}`)
|
|
let div_debug = document.getElementById(`debug-dice-form-${tid}`)
|
|
if (visible) {
|
|
div.classList.remove('d-none')
|
|
div_debug.classList.remove('d-none')
|
|
}
|
|
else {
|
|
div.classList.add('d-none')
|
|
div_debug.classList.add('d-none')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display or hide the box button.
|
|
* @param tid The tournament id
|
|
* @param visible The visibility status
|
|
*/
|
|
function updateBoxVisibility(tid, visible) {
|
|
let div = document.getElementById(`draw-problem-${tid}`)
|
|
let div_debug = document.getElementById(`debug-problem-form-${tid}`)
|
|
if (visible) {
|
|
div.classList.remove('d-none')
|
|
div_debug.classList.remove('d-none')
|
|
}
|
|
else {
|
|
div.classList.add('d-none')
|
|
div_debug.classList.add('d-none')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display or hide the accept and reject buttons.
|
|
* @param tid The tournament id
|
|
* @param visible The visibility status
|
|
*/
|
|
function updateButtonsVisibility(tid, visible) {
|
|
let div = document.getElementById(`buttons-${tid}`)
|
|
if (visible)
|
|
div.classList.remove('d-none')
|
|
else
|
|
div.classList.add('d-none')
|
|
}
|
|
|
|
/**
|
|
* Display or hide the export button.
|
|
* @param tid The tournament id
|
|
* @param visible The visibility status
|
|
*/
|
|
function updateExportVisibility(tid, visible) {
|
|
let div = document.getElementById(`export-${tid}`)
|
|
if (visible)
|
|
div.classList.remove('d-none')
|
|
else
|
|
div.classList.add('d-none')
|
|
}
|
|
|
|
/**
|
|
* Display or hide the continuation button.
|
|
* @param tid The tournament id
|
|
* @param visible The visibility status
|
|
*/
|
|
function updateContinueVisibility(tid, visible) {
|
|
let div = document.getElementById(`continue-${tid}`)
|
|
if (div !== null) {
|
|
// Only present during the final
|
|
if (visible)
|
|
div.classList.remove('d-none')
|
|
else
|
|
div.classList.add('d-none')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the different pools for the given round, and update the interface.
|
|
* @param tid The tournament id
|
|
* @param round The round number, as integer (1 or 2, or 3 for ETEAM)
|
|
* @param poules The list of poules, which are represented with their letters and trigrams,
|
|
* [{'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}]
|
|
*/
|
|
function updatePoules(tid, round, poules) {
|
|
let roundList = document.getElementById(`recap-${tid}-round-list`)
|
|
let poolListId = `recap-${tid}-round-${round}-pool-list`
|
|
let poolList = document.getElementById(poolListId)
|
|
if (poolList === null) {
|
|
// Add a div for the round in the recap div
|
|
let div = document.createElement('div')
|
|
div.id = `recap-${tid}-round-${round}`
|
|
div.classList.add('col-md-6', 'px-3', 'py-3')
|
|
div.setAttribute('data-tournament', tid)
|
|
|
|
let title = document.createElement('strong')
|
|
title.textContent = 'Tour ' + round
|
|
|
|
poolList = document.createElement('ul')
|
|
poolList.id = poolListId
|
|
poolList.classList.add('list-group', 'list-group-flush')
|
|
|
|
div.append(title, poolList)
|
|
roundList.append(div)
|
|
}
|
|
|
|
let c = 1
|
|
|
|
for (let poule of poules) {
|
|
let teamListId = `recap-${tid}-round-${round}-pool-${poule.letter}-team-list`
|
|
let teamList = document.getElementById(teamListId)
|
|
if (teamList === null) {
|
|
// Add a div for the pool in the recap div
|
|
let li = document.createElement('li')
|
|
li.id = `recap-${tid}-round-${round}-pool-${poule.letter}`
|
|
li.classList.add('list-group-item', 'px-3', 'py-3')
|
|
li.setAttribute('data-tournament', tid)
|
|
|
|
let title = document.createElement('strong')
|
|
title.textContent = 'Poule ' + poule.letter + round
|
|
|
|
teamList = document.createElement('ul')
|
|
teamList.id = teamListId
|
|
teamList.classList.add('list-group', 'list-group-flush')
|
|
|
|
li.append(title, teamList)
|
|
poolList.append(li)
|
|
}
|
|
teamList.innerHTML = ""
|
|
|
|
for (let team of poule.teams) {
|
|
// Reorder dices
|
|
let diceDiv = document.getElementById(`dice-${tid}-${team}`)
|
|
diceDiv.parentElement.style.order = c.toString()
|
|
c += 1
|
|
|
|
let teamLiId = `recap-${tid}-round-${round}-team-${team}`
|
|
|
|
// Add a line for the team in the recap
|
|
let teamLi = document.createElement('li')
|
|
teamLi.id = teamLiId
|
|
teamLi.classList.add('list-group-item')
|
|
teamLi.setAttribute('data-tournament', tid)
|
|
|
|
teamList.append(teamLi)
|
|
|
|
// Add the accepted problem div (empty for now)
|
|
let acceptedDivId = `recap-${tid}-round-${round}-team-${team}-accepted`
|
|
let acceptedDiv = document.getElementById(acceptedDivId)
|
|
if (acceptedDiv === null) {
|
|
acceptedDiv = document.createElement('div')
|
|
acceptedDiv.id = acceptedDivId
|
|
acceptedDiv.classList.add('badge', 'rounded-pill', 'text-bg-warning')
|
|
acceptedDiv.textContent = `${team} 📃 ?`
|
|
teamLi.append(acceptedDiv)
|
|
}
|
|
|
|
// Add the rejected problems div (empty for now)
|
|
let rejectedDivId = `recap-${tid}-round-${round}-team-${team}-rejected`
|
|
let rejectedDiv = document.getElementById(rejectedDivId)
|
|
if (rejectedDiv === null) {
|
|
rejectedDiv = document.createElement('div')
|
|
rejectedDiv.id = rejectedDivId
|
|
rejectedDiv.classList.add('badge', 'rounded-pill', 'text-bg-danger')
|
|
rejectedDiv.textContent = '🗑️'
|
|
teamLi.append(rejectedDiv)
|
|
}
|
|
}
|
|
|
|
// Draw tables
|
|
let tablesDiv = document.getElementById(`tables-${tid}`)
|
|
let tablesRoundDiv = document.getElementById(`tables-${tid}-round-${round}`)
|
|
if (tablesRoundDiv === null) {
|
|
// Add the tables div for the current round if necessary
|
|
let card = document.createElement('div')
|
|
card.classList.add('card', 'col-md-6')
|
|
tablesDiv.append(card)
|
|
|
|
let cardHeader = document.createElement('div')
|
|
cardHeader.classList.add('card-header')
|
|
cardHeader.innerHTML = `<h2>Tour ${round}</h2>`
|
|
card.append(cardHeader)
|
|
|
|
tablesRoundDiv = document.createElement('div')
|
|
tablesRoundDiv.id = `tables-${tid}-round-${round}`
|
|
tablesRoundDiv.classList.add('card-body', 'd-flex', 'flex-wrap')
|
|
card.append(tablesRoundDiv)
|
|
}
|
|
|
|
for (let poule of poules) {
|
|
if (poule.teams.length === 0)
|
|
continue
|
|
|
|
// Display the table for the pool
|
|
updatePouleTable(tid, round, poule)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the table for the given round and the given pool, where there will be the chosen problems.
|
|
* @param tid The tournament id
|
|
* @param round The round number, as integer (1 or 2, or 3 for ETEAM)
|
|
* @param poule The current pool, which id represented with its letter and trigrams,
|
|
* {'letter': 'A', 'teams': ['ABC', 'DEF', 'GHI']}
|
|
*/
|
|
function updatePouleTable(tid, round, poule) {
|
|
let tablesRoundDiv = document.getElementById(`tables-${tid}-round-${round}`)
|
|
let pouleTable = document.getElementById(`table-${tid}-${round}-${poule.letter}`)
|
|
if (pouleTable === null) {
|
|
// Create table
|
|
let card = document.createElement('div')
|
|
card.classList.add('card', 'w-100', 'my-3', `order-${poule.letter.charCodeAt(0) - 64}`)
|
|
tablesRoundDiv.append(card)
|
|
|
|
let cardHeader = document.createElement('div')
|
|
cardHeader.classList.add('card-header')
|
|
cardHeader.innerHTML = `<h2>Poule ${poule.letter}${round}</h2>`
|
|
card.append(cardHeader)
|
|
|
|
let cardBody = document.createElement('div')
|
|
cardBody.classList.add('card-body')
|
|
card.append(cardBody)
|
|
|
|
pouleTable = document.createElement('table')
|
|
pouleTable.id = `table-${tid}-${round}-${poule.letter}`
|
|
pouleTable.classList.add('table', 'table-stripped')
|
|
cardBody.append(pouleTable)
|
|
|
|
let thead = document.createElement('thead')
|
|
pouleTable.append(thead)
|
|
|
|
let phaseTr = document.createElement('tr')
|
|
thead.append(phaseTr)
|
|
|
|
let teamTh = document.createElement('th')
|
|
teamTh.classList.add('text-center')
|
|
teamTh.rowSpan = poule.teams.length === 5 ? 3 : 2
|
|
teamTh.textContent = "Équipe"
|
|
phaseTr.append(teamTh)
|
|
|
|
// Add columns
|
|
for (let i = 1; i <= (poule.teams.length === 4 ? 4 : 3); ++i) {
|
|
let phaseTh = document.createElement('th')
|
|
phaseTh.classList.add('text-center')
|
|
if (poule.teams.length === 5 && i < 3)
|
|
phaseTh.colSpan = 2
|
|
phaseTh.textContent = `Phase ${i}`
|
|
phaseTr.append(phaseTh)
|
|
}
|
|
|
|
if (poule.teams.length === 5) {
|
|
let roomTr = document.createElement('tr')
|
|
thead.append(roomTr)
|
|
|
|
for (let i = 0; i < 5; ++i) {
|
|
let roomTh = document.createElement('th')
|
|
roomTh.classList.add('text-center')
|
|
roomTh.textContent = `Salle ${1 + (i % 2)}`
|
|
roomTr.append(roomTh)
|
|
}
|
|
}
|
|
|
|
let problemTr = document.createElement('tr')
|
|
thead.append(problemTr)
|
|
|
|
for (let team of poule.teams) {
|
|
let problemTh = document.createElement('th')
|
|
problemTh.classList.add('text-center')
|
|
// Problem is unknown for now
|
|
problemTh.innerHTML = `Pb. <span id="table-${tid}-round-${round}-problem-${team}">?</span>`
|
|
problemTr.append(problemTh)
|
|
}
|
|
|
|
// Add body
|
|
let tbody = document.createElement('tbody')
|
|
pouleTable.append(tbody)
|
|
|
|
for (let i = 0; i < poule.teams.length; ++i) {
|
|
let team = poule.teams[i]
|
|
|
|
let teamTr = document.createElement('tr')
|
|
tbody.append(teamTr)
|
|
|
|
// First create cells, then we will add them in the table
|
|
let teamTd = document.createElement('td')
|
|
teamTd.classList.add('text-center')
|
|
teamTd.innerText = team
|
|
teamTr.append(teamTd)
|
|
|
|
let defenderTd = document.createElement('td')
|
|
defenderTd.classList.add('text-center')
|
|
defenderTd.innerText = 'Déf'
|
|
|
|
let opponentTd = document.createElement('td')
|
|
opponentTd.classList.add('text-center')
|
|
opponentTd.innerText = 'Opp'
|
|
|
|
let reviewerTd = document.createElement('td')
|
|
reviewerTd.classList.add('text-center')
|
|
reviewerTd.innerText = 'Rap'
|
|
|
|
// Put the cells in their right places, according to the pool size and the row number.
|
|
if (poule.teams.length === 3) {
|
|
switch (i) {
|
|
case 0:
|
|
teamTr.append(defenderTd, reviewerTd, opponentTd)
|
|
break
|
|
case 1:
|
|
teamTr.append(opponentTd, defenderTd, reviewerTd)
|
|
break
|
|
case 2:
|
|
teamTr.append(reviewerTd, opponentTd, defenderTd)
|
|
break
|
|
}
|
|
} else if (poule.teams.length === 4) {
|
|
let emptyTd = document.createElement('td')
|
|
switch (i) {
|
|
case 0:
|
|
teamTr.append(defenderTd, emptyTd, reviewerTd, opponentTd)
|
|
break
|
|
case 1:
|
|
teamTr.append(opponentTd, defenderTd, emptyTd, reviewerTd)
|
|
break
|
|
case 2:
|
|
teamTr.append(reviewerTd, opponentTd, defenderTd, emptyTd)
|
|
break
|
|
case 3:
|
|
teamTr.append(emptyTd, reviewerTd, opponentTd, defenderTd)
|
|
break
|
|
}
|
|
} else if (poule.teams.length === 5) {
|
|
let emptyTd = document.createElement('td')
|
|
let emptyTd2 = document.createElement('td')
|
|
switch (i) {
|
|
case 0:
|
|
teamTr.append(defenderTd, emptyTd, opponentTd, reviewerTd, emptyTd2)
|
|
break
|
|
case 1:
|
|
teamTr.append(emptyTd, defenderTd, reviewerTd, emptyTd2, opponentTd)
|
|
break
|
|
case 2:
|
|
teamTr.append(opponentTd, emptyTd, defenderTd, emptyTd2, reviewerTd)
|
|
break
|
|
case 3:
|
|
teamTr.append(reviewerTd, opponentTd, emptyTd, defenderTd, emptyTd2)
|
|
break
|
|
case 4:
|
|
teamTr.append(emptyTd, reviewerTd, emptyTd2, opponentTd, defenderTd)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Highlight the team that is currently choosing its problem.
|
|
* @param tid The tournament id
|
|
* @param round The current round number, as integer (1 or 2, or 3 for ETEAM)
|
|
* @param pool The current pool letter (A, B, C or D) (null if non-relevant)
|
|
* @param team The current team trigram (null if non-relevant)
|
|
*/
|
|
function updateActiveRecap(tid, round, pool, team) {
|
|
// Remove the previous highlights
|
|
document.querySelectorAll(`div.text-bg-secondary[data-tournament="${tid}"]`)
|
|
.forEach(elem => elem.classList.remove('text-bg-secondary'))
|
|
document.querySelectorAll(`li.list-group-item-success[data-tournament="${tid}"]`)
|
|
.forEach(elem => elem.classList.remove('list-group-item-success'))
|
|
document.querySelectorAll(`li.list-group-item-info[data-tournament="${tid}"]`)
|
|
.forEach(elem => elem.classList.remove('list-group-item-info'))
|
|
|
|
// Highlight current round, if existing
|
|
let roundDiv = document.getElementById(`recap-${tid}-round-${round}`)
|
|
if (roundDiv !== null)
|
|
roundDiv.classList.add('text-bg-secondary')
|
|
|
|
// Highlight current pool, if existing
|
|
let poolLi = document.getElementById(`recap-${tid}-round-${round}-pool-${pool}`)
|
|
if (poolLi !== null)
|
|
poolLi.classList.add('list-group-item-success')
|
|
|
|
// Highlight current team, if existing
|
|
let teamLi = document.getElementById(`recap-${tid}-round-${round}-team-${team}`)
|
|
if (teamLi !== null)
|
|
teamLi.classList.add('list-group-item-info')
|
|
|
|
let debugSpan = document.getElementById(`debug-problem-${tid}-team`)
|
|
if (debugSpan && team) {
|
|
debugSpan.innerText = team
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the recap and the table when a team accepts a problem.
|
|
* @param tid The tournament id
|
|
* @param round The current round, as integer (1 or 2, or 3 for ETEAM)
|
|
* @param team The current team trigram
|
|
* @param problem The accepted problem, as integer
|
|
*/
|
|
function setProblemAccepted(tid, round, team, problem) {
|
|
// Update recap
|
|
let recapDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-accepted`)
|
|
if (problem !== null) {
|
|
recapDiv.classList.remove('text-bg-warning')
|
|
recapDiv.classList.add('text-bg-success')
|
|
} else {
|
|
recapDiv.classList.add('text-bg-warning')
|
|
recapDiv.classList.remove('text-bg-success')
|
|
}
|
|
recapDiv.textContent = `${team} 📃 ${problem ? problem : '?'}`
|
|
|
|
// Update table
|
|
let tableSpan = document.getElementById(`table-${tid}-round-${round}-problem-${team}`)
|
|
tableSpan.textContent = problem ? problem : '?'
|
|
}
|
|
|
|
/**
|
|
* Update the recap when a team rejects a problem.
|
|
* @param tid The tournament id
|
|
* @param round The current round, as integer (1 or 2, or 3 for ETEAM)
|
|
* @param team The current team trigram
|
|
* @param rejected The full list of rejected problems
|
|
*/
|
|
function setProblemRejected(tid, round, team, rejected) {
|
|
// Update recap
|
|
let recapDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-rejected`)
|
|
recapDiv.textContent = `🗑️ ${rejected.join(', ')}`
|
|
|
|
let penaltyDiv = document.getElementById(`recap-${tid}-round-${round}-team-${team}-penalty`)
|
|
if (rejected.length > problems_count - RECOMMENDED_SOLUTIONS_COUNT) {
|
|
// If more than P - 5 problems were rejected, add a penalty of 25% of the coefficient of the oral defender
|
|
// This is P - 6 for the ETEAM
|
|
if (penaltyDiv === null) {
|
|
penaltyDiv = document.createElement('div')
|
|
penaltyDiv.id = `recap-${tid}-round-${round}-team-${team}-penalty`
|
|
penaltyDiv.classList.add('badge', 'rounded-pill', 'text-bg-info')
|
|
recapDiv.parentNode.append(penaltyDiv)
|
|
}
|
|
penaltyDiv.textContent = `❌ ${25 * (rejected.length - (problems_count - RECOMMENDED_SOLUTIONS_COUNT))} %`
|
|
} else {
|
|
// Eventually remove this div
|
|
if (penaltyDiv !== null)
|
|
penaltyDiv.remove()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For a 5-teams pool, we may reorder the pool if two teams select the same problem.
|
|
* Then, we redraw the table and set the accepted problems.
|
|
* @param tid The tournament id
|
|
* @param round The current round, as integer (1 or 2, or 3 for ETEAM)
|
|
* @param poule The pool represented by its letter
|
|
* @param teams The teams list represented by their trigrams, ["ABC", "DEF", "GHI", "JKL", "MNO"]
|
|
* @param problems The accepted problems in the same order than the teams, [1, 1, 2, 2, 3]
|
|
*/
|
|
function reorderPoule(tid, round, poule, teams, problems) {
|
|
// Redraw the pool table
|
|
let table = document.getElementById(`table-${tid}-${round}-${poule}`)
|
|
table.parentElement.parentElement.remove()
|
|
|
|
updatePouleTable(tid, round, {'letter': poule, 'teams': teams})
|
|
|
|
// Put the problems in the table
|
|
for (let i = 0; i < teams.length; ++i) {
|
|
let team = teams[i]
|
|
let problem = problems[i]
|
|
|
|
setProblemAccepted(tid, round, team, problem)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process the received data from the server.
|
|
* @param tid The tournament id
|
|
* @param data The received message
|
|
*/
|
|
function processMessage(tid, data) {
|
|
switch (data.type) {
|
|
case 'alert':
|
|
// Add alert message
|
|
addMessage(data.message, data.alert_type)
|
|
break
|
|
case 'notification':
|
|
// Add notification
|
|
showNotification(data.title, data.body)
|
|
break
|
|
case 'set_info':
|
|
// Update information banner
|
|
setInfo(tid, data.information)
|
|
break
|
|
case 'draw_start':
|
|
// Start the draw and update the interface
|
|
drawStart(tid, data.trigrams)
|
|
break
|
|
case 'abort':
|
|
// Abort the current draw
|
|
drawAbort(tid)
|
|
break
|
|
case 'dice':
|
|
// Update the interface after a dice launch
|
|
updateDiceInfo(tid, data.team, data.result)
|
|
break
|
|
case 'dice_visibility':
|
|
// Update the dice button visibility
|
|
updateDiceVisibility(tid, data.visible)
|
|
break
|
|
case 'box_visibility':
|
|
// Update the box button visibility
|
|
updateBoxVisibility(tid, data.visible)
|
|
break
|
|
case 'buttons_visibility':
|
|
// Update the accept/reject buttons visibility
|
|
updateButtonsVisibility(tid, data.visible)
|
|
break
|
|
case 'export_visibility':
|
|
// Update the export button visibility
|
|
updateExportVisibility(tid, data.visible)
|
|
break
|
|
case 'continue_visibility':
|
|
// Update the continue button visibility for the final tournament
|
|
updateContinueVisibility(tid, data.visible)
|
|
break
|
|
case 'set_poules':
|
|
// Set teams order and pools and update the interface
|
|
updatePoules(tid, data.round, data.poules)
|
|
break
|
|
case 'set_active':
|
|
// Highlight the team that is selecting a problem
|
|
updateActiveRecap(tid, data.round, data.poule, data.team)
|
|
break
|
|
case 'set_problem':
|
|
// Mark a problem as accepted and update the interface
|
|
setProblemAccepted(tid, data.round, data.team, data.problem)
|
|
break
|
|
case 'reject_problem':
|
|
// Mark a problem as rejected and update the interface
|
|
setProblemRejected(tid, data.round, data.team, data.rejected)
|
|
break
|
|
case 'reorder_poule':
|
|
// Reorder a pool and redraw the associated table
|
|
reorderPoule(tid, data.round, data.poule, data.teams, data.problems)
|
|
break
|
|
}
|
|
}
|
|
|
|
function setupSocket(nextDelay = 1000) {
|
|
// Open a global websocket
|
|
socket = new WebSocket(
|
|
(document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/ws/draw/'
|
|
)
|
|
|
|
// Listen on websockets and process messages from the server
|
|
socket.addEventListener('message', e => {
|
|
// Parse received data as JSON
|
|
const data = JSON.parse(e.data)
|
|
|
|
processMessage(data['tid'], data)
|
|
})
|
|
|
|
// Manage errors
|
|
socket.addEventListener('close', e => {
|
|
console.error('Chat socket closed unexpectedly, restarting…')
|
|
setTimeout(() => setupSocket(2 * nextDelay), nextDelay)
|
|
})
|
|
|
|
// When the socket is opened, set the language in order to receive alerts in the good language
|
|
socket.addEventListener('open', e => {
|
|
socket.send(JSON.stringify({
|
|
'tid': tournaments[0].id,
|
|
'type': 'set_language',
|
|
'language': document.getElementsByName('language')[0].value,
|
|
}))
|
|
})
|
|
|
|
for (let tournament of tournaments) {
|
|
// Manage the start form
|
|
let format_form = document.getElementById('format-form-' + tournament.id)
|
|
if (format_form !== null) {
|
|
format_form.addEventListener('submit', function (e) {
|
|
e.preventDefault()
|
|
|
|
socket.send(JSON.stringify({
|
|
'tid': tournament.id,
|
|
'type': 'start_draw',
|
|
'fmt': document.getElementById('format-' + tournament.id).value
|
|
}))
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
setupSocket()
|
|
|
|
if (document.querySelector('a[href="/admin/"]')) {
|
|
// Administrators can fake the draw
|
|
// This is useful for debug purposes, or
|
|
document.getElementsByTagName('body')[0].addEventListener('keyup', event => {
|
|
if (event.key === 'f') {
|
|
let activeTab = document.querySelector('#tournaments-tab button.active')
|
|
let tid = activeTab.id.substring(4)
|
|
|
|
let dice = document.getElementById(`launch-dice-${tid}`)
|
|
let box = document.getElementById(`draw-problem-${tid}`)
|
|
let value = NaN
|
|
if (!dice.classList.contains('d-none')) {
|
|
value = parseInt(prompt("Entrez la valeur du dé (laissez vide pour annuler) :"))
|
|
if (!isNaN(value) && 1 <= value && value <= 100)
|
|
drawDice(tid, null, value)
|
|
|
|
} else if (!box.classList.contains('d-none')) {
|
|
value = parseInt(prompt("Entrez le numéro du problème à choisir (laissez vide pour annuler) :"))
|
|
if (!isNaN(value) && 1 <= value && value <= 8)
|
|
drawProblem(tid, value)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|