mirror of
				https://gitlab.com/animath/si/plateforme.git
				synced 2025-10-31 22:24:30 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			857 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			857 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| (async () => {
 | |
|     // check notification permission
 | |
|     // This is useful to alert people that they should do something
 | |
|     await Notification.requestPermission()
 | |
| })()
 | |
| 
 | |
| const TFJM = JSON.parse(document.getElementById('TFJM_settings').textContent)
 | |
| const RECOMMENDED_SOLUTIONS_COUNT = TFJM.RECOMMENDED_SOLUTIONS_COUNT
 | |
| 
 | |
| 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 nextTeamDiv = document.querySelector(` div[id="dices-${tid}"] > div > div[class*="text-bg-warning"]`)
 | |
|         if (nextTeamDiv) {
 | |
|             // If there is one team that does not have launched its dice, then we update the debug section
 | |
|             let nextTeam = nextTeamDiv.getAttribute("data-team")
 | |
|             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 reporterTd = document.createElement('td')
 | |
|                 reporterTd.classList.add('text-center')
 | |
|                 reporterTd.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(reporterTd, reviewerTd, opponentTd)
 | |
|                             break
 | |
|                         case 1:
 | |
|                             teamTr.append(opponentTd, reporterTd, reviewerTd)
 | |
|                             break
 | |
|                         case 2:
 | |
|                             teamTr.append(reviewerTd, opponentTd, reporterTd)
 | |
|                             break
 | |
|                     }
 | |
|                 } else if (poule.teams.length === 4) {
 | |
|                     let emptyTd = document.createElement('td')
 | |
|                     switch (i) {
 | |
|                         case 0:
 | |
|                             teamTr.append(reporterTd, emptyTd, reviewerTd, opponentTd)
 | |
|                             break
 | |
|                         case 1:
 | |
|                             teamTr.append(opponentTd, reporterTd, emptyTd, reviewerTd)
 | |
|                             break
 | |
|                         case 2:
 | |
|                             teamTr.append(reviewerTd, opponentTd, reporterTd, emptyTd)
 | |
|                             break
 | |
|                         case 3:
 | |
|                             teamTr.append(emptyTd, reviewerTd, opponentTd, reporterTd)
 | |
|                             break
 | |
|                     }
 | |
|                 } else if (poule.teams.length === 5) {
 | |
|                     let emptyTd = document.createElement('td')
 | |
|                     let emptyTd2 = document.createElement('td')
 | |
|                     switch (i) {
 | |
|                         case 0:
 | |
|                             teamTr.append(reporterTd, emptyTd, opponentTd, reviewerTd, emptyTd2)
 | |
|                             break
 | |
|                         case 1:
 | |
|                             teamTr.append(emptyTd, reporterTd, reviewerTd, emptyTd2, opponentTd)
 | |
|                             break
 | |
|                         case 2:
 | |
|                             teamTr.append(opponentTd, emptyTd, reporterTd, emptyTd2, reviewerTd)
 | |
|                             break
 | |
|                         case 3:
 | |
|                             teamTr.append(reviewerTd, opponentTd, emptyTd, reporterTd, emptyTd2)
 | |
|                             break
 | |
|                         case 4:
 | |
|                             teamTr.append(emptyTd, reviewerTd, emptyTd2, opponentTd, reporterTd)
 | |
|                             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 reporter
 | |
|             // 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)
 | |
| 
 | |
|             let recapTeam = document.getElementById(`recap-${tid}-round-${round}-team-${team}`)
 | |
|             recapTeam.style.order = i.toString()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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)
 | |
|                 }
 | |
|             }
 | |
|         })
 | |
|     }
 | |
| })
 |