diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt index 134862d..a0f0dbe 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt @@ -6,12 +6,12 @@ import org.jeudego.pairgoth.model.Game import org.jeudego.pairgoth.model.MacMahon import org.jeudego.pairgoth.model.Pairable import org.jeudego.pairgoth.model.PairingType +import org.jeudego.pairgoth.model.Player import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.historyBefore import org.jeudego.pairgoth.pairing.HistoryHelper import org.jeudego.pairgoth.pairing.solver.MacMahonSolver -import kotlin.math.ceil import kotlin.math.floor import kotlin.math.max import kotlin.math.min @@ -129,20 +129,23 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f return sortedPairables } -fun Tournament<*>.populateFrozenStandings(sortedPairables: List, round: Int = rounds) { +fun Tournament<*>.populateStandings(sortedPairables: List, round: Int = rounds) { val sortedMap = sortedPairables.associateBy { it.getID()!! } // refresh name, firstname, club and level - sortedMap.forEach { (id, player) -> - val mutable = player as Json.MutableObject - val live = players[id]!! - mutable["name"] = live.name - mutable["firstname"] = live.firstname - mutable["club"] = live.club - mutable["rating"] = live.rating - mutable["rank"] = live.rank + sortedMap.forEach { (id, pairable) -> + val mutable = pairable as Json.MutableObject + pairables[id]?.let { + mutable["name"] = it.name + if (it is Player) { + mutable["firstname"] = it.firstname + } + mutable["club"] = it.club + mutable["rating"] = it.rating + mutable["rank"] = it.rank + } } // fill result diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt index 1fa0ae4..c4d080c 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt @@ -2,8 +2,13 @@ package org.jeudego.pairgoth.api import com.republicate.kson.Json import com.republicate.kson.toJsonArray +import com.republicate.kson.toMutableJsonArray import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.model.Game +import org.jeudego.pairgoth.model.ID +import org.jeudego.pairgoth.model.Player +import org.jeudego.pairgoth.model.TeamTournament +import org.jeudego.pairgoth.model.TeamTournament.Team import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.toID @@ -21,8 +26,8 @@ object PairingHandler: PairgothApiHandler { val playing = tournament.games(round).values.flatMap { listOf(it.black, it.white) }.toSet() - val unpairables = tournament.pairables.values.filter { it.final && it.skip.contains(round) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() - val pairables = tournament.pairables.values.filter { it.final && !it.skip.contains(round) && !playing.contains(it.id) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() + val unpairables = tournament.pairables.values.filter { it.final && !it.canPlay(round) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() + val pairables = tournament.pairables.values.filter { it.final && it.canPlay(round) && !playing.contains(it.id) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() val games = tournament.games(round).values.sortedBy { if (it.table == 0) Int.MAX_VALUE else it.table } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt index d5b4fb3..e978fd1 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt @@ -22,7 +22,6 @@ import java.nio.charset.StandardCharsets import java.text.DecimalFormat import java.text.Normalizer import java.util.* -import kotlin.collections.ArrayList object StandingsHandler: PairgothApiHandler { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { @@ -31,7 +30,7 @@ object StandingsHandler: PairgothApiHandler { val includePreliminary = request.getParameter("include_preliminary")?.let { it.toBoolean() } ?: false val sortedPairables = tournament.getSortedPairables(round, includePreliminary) - tournament.populateFrozenStandings(sortedPairables, round) + tournament.populateStandings(sortedPairables, round) val acceptHeader = request.getHeader("Accept") as String? val accept = acceptHeader?.substringBefore(";") diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt index 4f23fc9..94676be 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt @@ -26,6 +26,8 @@ sealed class Pairable(val id: ID, val name: String, val rating: Int, val rank: I fun equals(other: Pairable): Boolean { return id == other.id } + + open fun canPlay(round: Int) = !skip.contains(round) } object ByePlayer: Pairable(0, "bye", 0, Int.MIN_VALUE, true) { diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt index ab740a1..c92a7ff 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -255,10 +255,14 @@ class TeamTournament( json["rank"] = rank country?.also { json["country"] = it } club?.also { json["club"] = it } + json["names"] = playerIds.mapNotNull { players[it]?.fullName() }.toJsonArray() + json["ranks"] = playerIds.mapNotNull { players[it]?.rank }.toJsonArray() } val teamOfIndividuals: Boolean get() = type.individual - override val skip get() = playerIds.map { players[it]!!.skip }.reduce { left, right -> (left union right) as MutableSet } + // override val skip get() = playerIds.map { players[it]!!.skip }.reduce { left, right -> (left union right) as MutableSet } + + override fun canPlay(round: Int) = teamPlayers.filter { it.canPlay(round) }.size == type.playersNumber } fun teamFromJson(json: Json.Object, default: TeamTournament.Team? = null): Team { @@ -288,7 +292,8 @@ class TeamTournament( fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = null): Tournament<*> { val type = json.getString("type")?.uppercase()?.let { Tournament.Type.valueOf(it) } ?: default?.type ?: badRequest("missing type") // No clean way to avoid this redundancy - val tournament = if (type.playersNumber == 1) + val tournament = + if (type.playersNumber == 1) StandardTournament( id = json.getInt("id") ?: default?.id ?: nextTournamentId, type = type, @@ -301,7 +306,7 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n location = json.getString("location") ?: default?.location ?: badRequest("missing location"), online = json.getBoolean("online") ?: default?.online ?: false, komi = json.getDouble("komi") ?: default?.komi ?: 7.5, - rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: default?.rules ?: Rules.FRENCH, + rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: default?.rules ?: if (json.getString("country")?.lowercase(Locale.ROOT) == "fr") Rules.FRENCH else Rules.AGA, gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19, timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"), rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"), @@ -317,7 +322,7 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n startDate = json.getString("startDate")?.let { LocalDate.parse(it) } ?: default?.startDate ?: badRequest("missing startDate"), endDate = json.getString("endDate")?.let { LocalDate.parse(it) } ?: default?.endDate ?: badRequest("missing endDate"), director = json.getString("director") ?: default?.director ?: "", - country = json.getString("country") ?: default?.country ?: badRequest("missing country"), + country = (json.getString("country") ?: default?.country ?: "fr").let { if (it.isEmpty()) "fr" else it }, location = json.getString("location") ?: default?.location ?: badRequest("missing location"), online = json.getBoolean("online") ?: default?.online ?: false, komi = json.getDouble("komi") ?: default?.komi ?: 7.5, diff --git a/view-webapp/src/main/sass/tour.scss b/view-webapp/src/main/sass/tour.scss index 1dfc172..031c53c 100644 --- a/view-webapp/src/main/sass/tour.scss +++ b/view-webapp/src/main/sass/tour.scss @@ -284,6 +284,21 @@ max-width: max(50vw, 20em); } + #composition.multi-select .listitem { + cursor: default; + i.icon { + cursor: pointer; + @media (hover: hover) { + display: none; + } + } + &:hover { + i.icon { + display: inline; + } + } + } + /* pairing section */ #pairing-content { @@ -345,7 +360,7 @@ } } } - #pairables { + #pairables, #teams { margin-bottom: 1em; } #paired { @@ -379,7 +394,7 @@ gap: 1em; max-width: max(10em, 20vw); } - #unpairables, #previous_games { + #unpairables, #previous_games, #composition { display: flex; flex-flow: column nowrap; min-height: 10vh; diff --git a/view-webapp/src/main/webapp/WEB-INF/translations/de b/view-webapp/src/main/webapp/WEB-INF/translations/de index caacc8c..ddb6320 100644 --- a/view-webapp/src/main/webapp/WEB-INF/translations/de +++ b/view-webapp/src/main/webapp/WEB-INF/translations/de @@ -124,6 +124,7 @@ Surround winner's name or ½-½ Namen des Gewinners oder 0,5 - 0,5 umrahmen Signature: Unterschrift: Swiss Schweizer System Table Tisch +Teams Teams Team of 2 individual players Team aus 2 Einzelspielern Team of 3 individual players Team aus 3 Einzelspielern Team of 4 individual players Team aus 4 Einzelspielern diff --git a/view-webapp/src/main/webapp/WEB-INF/translations/fr b/view-webapp/src/main/webapp/WEB-INF/translations/fr index a4cf627..4a2aa5a 100644 --- a/view-webapp/src/main/webapp/WEB-INF/translations/fr +++ b/view-webapp/src/main/webapp/WEB-INF/translations/fr @@ -120,6 +120,7 @@ Standings after round Classement après la ronde Stay in the browser Rester dans le navigateur  Sudden death Mort subite Swiss Suisse +Teams Équipes Team of 2 individual players Équipe de 2 joueurs individuels Team of 3 individual players Équipe de 3 joueurs individuels Team of 4 individual players Équipe de 4 joueurs individuels diff --git a/view-webapp/src/main/webapp/WEB-INF/translations/kr b/view-webapp/src/main/webapp/WEB-INF/translations/kr index fb975bc..20c0c5a 100644 --- a/view-webapp/src/main/webapp/WEB-INF/translations/kr +++ b/view-webapp/src/main/webapp/WEB-INF/translations/kr @@ -124,6 +124,7 @@ Sudden death 서든 데스 Surround winner's name or ½-½ 승자의 이름 또는 0.5 - 0.5을 둘러싸기 Signature: 서명: Swiss 스위스 +Teams 팀 Team of 2 individual players 2인 팀 Team of 3 individual players 3인 팀 Team of 4 individual players 4인 팀 diff --git a/view-webapp/src/main/webapp/js/tour-pairing.inc.js b/view-webapp/src/main/webapp/js/tour-pairing.inc.js index 641d559..324bd87 100644 --- a/view-webapp/src/main/webapp/js/tour-pairing.inc.js +++ b/view-webapp/src/main/webapp/js/tour-pairing.inc.js @@ -135,6 +135,8 @@ function hideOpponents() { onLoad(()=>{ // note - this handler is also in use for lists on Mac Mahon super groups and teams pages + // CB TODO - there is some code cleaning to to around the listitems reuse and events: + // the on('click') method should not define specific behaviors for this page, just dispatch custom events $('.listitem').on('click', e => { let listitem = e.target.closest('.listitem'); let box = e.target.closest('.multi-select'); @@ -156,17 +158,23 @@ onLoad(()=>{ if (e.detail === 1) { // single click focused = listitem.toggleClass('selected').attr('draggable', listitem.hasClass('selected')); - if (box.getAttribute('id') === 'pairables') showOpponents(focused) - } else if (listitem.closest('#pairing-lists')) { - // on pairing page - if (listitem.closest('#paired')) { - // double click - hideOpponents() - focused = listitem.attr('draggable', listitem.hasClass('selected')); - editGame(focused); - } else if (listitem.closest('#pairables')) { - editPairable(focused); + if (box.getAttribute('id') === 'pairables') { + if (focused.hasClass('selected')) showOpponents(focused); + else hideOpponents(); } + } else { + if (listitem.closest('#pairing-lists')) { + // on pairing page + if (listitem.closest('#paired')) { + // double click + hideOpponents() + focused = listitem.attr('draggable', listitem.hasClass('selected')); + editGame(focused); + } else if (listitem.closest('#pairables')) { + editPairable(focused); + } + } + box.dispatchEvent(new CustomEvent('listitem-dblclk', { 'detail': parseInt(listitem.data('id')) })); } } box.dispatchEvent(new CustomEvent('listitems')); diff --git a/view-webapp/src/main/webapp/js/tour-registration.inc.js b/view-webapp/src/main/webapp/js/tour-registration.inc.js index 5379c90..8b4133e 100644 --- a/view-webapp/src/main/webapp/js/tour-registration.inc.js +++ b/view-webapp/src/main/webapp/js/tour-registration.inc.js @@ -105,6 +105,11 @@ function parseRank(rank) { return ''; } +function displayRank(rank) { + rank = parseInt(rank); + return rank < 0 ? `${-rank}k` : `${rank + 1}d`; +} + function fillPlayer(player) { // hack UK / GB let country = player.country.toLowerCase(); diff --git a/view-webapp/src/main/webapp/js/tour-teams.inc.js b/view-webapp/src/main/webapp/js/tour-teams.inc.js index 96d3fa2..d21fa28 100644 --- a/view-webapp/src/main/webapp/js/tour-teams.inc.js +++ b/view-webapp/src/main/webapp/js/tour-teams.inc.js @@ -24,6 +24,44 @@ function split(teams) { }); } +function join(players, team) { + console.log(team) + console.log(teams.get(team)) + api.putJson(`tour/${tour_id}/team/${team}`, { + "players": teams.get(team).players.concat(players) + }).then(rst => { + if (rst !== 'error') { + document.location.reload(); + } + }); +} + +function leave(teamId, playerId) { + let team = teams.get(teamId); + let index = team.players.indexOf(playerId); + if (index > -1) { + team.players.splice(index, 1); + api.putJson(`tour/${tour_id}/team/${teamId}`, { + "players": team.players + }).then(rst => { + if (rst !== 'error') { + document.location.reload(); + } + }); + } +} + +function showTeam(teamId) { + let team = teams.get(teamId); + $('#composition')[0].clearChildren(); + $('#composition').attr('title', team.name).removeClass('hidden'); + $('#composition').data('id', teamId); + for (i = 0; i < team.players.length; ++i) { + let listitem = `
${team.names[i]}${displayRank(team.ranks[i])} 
` + $('#composition')[0].insertAdjacentHTML('beforeend', listitem); + } +} + onLoad(() => { $('#teamup').on('click', e => { let rows = $('#teamables .selected.listitem') @@ -39,14 +77,65 @@ onLoad(() => { let teams = rows.map(item => parseInt(item.data("id"))); if (teams.length !== 0) split(teams); }); - $('#teamables').on('listitems', () => { + $('#join').on('click', e => { let rows = $('#teamables .selected.listitem'); - if (rows.length === teamSize) { - $('#team-name')[0].value = rows.map(row => row.data('name')).join('-'); - $('#teamup').removeClass('disabled'); - } else { - $('#team-name')[0].value = ''; - $('#teamup').addClass('disabled'); + let players = rows.map(item => parseInt(item.data("id"))); + let teams = $('#teams .selected.listitem'); + if (players.length !== 0 && teams.length === 1) { + join(players, parseInt(teams[0].data("id"))); } }); -}); \ No newline at end of file + $('#team-name').on('input', () => { + if ($('#team-name')[0].value === '') { + $('#teamup').addClass('disabled'); + } else if ($('#teamables .selected.listitem').length > 0) { + $('#teamup').removeClass('disabled'); + } + $('#team-name').data('manual', true); + }); + $('#teamables, #teams').on('listitems', () => { + let players = $('#teamables .selected.listitem'); + let teams = $('#teams .selected.listitem'); + if (players.length === 0) { + $('#teamup').addClass('disabled'); + $('#join').addClass('disabled'); + if(!$('#team-name').data('manual')) { + $('#team-name')[0].value = ''; + } + } else { + if(!$('#team-name').data('manual')) { + $('#team-name')[0].value = players.map(row => row.data('name')).join('-'); + } + if ($('#team-name')[0].value !== '') { + $('#teamup').removeClass('disabled'); + } + if (teams.length === 1) { + $('#join').removeClass('disabled'); + } else { + $('#join').addClass('disabled'); + } + } + if (teams.length === 0) { + $('#split').addClass('disabled'); + $('#composition').addClass('hidden'); + } else { + $('#split').removeClass('disabled'); + if (focused && focused.closest('.multi-select').attr('id') === 'teams') { + showTeam(parseInt(focused.data('id'))); + } + } + }); + /* If we want double-click... + $('#teams').on('listitem-dblclk', e => { + console.log(teams.get(e.detail)); + }); + */ + $('#composition').on('click', e => { + console.log('click') + if (e.target.matches('.listitem i')) { + let team = parseInt(e.target.closest('.multi-select').data('id')); + let player = parseInt(e.target.closest('.listitem').data('id')); + leave(team, player); + } + }); +}); diff --git a/view-webapp/src/main/webapp/tour-information.inc.html b/view-webapp/src/main/webapp/tour-information.inc.html index 8f2c79a..ed64331 100644 --- a/view-webapp/src/main/webapp/tour-information.inc.html +++ b/view-webapp/src/main/webapp/tour-information.inc.html @@ -71,12 +71,10 @@ - #* TODO - *#
diff --git a/view-webapp/src/main/webapp/tour-registration.inc.html b/view-webapp/src/main/webapp/tour-registration.inc.html index b1c7c21..91125dc 100644 --- a/view-webapp/src/main/webapp/tour-registration.inc.html +++ b/view-webapp/src/main/webapp/tour-registration.inc.html @@ -1,7 +1,9 @@ #set($parts = $api.get("tour/${params.id}/part")) -#if($tour.type == 'INDIVIDUAL' || $tour.type.startsWith('TEAM')) +#if($tour.type == 'INDIVIDUAL') + ## Standard tournament #set($pmap = $utils.toMap($parts)) #else + ## Pairgo, rengo and teams of individuals #set($teams = $api.get("tour/${params.id}/team")) #set($pmap = $utils.toMap($teams)) #end diff --git a/view-webapp/src/main/webapp/tour-teams.inc.html b/view-webapp/src/main/webapp/tour-teams.inc.html index d848f74..b821cd5 100644 --- a/view-webapp/src/main/webapp/tour-teams.inc.html +++ b/view-webapp/src/main/webapp/tour-teams.inc.html @@ -18,17 +18,32 @@ Team up
- + -
+
+
#foreach($team in $teams) -
$team.name#rank($team.rank)#if($team.country) $team.country#end
+
$team.name#rank($team.rank)#if($team.country) $team.country#end
#end +
+
+ \ No newline at end of file