From 9c6c96d0a6d5dfdc6f4d3703c3a57b353109e704 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 8 Feb 2024 00:45:41 +0100 Subject: [PATCH 001/224] Add the basis for an improved scoreboard for individual tournaments: - Add display type tournamentcourt - Save the timestamp of the call of a match in the DB - Send the timestamp of the call of a game to BUP --- bts/admin.js | 2 + bts/btp_sync.js | 53 +++++++++++++++++----- bts/http_api.js | 3 ++ fetch-btp.js | 2 +- static/js/change.js | 3 ++ static/js/ci18n_de.js | 9 ++++ static/js/ci18n_en.js | 9 ++++ static/js/ctournament.js | 96 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 166 insertions(+), 11 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 56876b9..a4de32e 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -55,6 +55,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'btp_ip', 'btp_password', 'is_team', 'is_nation_competition', 'only_now_on_court', + 'warmup', 'warmup_ready', 'warmup_start', 'ticker_enabled', 'ticker_url', 'ticker_password', 'language', 'dm_style', 'logo_background_color', 'logo_foreground_color']); @@ -175,6 +176,7 @@ function _extract_setup(msg_setup) { 'incomplete', 'scheduled_time_str', 'scheduled_date', + 'called_timestamp', 'teams', 'override_colors', ]); diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 625f99c..99b1b35 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -18,7 +18,7 @@ function date_str(dt) { return utils.pad(dt.year, 2, '0') + '-' + utils.pad(dt.month, 2, '0') + '-' + utils.pad(dt.day, 2, '0'); } -function craft_match(tkey, btp_id, court_map, event, draw, officials, bm, match_ids_on_court, match_types, is_league) { +function craft_match(app, tkey, btp_id, court_map, event, draw, officials, bm, match_ids_on_court, match_types, is_league) { if (!bm.IsMatch) { return; } @@ -53,7 +53,24 @@ function craft_match(tkey, btp_id, court_map, event, draw, officials, bm, match_ match_name, event_name, teams, + warmup: 'none', }; + + app.db.tournaments.findOne({key: tkey}, (err, tournament) => { + if (err) { + return callback(err); + } + if (tournament.warmup) { + setup.warmup = tournament.warmup; + } + if (tournament.warmup_ready) { + setup.warmup_ready = tournament.warmup_ready; + } + if (tournament.warmup_start) { + setup.warmup_start = tournament.warmup_start; + } + }); + if (scheduled_time_str) { setup.scheduled_time_str = scheduled_time_str; } @@ -201,13 +218,18 @@ function integrate_matches(app, tkey, btp_state, court_map, callback) { return; } - const match = craft_match(tkey, btp_id, court_map, event, draw, officials, bm, match_ids_on_court); + const match = craft_match(app, tkey, btp_id, court_map, event, draw, officials, bm, match_ids_on_court); if (!match) { cb(); return; } if (cur_match) { + if(cur_match.setup.called_timestamp) { + // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. + match.setup.called_timestamp = cur_match.setup.called_timestamp; + } + if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { // No update required cb(); @@ -381,16 +403,27 @@ function integrate_now_on_court(app, tkey, callback) { async.each(now_on_court_matches, (match, cb) => { const court_id = match.setup.court_id; const match_id = match._id; - if (!court_id || !match_id) return cb(); // TODO in async we would assert both to be true + const called_timestamp = Date.now(); - const court_q = {_id: court_id}; - app.db.courts.find(court_q, (err, courts) => { - if (err) return cb(err); - if (courts.length !== 1) return cb(); - const [court] = courts; + if (!court_id || !match_id) return cb(); // TODO in async we would assert both to be true - app.db.courts.update(court_q, {$set: {match_id}}, {}, (err) => cb(err)); - }); + const setup = match.setup; + if(!setup.called_timestamp) { + setup.called_timestamp = called_timestamp; + const match_q = {_id: match_id}; + app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + if (err) return cb(err); + + const court_q = {_id: court_id}; + app.db.courts.find(court_q, (err, courts) => { + if (err) return cb(err); + if (courts.length !== 1) return cb(); + const [court] = courts; + + app.db.courts.update(court_q, {$set: {match_id}}, {}, (err) => cb(err)); + }); + }); + } }, callback); }); }); diff --git a/bts/http_api.js b/bts/http_api.js index 4e8014a..77b1523 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -164,6 +164,9 @@ function matches_handler(req, res) { if (dc.match_id) { res.match_id = 'bts_' + dc.match_id; } + if (dc.called_timestamp) { + res.called_timestamp = dc.called_timestamp; + } return res; }); diff --git a/fetch-btp.js b/fetch-btp.js index 8a2e6c3..f329dbb 100755 --- a/fetch-btp.js +++ b/fetch-btp.js @@ -286,7 +286,7 @@ async function main() { const btp_id = tkey + '_' + discipline_name + '_' + match_num; const match = btp_sync.craft_match( - tkey, btp_id, pseudo_court_map, event, draw, officials, bm, match_ids_on_court); + btp_conn.app, tkey, btp_id, pseudo_court_map, event, draw, officials, bm, match_ids_on_court); if (!match) { continue; } diff --git a/static/js/change.js b/static/js/change.js index b7bee05..11a50cd 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -44,6 +44,9 @@ function default_handler_func(rerender, special_funcs, c) { curt.is_nation_competition = c.val.is_nation_competition; curt.only_now_on_court = c.val.only_now_on_court; curt.btp_timezone = c.val.btp_timezone; + curt.warmup = c.val.warmup; + curt.warmup_ready = c.val.warmup_ready; + curt.warmup_start = c.val.warmup_start; curt.btp_enabled = c.val.btp_enabled; curt.btp_autofetch_enabled = c.val.btp_autofetch_enabled; curt.btp_readonly = c.val.btp_readonly; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 2df4b56..bbc9ddc 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -51,6 +51,15 @@ var ci18n_de = { 'tournament:edit:courts': 'Felder:', 'tournament:edit:dm_style': 'Standard-Ansicht:', 'tournament:edit:only_now_on_court': 'Spiele müssen auf Feld gezogen werden', +'tournament:edit:warmup_timer_behavior': 'Verhalten des Vorbereitungs-Countdowns:', +'tournament:edit:warmup_timer_behavior:bwf-2016': 'BWF ab 2016 (ab Auslosung)', +'tournament:edit:warmup_timer_behavior:legacy': 'Deutschland (ab Auslosung)', +'tournament:edit:warmup_timer_behavior:choise': 'ab Auslosung (individuelle Zeit)', +'tournament:edit:warmup_timer_behavior:call-down': 'ab Aufruf (Countdown)', +'tournament:edit:warmup_timer_behavior:call-up': 'ab Aufruf (Timer)', +'tournament:edit:warmup_timer_behavior:none': 'Sofort beginen', +'tournament:edit:warmup_ready': 'Spielbereit in Sekunden:', +'tournament:edit:warmup_start': 'Spielstart nach Sekunden:', 'tournament:edit:btp:enabled': 'BTP-Anbindung aktivieren', 'tournament:edit:btp:autofetch_enabled': 'Automatisch synchronisieren', 'tournament:edit:btp:readonly': 'Nur lesen', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index bd4e9fd..c3a69df 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -51,6 +51,15 @@ var ci18n_en = { 'tournament:edit:courts': 'Courts:', 'tournament:edit:dm_style': 'Default display style:', 'tournament:edit:only_now_on_court': 'Matches have to be dragged onto court', +'tournament:edit:warmup_timer_behavior': 'Behaviour of the preparation countdown:', +'tournament:edit:warmup_timer_behavior:bwf-2016': 'BWF 2016+ (after choice of side)', +'tournament:edit:warmup_timer_behavior:legacy': 'legacy (after choice of side)', +'tournament:edit:warmup_timer_behavior:choise': 'after choice of side (individual time)', +'tournament:edit:warmup_timer_behavior:call-down': 'from call (countdown)', +'tournament:edit:warmup_timer_behavior:call-up': 'from call (timer)', +'tournament:edit:warmup_timer_behavior:none': 'none', +'tournament:edit:warmup_ready': 'Ready in seconds:', +'tournament:edit:warmup_start': 'Game starts after seconds:', 'tournament:edit:btp:enabled': 'Enable BTP synchronization', 'tournament:edit:btp:autofetch_enabled': 'Automatic synchronization', 'tournament:edit:btp:readonly': 'Read only', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 2a48c34..88f10fc 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -360,6 +360,99 @@ function ui_edit() { uiu.el(only_now_on_court_label, 'input', attrs); uiu.el(only_now_on_court_label, 'span', {}, ci18n('tournament:edit:only_now_on_court')); + // Warmup Timer + if (!curt.warmup_ready) { + curt.warmup_ready = 150; + } + + if (!curt.warmup_start) { + curt.warmup_start = 180; + } + + var warmup_options = [ ['bwf-2016' , 90, 120, true], + ['legacy' , 120, 120, true], + ['choise' , curt.warmup_ready, curt.warmup_start, false], + ['call-down' , curt.warmup_ready, curt.warmup_start, false], + ['call-up' , 0, 0, true], + ['none' , 0, 0, true]]; + + var last_selected_warmup = warmup_options[0]; + + const warmup_timer_label = uiu.el(form, 'label'); + uiu.el(warmup_timer_label, 'span', {}, ci18n('tournament:edit:warmup_timer_behavior')); + const warmup_timer_select = uiu.el(warmup_timer_label, 'select', { + name: 'warmup', + }); + uiu.el(warmup_timer_select, 'option', {value: warmup_options[0][0]}, ci18n('tournament:edit:warmup_timer_behavior:' + warmup_options[0][0]), {wo: warmup_options[0][0]}); + let warmup_marked = false; + + const warmup_ready = uiu.el(form, 'label'); + uiu.el(warmup_ready, 'span', {}, ci18n('tournament:edit:warmup_ready')); + var warmup_ready_input = uiu.el(warmup_ready, 'input', { + type: 'number', + name: 'warmup_ready', + required: 'required', + disabled: warmup_options[0][3], + value: warmup_options[0][1], + 'class': 'ct_name', + }); + + const warmup_start = uiu.el(form, 'label'); + uiu.el(warmup_start, 'span', {}, ci18n('tournament:edit:warmup_start')); + var warmup_start_input = uiu.el(warmup_start, 'input', { + type: 'number', + name: 'warmup_start', + required: 'required', + disabled: warmup_options[0][3], + value: warmup_options[0][2], + 'class': 'ct_name', + }); + + for (const wo of warmup_options.slice(1)) { + const attrs = { + value: wo[0], + } + + if ((wo[0] === curt.warmup) && !warmup_marked) { + warmup_marked = true; + attrs.selected = 'selected'; + + warmup_ready_input.value = wo[1]; + warmup_ready_input.disabled = wo[3]; + warmup_start_input.value = wo[2]; + warmup_start_input.disabled = wo[3]; + + last_selected_warmup = wo; + } + + uiu.el(warmup_timer_select, 'option', attrs, ci18n('tournament:edit:warmup_timer_behavior:'+wo[0])); + } + + warmup_timer_select.onchange = function() { + console.log(last_selected_warmup); + if (!last_selected_warmup[3]) { + console.log("Sichern!"); + for (const wo of warmup_options) { + if (!wo[3]) + { + wo[1] = warmup_ready_input.value; + wo[2] = warmup_start_input.value; + } + } + } + + for (const wo of warmup_options) { + if (warmup_timer_select.value == wo[0]) { + warmup_ready_input.value = wo[1]; + warmup_ready_input.disabled = wo[3]; + warmup_start_input.value = wo[2]; + warmup_start_input.disabled = wo[3]; + + last_selected_warmup = wo; + } + } + }; + // BTP const btp_fieldset = uiu.el(form, 'fieldset'); const btp_enabled_label = uiu.el(btp_fieldset, 'label'); @@ -480,6 +573,9 @@ function ui_edit() { btp_password: data.btp_password, btp_timezone: data.btp_timezone, dm_style: data.dm_style, + warmup: data.warmup, + warmup_ready: data.warmup_ready, + warmup_start: data.warmup_start, ticker_enabled: (!! data.ticker_enabled), ticker_url: data.ticker_url, ticker_password: data.ticker_password, From 6e78f4332a75088249eb8effeefa73a79d0335ab Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 14 Feb 2024 21:06:07 +0100 Subject: [PATCH 002/224] use the last looser on a court as tabletoperator if there is no umpire for this match --- bts/admin.js | 1 + bts/btp_sync.js | 81 +++++++++++++++++++++++++++++++++++++++++---- static/js/cmatch.js | 16 +++++---- 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index a4de32e..a734ac5 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -178,6 +178,7 @@ function _extract_setup(msg_setup) { 'scheduled_date', 'called_timestamp', 'teams', + 'tabletoperators', 'override_colors', ]); if (!setup.match_name && setup.match_num) { diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 99b1b35..8a44998 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -226,10 +226,20 @@ function integrate_matches(app, tkey, btp_state, court_map, callback) { if (cur_match) { if(cur_match.setup.called_timestamp) { - // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. + // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. match.setup.called_timestamp = cur_match.setup.called_timestamp; } + if (cur_match.setup.tabletoperators) { + // tabletoperators is not from btp so we have to coppy it to the match generated by btp. + match.setup.tabletoperators = cur_match.setup.tabletoperators; + } + + if (cur_match.setup.tabletoperators_btp_ids) { + // tabletoperators_btp_ids is not from btp so we have to coppy it to the match generated by btp. + match.setup.tabletoperators_btp_ids = cur_match.setup.tabletoperators_btp_ids; + } + if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { // No update required cb(); @@ -386,9 +396,12 @@ function calculate_match_ids_on_court(btp_state) { return res; } -function integrate_now_on_court(app, tkey, callback) { +async function integrate_now_on_court(app, tkey, callback) { + const admin = require('./admin'); // avoid dependency cycle + const stournament = require('./stournament'); // avoid dependency cycle + // TODO after switching to async, this should happen during court&match construction - app.db.tournaments.findOne({key: tkey}, (err, tournament) => { + app.db.tournaments.findOne({key: tkey}, async (err, tournament) => { if (err) { return callback(err); } @@ -397,10 +410,10 @@ function integrate_now_on_court(app, tkey, callback) { return; // Nothing to do here } - app.db.matches.find({'setup.now_on_court': true}, (err, now_on_court_matches) => { + app.db.matches.find({'setup.now_on_court': true}, async (err, now_on_court_matches) => { if (err) return callback(err); - async.each(now_on_court_matches, (match, cb) => { + async.each(now_on_court_matches, async (match, cb) => { const court_id = match.setup.court_id; const match_id = match._id; const called_timestamp = Date.now(); @@ -410,6 +423,14 @@ function integrate_now_on_court(app, tkey, callback) { const setup = match.setup; if(!setup.called_timestamp) { setup.called_timestamp = called_timestamp; + try{ + const [tabletoperators, tabletoperators_btp_ids] = await get_last_looser_on_court(app, tkey, court_id, setup.umpire_name); + setup.tabletoperators = tabletoperators; + setup.tabletoperators_btp_ids = tabletoperators_btp_ids; + } catch (err) { + callback(err) + } + const match_q = {_id: match_id}; app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { if (err) return cb(err); @@ -420,7 +441,13 @@ function integrate_now_on_court(app, tkey, callback) { if (courts.length !== 1) return cb(); const [court] = courts; - app.db.courts.update(court_q, {$set: {match_id}}, {}, (err) => cb(err)); + app.db.courts.update(court_q, {$set: {match_id}}, {}, (err) => { + if (err) { + return cb(err); + } + + admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + }); }); }); } @@ -430,6 +457,48 @@ function integrate_now_on_court(app, tkey, callback) { // TODO clear courts (better in async) } +function get_last_looser_on_court(app, tkey, court_id, umpire_name) { + return new Promise((resolve, reject) => { + const match_querry = { 'tournament_key': tkey, + 'setup.court_id': court_id, + 'end_ts':{$exists: true}}; + + let tabletoperators = undefined; + let tabletoperators_btp_ids = undefined; + + app.db.matches.find(match_querry).sort({'end_ts': -1}).limit(1).exec((err, last_match_on_court) => { + if (err) { + return reject(err); + } + + if (last_match_on_court.length === 1 && !umpire_name) { + const team = last_match_on_court[0].setup.teams[last_match_on_court[0].btp_winner % 2]; + if(team && typeof team.players !== 'undefined') { + tabletoperators = []; + tabletoperators_btp_ids = []; + + team.players.forEach((player) => { + tabletoperators.push(player); + }); + + tabletoperators_btp_ids = []; + if (last_match_on_court[0].btp_player_ids.length == 2) { + tabletoperators_btp_ids.push( + (last_match_on_court[0].btp_winner == 1 ? last_match_on_court[0].btp_player_ids[1] : last_match_on_court[0].btp_player_ids[0])); + } else if (last_match_on_court[0].btp_player_ids.length == 4){ + tabletoperators_btp_ids.push( + (last_match_on_court[0].btp_winner == 1 ? last_match_on_court[0].btp_player_ids[2] : last_match_on_court[0].btp_player_ids[0])); + tabletoperators_btp_ids.push( + (last_match_on_court[0].btp_winner == 1 ? last_match_on_court[0].btp_player_ids[3] : last_match_on_court[0].btp_player_ids[1])); + } + } + } + + resolve([tabletoperators, tabletoperators_btp_ids]); + }); + }); +} + function fetch(app, tkey, response, callback) { let btp_state; try { diff --git a/static/js/cmatch.js b/static/js/cmatch.js index c571276..14b943a 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -105,15 +105,19 @@ function render_match_row(tr, match, court, style) { if (setup.umpire_name) { uiu.el(to_td, 'span', {}, setup.umpire_name); if (setup.service_judge_name) { - uiu.el(to_td, 'span', {}, '\u200B+'); + uiu.el(to_td, 'span', {}, ' \u200B+ '); uiu.el(to_td, 'span', {}, setup.service_judge_name); } + } else if (setup.tabletoperators && setup.tabletoperators.length > 0){ + uiu.el(to_td, 'spann', 'match_no_umpire', '('); + uiu.el(to_td, 'spann', 'match_no_umpire', setup.tabletoperators[0].name ); + if (setup.tabletoperators.length > 1) { + uiu.el(to_td, 'spann', 'match_no_umpire', ' \u200B/ '); + uiu.el(to_td, 'spann', 'match_no_umpire', setup.tabletoperators[1].name ); + } + uiu.el(to_td, 'spann', 'match_no_umpire', ')'); } else { - uiu.el( - to_td, 'span', - (setup.umpire_name ? ('match_umpire match_umpire_style_' + style) : 'match_no_umpire'), - ci18n('No umpire') - ); + uiu.el(to_td, 'span', 'match_no_umpire', ci18n('No umpire')); } } From db57a808767dc65b0819d8fa5f910bd634fe89ae Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Wed, 14 Feb 2024 22:27:17 +0100 Subject: [PATCH 003/224] #Announcements via TTS. One Interaction with Adminpage required --- bts/btp_sync.js | 1 + static/cbts.html | 1 + static/js/announcements.js | 140 +++++++++++++++++++++++++++++++++++++ static/js/change.js | 6 ++ 4 files changed, 148 insertions(+) create mode 100644 static/js/announcements.js diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 8a44998..87b54c1 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -447,6 +447,7 @@ async function integrate_now_on_court(app, tkey, callback) { } admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + admin.notify_change(app, tkey, 'match_called_on_court', match); }); }); }); diff --git a/static/cbts.html b/static/cbts.html index 6222446..e884198 100644 --- a/static/cbts.html +++ b/static/cbts.html @@ -64,6 +64,7 @@ + diff --git a/static/js/announcements.js b/static/js/announcements.js new file mode 100644 index 0000000..2083966 --- /dev/null +++ b/static/js/announcements.js @@ -0,0 +1,140 @@ + +function announceNewMatch(matchSetup){ + var field = createFieldAnnouncement(matchSetup); + var matchNumber = createMatchNumberAnnouncement(matchSetup); + var eventName = createEventAnnouncement(matchSetup); + var round = createRoundAnnouncement(matchSetup); + var teams = createTeamAnnouncement(matchSetup); + var tabletOperator = createTabletOperator(matchSetup); + announce([field, matchNumber, eventName, round, teams, tabletOperator, field]); +} + +function announcePreparationMatch(matchSetup) { + var preparation = createPreparationAnnouncement(); + var matchNumber = createMatchNumberAnnouncement(matchSetup); + var eventName = createEventAnnouncement(matchSetup); + var round = createRoundAnnouncement(matchSetup); + var teams = createTeamAnnouncement(matchSetup); + announce([preparation, matchNumber, eventName, round, teams]); +} + +function createTeamAnnouncement(matchSetup){ + var teams = createSingleTeam(matchSetup.teams[0])+", gegen "+createSingleTeam(matchSetup.teams[1]); + return teams; +} +function createTabletOperator(matchSetup) { + var tabletOperator = "Tabletbedienung: "; + if (matchSetup.teams[1].players[0].state) { + tabletOperator = tabletOperator + matchSetup.teams[1].players[0].state; + } else + { + tabletOperator = tabletOperator + "Verlierer des vorhergehenden Spiels"; + } + + return tabletOperator; +} + +function createSingleTeam(teamSetup){ + var team = teamSetup.players[0].name; + if (teamSetup.players.length == 2){ + team = team+" und "+teamSetup.players[1].name + } + return team; +} +function createRoundAnnouncement(matchSetup){ + var round = matchSetup.match_name; + if (round == "R16") { + round = "Achtelfinale"; + } else if (round == "VF") { + round = "Viertelfinale"; + } else if (round == "HF") { + round = "Halbfinale"; + } else if (round == "Finale") { + round = "Finale"; + } else if (round.indexOf('/') !== -1) { + var roundParts = round.split("/") + var diff = roundParts[1] - roundParts[0]; + if (diff > 1) { + round = "Zwischenrunde"; + } else { + round = "Spiel um Platz " + roundParts[0] + " und " + roundParts[1]; + } + } else if (round.indexOf('-') !== -1) { + round = "Zwischenrunde"; + } else { + round = ""; + } + return round; +} +function createEventAnnouncement(matchSetup){ + var eventParts = matchSetup.event_name.split(" "); + var eventName = ""; + if (eventParts[0] == 'JE'){ + eventName = "Jungeneinzel" + }else if (eventParts[0] == 'JD') { + eventName = "Jungendoppel" + }else if (eventParts[0] == 'ME') { + eventName = "Mdcheneinzel" + }else if (eventParts[0] == 'MD') { + eventName = "Mdchendoppel" + } else if (eventParts[0] == 'GD' || eventParts[0] == 'MX') { + eventName = "Gemischtesdoppel" + }else if (eventParts[0] == 'HE'){ + eventName = "Herreneinzel" + }else if (eventParts[0] == 'HD') { + eventName = "Herrendoppel" + }else if (eventParts[0] == 'DE') { + eventName = "Dameneinzel" + }else if (eventParts[0] == 'DD') { + eventName = "Damendoppel" + } + if (eventName == "") { + if (eventParts[1] == 'JE') { + eventName = "Jungeneinzel" + } else if (eventParts[1] == 'JD') { + eventName = "Jungendoppel" + } else if (eventParts[1] == 'ME') { + eventName = "Mdcheneinzel" + } else if (eventParts[1] == 'MD') { + eventName = "Mdchendoppel" + } else if (eventParts[1] == 'GD' || eventParts[1] == 'MX') { + eventName = "Gemischtesdoppel" + } else if (eventParts[1] == 'HE') { + eventName = "Herreneinzel" + } else if (eventParts[1] == 'HD') { + eventName = "Herrendoppel" + } else if (eventParts[1] == 'DE') { + eventName = "Dameneinzel" + } else if (eventParts[1] == 'DD') { + eventName = "Damendoppel" + } + eventName = eventName + " " + eventParts[0]; + } else { + eventName = eventName + " " + eventParts[1]; + } + return eventName; +} +function createMatchNumberAnnouncement(matchSetup){ + var number = matchSetup.match_num; + return "Spiel Nummer " + number + "!"; +} +function createFieldAnnouncement(matchSetup){ + var court = matchSetup.court_id.split("_")[1]; + return "Auf Spielfeld " + court + "!"; +} +function createPreparationAnnouncement() { + return "In Vorbereitung:"; +} +function announce(callArray) { + const voices = window.speechSynthesis.getVoices(); + callArray.forEach(function (part) { + var words = new SpeechSynthesisUtterance(part); + words.lang = "de-DE"; + words.rate = 1.05; + words.pitch = 0.9; + words.volume = 2.0; + words.voice = voices[6]; + window.speechSynthesis.speak(words); + }); + +} \ No newline at end of file diff --git a/static/js/change.js b/static/js/change.js index 11a50cd..052fa41 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -113,6 +113,12 @@ function default_handler_func(rerender, special_funcs, c) { curt.courts = c.val.all_courts; rerender(); break; + case 'match_preparation_call': + announcePreparationMatch(c.val.setup); + break; + case 'match_called_on_court': + announceNewMatch(c.val.setup); + break; case 'umpires_changed': curt.umpires = c.val.all_umpires; uiu.qsEach('select[name="umpire_name"]', function(select) { From 666c82b93c4f10e72aff7c5f056ee403d0891be8 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Wed, 14 Feb 2024 23:19:09 +0100 Subject: [PATCH 004/224] Adjustment voiceselection and looser of previous match will be the tabletoperator --- .vs/VSWorkspaceState.json | 10 ++++++++++ ...0240e564-a5d5-41de-87af-6f426d28248c.vsidx | Bin 0 -> 9021 bytes ...a0c8b542-b4d5-453b-89cd-7de17945f172.vsidx | Bin 0 -> 1334466 bytes .vs/bts/v17/.wsuo | Bin 0 -> 20992 bytes .vs/bts/v17/TestStore/0/000.testlog | Bin 0 -> 20 bytes .vs/bts/v17/TestStore/0/testlog.manifest | Bin 0 -> 24 bytes .vs/slnx.sqlite | Bin 0 -> 118784 bytes static/js/announcements.js | 14 +++++++++++--- 8 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 .vs/VSWorkspaceState.json create mode 100644 .vs/bts/FileContentIndex/0240e564-a5d5-41de-87af-6f426d28248c.vsidx create mode 100644 .vs/bts/FileContentIndex/a0c8b542-b4d5-453b-89cd-7de17945f172.vsidx create mode 100644 .vs/bts/v17/.wsuo create mode 100644 .vs/bts/v17/TestStore/0/000.testlog create mode 100644 .vs/bts/v17/TestStore/0/testlog.manifest create mode 100644 .vs/slnx.sqlite diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000..2a7a65a --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,10 @@ +{ + "ExpandedNodes": [ + "", + "\\bts", + "\\static", + "\\static\\js" + ], + "SelectedNode": "\\static\\cbts.html", + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/bts/FileContentIndex/0240e564-a5d5-41de-87af-6f426d28248c.vsidx b/.vs/bts/FileContentIndex/0240e564-a5d5-41de-87af-6f426d28248c.vsidx new file mode 100644 index 0000000000000000000000000000000000000000..9b4783cd07fb30b243c208dd4a32f258ac6c95ae GIT binary patch literal 9021 zcmY+J31D1R6^1XNl&uN~q9{r`C{1iTB|xh*Ael7NBqcKm$#hLZ+msesXbNo#LRs3P zQ1;aokg|%EUBMlSf^2T=yMQbTR5no%DER;9eN!=={NH`|o_p@O=PoY{6PsJM+q@?2 zv}=l=a)vZnY@X12_>u)j&Kd0OpL5Wn1p|YN2YLsWEg4uaZ^_cd^9PTbvt(Xx|KQTz zrGs+@7tHNlxU_f9z`){V19Rssnl~`GG{11^ZmaK{uw~!lo9sL^bGvDvIi=z9p3;z% z3Q?{JU6E2_d{jRrr7cqWbV}QWnI1D671Xyi9%Za9o21kfkE^0dt*cYYg=U4Uys<{T zYO0oUo_V-7DnCp~fv!!dGu+B^X1UQ`#`3R^V%s)CRe7V0?6aoEly*;PO+2m-%eg5H zOKEgU>#DtEDG$=DGArxx^pv&^o5pBe5qfXTQT~;gzLZvle>G|715$cBB}J|#MWw=L z!r-+iZ-_^ASCb5~F6*P?@~Bahyq%nqEagpRD^uDkrD-WOga`T8Tr{*YBby5%Kf@y) zt;IF5Qmd2qOD?)MRwpQLawQw}y({#cPz^CFoG2KDq%ENuIh4ID6~9K=EFPtuQC6$m zW=k(seKDmEQu<05YY|ncfMwGZ9^^_%(B_eIIh-9$YP~8fHGK`Fk2;QvoUDxDjz~$_ zkbM@0oD7fTNEN3B?F%zyKsBpF0Df;g(SO z(Pmo_VQ8|;t8SGmHSHBCl?TmBDu>e7W0%&%=<=`qCJ*YXAk}MjjCEB?{Sn(;q1r9E zC}e3-oU+%aDSU3IuB=w=l^9oH$iJpGJW`=S^{7$QQ88#;)U-0ns+WyCs30@>)L61n z&1a)hts3;1h*_;_QfAa$PTHfdGS(k8+9dKR`+ZV+qPo`dqh6|17a26BG(S`Yeblrn znpD2JI<%%x6H`;AhRZ&!;Y3r=`kg)Ve6K>Zp%m(?`9AM|aId@yL}P^-L^yQZ`z<}xfg%6|81 z$FD&FXdmvoCDf6O=dGcJkLQoUkHc-CLveqt zSnv~Yd$uo)KNB-jF5VH?!jDf;gZ+o5jXs4v3~*a;7ST~PON*jJ#ghy_4--Q2!Z^3`Tf5W%oJMcg7UHBe+A8vpj zzz?A>34PKgum)}lhrrF?P`EkV0&WSnf?LC3@MG}fa2vQS+zx&MZVz{WpM*QYPr*;a zo#1ESXW`E9bMW(U7dRY_fH}A;`~v(U+zpO|wXhC;366rJ;TV{QW8vz*FIA@N{?v zJQJP;&xYr~bK!aLeE3cHE%`6Yxp+6#NT( z8a@M`g@1+5!RO%%@J0AH_;>gcd>Ot1*Ta9nSK({$b@&E+6aEvv1^)&A4c~_E!2iH^ z;d}6XxB-3uKZL1<_dl$Go5CS*GdL9Li|iy9X|G`t{S4Lb2z?RNAl+C0a`BWC>-6h- zmon{9c~qJhT9J;`Z!Q(0rl2o_a{9))5L%Q@N^Q~>eG!?{x6!U|r94UHwp5l*j$U2* zUM8n+@;arJu$&r}Q{^Lxfy6mjxBZql? zNmfv~J!%|PUYWIas@>Wy?TQ*_IIlccq#ivlj~%g!b&D0RBq7tO%dtD*bDnBI|V&Ag%(3Sk5bf^lpUAd z%QLdtrli;lnw{6AtQ=T9SI^FBc4=k>{o2NJw6=wIgwB*sQ5Fknh9b1)LyL-~sCBk> zNxgd=aWOo#OTD_*QmCUSDxcPFX=}84hr8_7R_Yx~t9Gte(e)h~f1<{)rDOBXt&@5^ zt=&@3)moOi_y6{{&zSeLbxf#j9D{Ybv`w^5l6ELU>olqNYP0M-R=37!jurIjtWyuK zk^Q=y&9hcLT<(!(BRMu_)N?MR{X?^rai$$-HgYkZtUkxiWHEMZF)WIDE~}<3)~8(~ z7nNs6-!5CLNAbE_?WGhRvU$2^QT=@a%8|u#_$e!^UJv)`jQTG9eWPh>e&{RYezj@t-ihLq`pzI znPs`KmQ~ZCk$szZHa;cU&E*(dBGT5-QfM~gipt)TEgH8~|9l%+OH!YwkKTG-jk0Py zKWn$t5qWn`kvhxP8B)*4d$g>`yt>&=^t!c24qR_P-c?!j1(m%b)|SwjQt$9K`Sf1) zsjyCwRurSvHO|;aZ)Cp%%Gyo7JADU~L%m8qyVe;}pGfbL(V?EJ_pfu23w1BAMs*K4 z$}D!P>+Q*V%l4joyT&c)pR;GJNPSm(=aoZUkKkUug?xUjek1r4)rQ)?cbC=HmGEDQ-QkRR_qcwh#`GSqm3r57L`Lm< zw91~DwG`Sd^=)G9ly*fgpHP<@LYqQ8C!a}ctJGszeO{+24}POIgf5rnHLlfX-s@rY z2>FQGZv&572(^t@-RgSpXshSvyL=&JJEI_-5|%Ti}^jnLeC8^J)wK=p!>iuZVOS{yP zd33()*L%Wmg>tCN)xT){ZuGgawug4b{_d7~MXWCSHuB$;U2$L3@m}#?Et7OFIvZY< zN}R@asrz|_`eKEw{wvIXkGkGF(yKKpv_a~d#o8I#tt|MR{t%_=LIS~~vhF0)Fn_$93q|DLcpyHP7WMzrg%61~Bn(-flJ zewFB??oA>(y<%o*)!gz~mv^3BQ8KZrv~%TT%I)zdUAL%_M5zEqJ9zXbc=*ddMs2#` z>Bb1ZYSpBwiV2g;XU=Sp?n494Y8+3G5uGt{=ESL`vz+OsKio8;6%%JwmO7IJ-a6Do zqO;1Y${Qk@Ik;1kwqJSW%*hSW#PVLteq7uLD-}Jc$_D8fLHJL&bK_S!yJGIIlKp5` zo3#kf`Lkb>FX-B+?U$5J zE-xwVOgH5$O@?^YUmHoZY|i*D{})J`lG?3tD<#p16%!{s&IUyjzbh%9E4# z|4o?-qGxxe8JJTxxwL!^Md2sX8#gqWXw65FukiIdAKCse3Uq11P&oc1$@@*gjp^_xa-e^#jy zoY1iQd+qwh5iKxLTrvrpahPlV5Wv~eq)Qd%{6+W%qs z&{%{rY~tmm|HtNmn$H@yeOkTC3HCcrCvT1Y|7p`d{WZ!ur%|VWjA(=O4CNU`jUzg8 zKz_ln0bOR4{Isr9|GGH2aj|J-)kNz3_{oInU7Sp*d4lJayU%G1=08rf@_67kx$@M= zZ_4_fjpFG^L?_RfNb&08K=q~O=Qf!rv0s%FP1N*mI?>rPD#~UtoH%8z>y|Z{XjOT| ztn#_1l4L&WizX2*nb1A2dxOWHecpLZ;pvp=6Q@>=&mVaHFMXa7sVnbxLv(0AN1~aZ zyCUkm(&BS_x!C^j0>_nR+~A6+^Gg5vnJc2B9ap;Lx4m6#f28wD12?)NI?QpU3qEi~ zbc`d>S)Ka0*#4+|N20?=xgk1clq1o}_q!tMeEN4EbVKx#0gfwO*}HGkdD<_3kR#FU zC9a4LcO?4m23JH!Iugy6kc;gX40a^it)m;F1BN;hof~#V)S0Irz2=IjGf!h;^IhmnN z(qp8*i|vne7XBd@xFYH-r=91zAv(tSpdNeP6;WrAdMvr1={)V9KhRP5Uq06rQD-pm z#za>{olpO>C9a5$b=-c!TW*M+U+74*D+jxTShE27Q^HK5WB z(ZLrw@^r%;Zio(Z)~Hp#8R&wig+m-yy6>N^h&mgTk=@UCvr=a@y?l`yqD9UJb;AU+ zZirssY^84cqZ^{bobBlcU%Me%=&VtLuf3q@2Q{F-GeWuZzix=0@9g+CJ~70_N{2gV zCbqrrhUiG=(|58bcnx3`B6;Wq+o^!wr(b3Lv)YzfJn$FXKgD-Hj zQu`lvMbz1#+_ylIW0G!*bUJE&VGORqppaKa1{POWR7sL{Q=HAz3u`xL>mYsUS92n=t$?R zQe2BsI!`8KkSOAv)}LYy$hm)@}0MTVQ^H_ z4{Fd5XE0HAz8j*2&LVa5OgBWwI1`W-hxx|1;OQu5%RjK28=|9~Me4FkToApW0WfjVLN`PU zowvVgmm8u*&T4w!cP@wy$#*UuJe+=E(@%d$15xy|1#XC*@4V8NE_Xw;frEPgW>-X= zQQzS`ZiqGz@fCP4a&b^&E^HkVcozrNKo~jXc2`850ouPlc0;s*;A}#R-?`ZSXlEq7;5TlFj&(LD71b_?4sRej zS@fJ6q78tF8NN%K&eP!ooYl0jn=7Ku6^U_|xFLGJv*RmU=!WQE=WNeYyW9|MAUY{c zpWx!4Mmocu*DiNM^b%*wfB(}ih!!?b;#|Ae1<|5>=k|twb}4Q;PmA)M4T?*m&T{(W zHEvegKuW&){fREN-#{DAp`1xBhz@mjeAy#i5p{-=uiop1Xai;AJ_pT+@9{GzKqWztdT_av|L$rbXY0l}> zTx@@UbLH%;fo_PN?+nn|Ep$V4u(LM}?{Y)50e5oREoClvI@I~}yX9Wyf@lL>og=Sx zL$m>RGW{i2M4cw<~WtPe)(iEK&~+azk{8vz2<|IX6UyIw!hgKXpU2&^d|VnN;C| zr$x>hwP?K?q7B&7+um|RbhNWKefo?mTx`FA=9WWSToHBF{j&nI+^p2OcyP`T7evS8 zJD>iMIyXccFerQdl}+FNm?6$Qee7WuL}$;ud}jIFnWvQG5_ipYvC;-IXx9d-To7#_ zI6E(WwhN+{G=QaUaY6LL27J`8XWS5NAmq6qcTUr{f8k(f;oowt3!)e0JEuBdddUUR zu>+ia)V7cdIlZKTfcJx`Zio(cUTObln?$sXS1VPHFD)snIz>l$CCJT>&1=f(M~P11 zO|Ycig};7wCC=q<|NlU=V&>1U1AAlGmF`#i%R9#2dcg(JhAu{Wpqq>B&zd;5Z03~m zQ_kw;^sAb_eRGj)<+RdY-h{U8?Isbuj8`i+d<*i2XI|Zum6GV>hVQH0HLS@*dB;)% zw-%<~)nuZRE9aEX(|g8Fxe;w-;x$d(e#2Lm&AYzIM9qajr>s)-^{&^OOq7>PPIbOj z_tM)sb!@_=ReJhmXP?u+rNTc}T-#KhD$xlgPHS!KJ!r>n!aBav38hYpeI>l2QP;no zL^OK4meXa;E{M+LJ!7>0yl(85SB!o+azRsh+VJJiYrkW{i^Z_PDjs`epk1!sXRS?x7w-OuPa;B zM4~07lV&?Hlq)TMrl~}yl+T*cfaRazy{?HXtty*QdR6&Mr>m-OzNpDWqgTtF%28nD zD@`Igy=+qD_!+Z#`n+AX3;!DlKaU?{peMt<9S>6ESi(!)Q>B{A6|4t^w>S9 zzu>_SsWmQEI;*_A%He{&?Q`7_J+{zaUM)EE-o_EFDy^&GU)}*ebE3Hm z`uKmV;ddoBHfsAnCt5ahayc)mEv+h@V4RB)+VYw5wR7L1xzF?Ybw!Sx%>LD9ZeznD+E=m?#8ml@zmUTKHf;w&-DGcit7KTU)gk` z4PK|;aeLE)%!; ztSnd_v3z31iIpJMZzRw`q$@7S66pj9h-5%=ARQnbA)O&<2(?Ez9@7#KY>q*v=__@u z5SN2s2Y5TePLx~I#BL$^8DgI%p@7_yB*7H1{Ss;|740S1MiSeJohgB~l9emLpmH1b&60+Ju&f&gvv*ObY20FlsL&%SC z9HgzTA(R2{S#oB&1mfZ46I;SQ4cwX?5Q|G915+iOrr)yD#hWa_G}xz$l_y1Hay+Kz zNsu2)R=)AMcB7ng2@Pz&wM>qZi?rY!Bxe^p~w$8U3tqNvO>R@u0Mg(5b7>jaoC%TU_iXN z(C=o#n+2~&f<6ftNeJ*o{`5xwUWo65l%BB8gMBXS5bS

W}Jyh#vs&F!;}hH3%s~ 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

Tr1&$g^pYZR!g%s~ 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

IsX}r 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 z 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�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

|CJE$3I3{bL{>L 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

  • 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%0AkIP&*_|3ir22>E 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 P+V{Vz*zBYJ`~Uv|9L?Gy literal 0 HcmV?d00001 diff --git a/.vs/bts/v17/TestStore/0/000.testlog b/.vs/bts/v17/TestStore/0/000.testlog new file mode 100644 index 0000000000000000000000000000000000000000..6bbf10ed81b7163bf1f3b40a26c65804007b7561 GIT binary patch literal 20 XcmY#XEb%NUP7PsYU|>)HVh{iTD<1;I literal 0 HcmV?d00001 diff --git a/.vs/bts/v17/TestStore/0/testlog.manifest b/.vs/bts/v17/TestStore/0/testlog.manifest new file mode 100644 index 0000000000000000000000000000000000000000..e92ede29d76aefe079835aeae278da5341f6e15c GIT binary patch literal 24 WcmXR;&-W=QP7PsZKmZO+yIuf90t7Vx literal 0 HcmV?d00001 diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..352f6d0cbd56a9d979cfd332f981d0316d339e5e GIT binary patch literal 118784 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 literal 0 HcmV?d00001 diff --git a/static/js/announcements.js b/static/js/announcements.js index 2083966..04de96f 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -26,8 +26,9 @@ function createTabletOperator(matchSetup) { var tabletOperator = "Tabletbedienung: "; if (matchSetup.teams[1].players[0].state) { tabletOperator = tabletOperator + matchSetup.teams[1].players[0].state; - } else - { + } else if (matchSetup.tabletoperators) { + tabletOperator = tabletOperator + createSingleTeam(matchSetup.tabletoperators); + } else { tabletOperator = tabletOperator + "Verlierer des vorhergehenden Spiels"; } @@ -127,13 +128,20 @@ function createPreparationAnnouncement() { } function announce(callArray) { const voices = window.speechSynthesis.getVoices(); + var voice = null; + for (var i = 0; i < voices.length; i++) { + if (voices[i].voiceURI == "Google Deutsch") { + voice = voices[i]; + break; + } + } callArray.forEach(function (part) { var words = new SpeechSynthesisUtterance(part); words.lang = "de-DE"; words.rate = 1.05; words.pitch = 0.9; words.volume = 2.0; - words.voice = voices[6]; + words.voice = voice; window.speechSynthesis.speak(words); }); From f8767780a3d57d989f0ef63b897f298c0e8fc80c Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Wed, 14 Feb 2024 23:23:35 +0100 Subject: [PATCH 005/224] Translate Shortnames to origin --- bts/btp_sync.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 87b54c1..e8e260f 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -159,6 +159,38 @@ function _craft_team(par) { if (p.Country && p.Country[0]) { pres.nationality = p.Country[0]; } + + if (p.State) { + switch (p.State[0]) { + case 'NIS': { + pres.state = "Niedersachsen"; + break; + } case 'SLH': { + pres.state = "Schleswig-Holstein"; + break; + } case 'BRE': { + pres.state = "Bremen"; + break; + } case 'BBB': { + pres.state = "Berlin Brandenburg"; + break; + } case 'SAH': { + pres.state = "Sachsen Anhalt"; + break; + } case 'HAM': { + pres.state = "Hamburg"; + break; + } case 'MVP': { + pres.state = "Mecklenburg Vorpommern"; + break; + } case 'NRW': { + pres.state = "Nordrhein Westfalen"; + break; + } + default: + pres.state = p.State[0] + } + } fix_player(pres); return pres; }); From 196ec8b9bd327af971f991955c5bb227bc115436 Mon Sep 17 00:00:00 2001 From: tengmel <160073256+tengmel@users.noreply.github.com> Date: Wed, 14 Feb 2024 23:28:07 +0100 Subject: [PATCH 006/224] Delete .vs directory --- .vs/VSWorkspaceState.json | 10 ---------- ...0240e564-a5d5-41de-87af-6f426d28248c.vsidx | Bin 9021 -> 0 bytes ...a0c8b542-b4d5-453b-89cd-7de17945f172.vsidx | Bin 1334466 -> 0 bytes .vs/bts/v17/.wsuo | Bin 20992 -> 0 bytes .vs/bts/v17/TestStore/0/000.testlog | Bin 20 -> 0 bytes .vs/bts/v17/TestStore/0/testlog.manifest | Bin 24 -> 0 bytes .vs/slnx.sqlite | Bin 118784 -> 0 bytes 7 files changed, 10 deletions(-) delete mode 100644 .vs/VSWorkspaceState.json delete mode 100644 .vs/bts/FileContentIndex/0240e564-a5d5-41de-87af-6f426d28248c.vsidx delete mode 100644 .vs/bts/FileContentIndex/a0c8b542-b4d5-453b-89cd-7de17945f172.vsidx delete mode 100644 .vs/bts/v17/.wsuo delete mode 100644 .vs/bts/v17/TestStore/0/000.testlog delete mode 100644 .vs/bts/v17/TestStore/0/testlog.manifest delete mode 100644 .vs/slnx.sqlite diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json deleted file mode 100644 index 2a7a65a..0000000 --- a/.vs/VSWorkspaceState.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "ExpandedNodes": [ - "", - "\\bts", - "\\static", - "\\static\\js" - ], - "SelectedNode": "\\static\\cbts.html", - "PreviewInSolutionExplorer": false -} \ No newline at end of file diff --git a/.vs/bts/FileContentIndex/0240e564-a5d5-41de-87af-6f426d28248c.vsidx b/.vs/bts/FileContentIndex/0240e564-a5d5-41de-87af-6f426d28248c.vsidx deleted file mode 100644 index 9b4783cd07fb30b243c208dd4a32f258ac6c95ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9021 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

    W}Jyh#vs&F!;}hH3%s~ z;2jQYD6YB?_9(0`MC4)w#=v_Ktg(n6hsf{X9f8PrSeL>(5mqs*$*^ajekL|g#nTnA zCLyU5)-+g^h|EF9azy6AHw9J|B3HwECA@QSzA7I@KYZa=OWBom_?uK;>cC1GJeaN~KSr6c{Kf-$(;_rq1 zXIOuMwHDq#!G09>dIUDZdJyM62Ky;k&tl*P#Baocr(xG3vK8Jf$lC<(lW4IHN46pG zcUb>`^*Zujg|`mYc1(Q{-Z$a<7pxax@4)-JV7-m-KViKLdpB-)1K#J+aVM_X1M3|m z?L*(U5d0YNpTIf*Yd_xj6iHvfsz>C%kpIDY7ajkNzz48?fb|(tKE&);Y5zUKM^SJH z0trWOmn9b*M!YQ@2g5fD-l^z!8N8LKFGt`e99a(UQ}8|x??!}b@%9sV@dJ22gLM>^ zEj+lu#jpzzHxr>s{7{Y?YcOyHymujZA1rS9IaqJP+5`Iwcn_lQhlmW&;|`60U4pfh z2=TnG*3+@W2-M)=NAxLi&%pbUZs)>-uw%tKqB54|6JLV(@?jOhI~0#nD#~Ebf<0ST z8!pGS%V5uA7@k^uy#ovTqJD&OCQy$5)*x{sipZ1KVC_SoAA&>SEyR5@@O34;<@k0k2G$_7 z7~wUrHsJn^n8Cg5Ky^KW-=jw_ecX}}I5HFAN>rEQYD#z*-Wt5M9O1PHZ$z*b2e)GH zbMS6M_z8G-K)z5m2D$iy*!v;uUI_L?RX^B+A%$2o0JpPz??(hqW4k zJ5hW)?6t_K#a2q}I;3qw@CihA!1@T@{a8|uJqM9Nnnz&uhOZZ@`rxCU@b*{Y1B*~F z6yrv#Qum^Gj6vWcv>1mGzf)dCN?=b!#$;HR!@2_YOq5q(+$0p1V%{`8&GzN!I|W{D za4zz%LEu{C&qs?J(P9z2)%epgc$Xkp1M4a*z7^iz>v`GNE8B0TxZVTrov?00;9gjd zsA5{{G5A5)8_@4>upfs-#y+jbTCx>iuS5GMVE+SwSJCT51pWo@3uv(my>?*1KjD2D z-kn(Y4l>?C;DGMh`zgF%q1WfIzJ_%Wkq^=P8(1;oJqqt3q(B8@`+4w&;LTSlja&%t zD1nOZHGqnxj=!nJdecLTbvK>LO8E=AwF5V{%OtfQB#Wpec?S9RspO5$hrX55S6qmMj^IPPt6`{Rz3^XOjK0j z9kW#XN?2joD^zE#J8;f@@ct3jpVjDbd4Y{cs)h9o>@Bd?!TTz#I;6dcf?epi1G9I- zeh*3SBk3J@KSuRO2<%6I_Ub!jX^cJ)E~o;bZPTAl|h1gjqg z<-^w>R}F++0DAz6hT*mgV4ttDmp2IBA+U!dr3itc_--_;3t^4Iv_f?*7hD4W7_=G* z`yzP9!aEM1{qES&*q35-3C2!DTA4Z;-(+|%hi`(qn+3BFxI#Jb!c^2(pyg$-C*jYf zNSTI|*~eT@O*uBtgEa-VaXn;_y#Qa%#dFuez6QZ-u_lZb^I_(_3#e62;npWuBIf%}oN9@b{qe}(-ZypN&dQ?Q>!#|^Om z2J3Oy8&Ur>(kNG3@%=MszXiTc2tJ7?)?x8;uqkGLhwmS-Uq{NT$f|?A9R)9<_)XaV zg8hnmD~nRM3n~AE{Sxe#k+&NoUW5GxtmiTMJ#=~>op$1qJ(#u^fp=iVt?0p9BxX6DPq02@XfhD1=@_a0kK%5f=Hw zx$q7~co2dVm@$yM5O@%Q7ZH9_Hx)d95XEvS+P|p(tUCy66aGp!O-snv`yx0MzIEue z4_iGF@kuyNb|y%86s&P5-iUUikaH=*t1w`X>Z5!LIf@_+MPCF)DZ=sGfW?OATZ>@7 ziSSN**%t>!;ni{at+{vL;3IhGG1Nbc-)>Z`?{NUWL%97ggxl$j-}lG=24l-`6pun= z9Fj}mpMZTc@TWjf9Q5H4o40Wi;E3nD?;teQe!>R&Qa& z0kr)T;Ul>AC^jF$G{|2MD`&V)xd@@L2#teWiuj2LPeP~^)*SSjkI;<>RU^0p2d+{s z*|#9(RS3EF0z$hWd(d$&7Qch=mq_^(DPQUK7tszKR@T{v5nh1s22@la_%OcTh=Z>q zv>SoF%67R!|{3bNzD<}W%PK;`TmtTC6|IEYT zTpT&|^=A+y{c2=gg_t`KSdA+lLDojxw+XTvA)X@-@gPDXF&;)A(t8xf&PU9hupdF` zM^?nEAK+EKKnfsK>*Mxo1mc>Qj)Z%)y?gbBHjiFdu2)i7?D)sMt_q82};}<`~an4nPlo?;RnJ2+5SfLHD{$IOM5ZEA zfyia>PeP;=3#K7sHvBxDIoLcM8D!Ty_@}^ce2V`{_!r>tTtu#e=Nk077T<;unUC@t zac~hbZouYhM3%w71bujRD-gK~hi-*`A=)lQh176fXEtn{s{kV=yNape}?}r@IQo%wb=a#j{XV$ zM{(}`$XJiaW_bRJ(;h^G#_B0#Jd4N%ME(ZP!-zZ%KW*333MT1aBG2I97WCPK$dig* z7$f+NfAJH>`~w-UW5KJ~T!)P9h`flSZ(`@a;D1F;R=II8o8QLLf5QI~JTIeuH|k%5 z=MDIu$2IRE@;)Lv5!r(Ud-3Eu*u4*tx3KwR)PDm1f8hTJ8DGGE0FnKOe2LAUV)Iwn zOz|*Yrq}<(=%R*kR#z(~M6xt;k2=J3@5qcHjT?jvq5FO|Ngc;V+8`AgD!DS*L3kkU@!-U@# z4^v=s<2ZyUW))b>qhEc%@E^743is~$j?H4 z9_-$T>4o=tVryTF;P(3|-kVALFcycRc-wH`i*gK`tMNi)DZ*UOgBZC1c^eT^i&_(j z6W;7K5IVFSp&i(=6WjM=Jq6=yc)w9|8u%U|OM%2LpIjFw3lrqJPO>Ole3`VdU^3*< zez3@V!chiRLl9pC-%t<(bh-rANL^1Xy*XuoD4I)}u7Kam>;#S2wFsM_W@Qb+Hz8&f zwlRcR4cp`vsAz<6bSuRV;`9xOd026prxu~D5M~m1g6q(aJbY6XL;={#uy*2%Pv9Y% zVzlxVe2mtKSiVOu1}TS-7Eymnlyb-j*dfE-BQ%UW&7KJ5q8WYtSX@_v@D-X02+TwL z3WSy-xK`5|dp01r1v%T0#8`tK=W`7$tyqymDE7#0K8YAu;!Tk5*)o@8b1{Y1l_B8( zlkg$F8ixQuHJLCKex_^Y!hfU25%G6H?t%Xngc(XOd?S>76y681g{rX*o^2TL0y1}F z{PPMO8G&+l;ZI@x4}L}s|2C>>5(V_RHZvRFBFiI}`sDotc|TK=RD&2t^g*khuyZlr z?D3Wv*gO|fqrqd$U7GD7>f1-bAd|PD?(Z5v`94#2<@qF!QBs@+{&}6mn=Q|d(ezYs zh9)1l_+vtT8@%h$@2^Uy<-@SHVf%aVybsS?$U6W(#fd`j+lk1?^b@LOH{pP91AmX6#w;npDdhHEGkr01opnu?%FAcR(+nyRrFd3Py( z3o`s)kAFW1o07IpH=>4u7I+`LpCJB##%tm4;57^nF)Ly#Pa;{kgSeV(DOJHHc(7=B z7Nc%CLJw*df^q1S&k;DJs|p`RUbehsL@uR2LQLt5!Xk#rV-*Wk%!7xi7-sFLt;|9^ z1OJogXjVdCnMO(2g#fcCOhkMN|L4dv!FV`UQW%UMhHb0SIAqS9Xo1HRnv3RCr3WF~ zaBwHK>_?E{!x03dp+TG^GcKSC^OO95Q3pFqMP>1DglOExYu1r@k}&-*qq+I;F?OR~ z9Jg}Ocui>KD8qM}#1Zp>jJ6nRF?;qLy#G)rYtzW@fR~1j5SOXEj}c(_Lm6gl9!;Vi z#+T7N>KT%fBUH&PG;72XGfb+}V7BU;5L}EeS81ZG3!$b#M@9P(|GABzH$f1m((E8r!VzglBejZB%mqOz|<1ugty4LR-Q2;K`H zBT8BTMv)I9{H%%`LwnzoNZE!o${vkp9lU#B@5DfA4Rd`5Otz#R?Y=W1+xPG?Ksy9R zWM2B09LX|T(5|x@^F*?`Cq5pf!cUo}&!Spc)d&$}E=3IY`+z3ms9e-69?&~aApZ#B zs7k$I5m@%u2*t}hB!!0nxI_bX#?goQ#pUoDecAIMwy#68ZJ>p>cWDxphKq*L&Qz$+ zFM4LE+gh>|%a{b-rfHvkwE4zbdix?YNE2CMY7KLJ)KD&Rj%FBuv*vqcKX&HJOug zo2Cwu9)@Qls%sHXr^B2WpB@3j>IZlbHCVM#6I1cq)U9V7QIVG65awQ}J|hsEtour1 z;%F{HMuRl#PEg7O9ib!B5zK_|K)<))eFGjEivJ;yBV5CGQ~?^R5je_YrQZz0vmD_E zQD<09>$HQGfr*G6IA~Ph7LRl=g0W|yX0-AyFuS-I?U~CWGB1HO5q_d^qXHF|X(s1v z2D7XhFcEFEfU*}{qB|PGc$T_A6Z8NqlUMODukwgV3)4J4rpXg#6#cabQyr)af7g)b zR+H}v7;qJS2mY^+R*#mnQ6H$Uv}mI!S}~f|wvVEi3}=v-AaTSw)Y+a$;s|K#cn;$NyHSk}hL9>q!Kt2v1;0y_y2h^PrtF0E4?XEL0wkV~a!zuqUA#&pN@grABQf;~W$nus)HEF057 zJem{(el|~qO)EDK_SNuH9cs{RrDpz1n6h3E&sMc^fjZUkD3D{qK@CVfYQKYz=`Wtz z_nKZPrbVUjcv$$8Fq2Nknzg6pya_E9W93H3lg2N2B;4PeFWNHbS zb}GUuW6EsejYC>=UP}R{;aZL2`;f$x%cF>2kBgZVp&Q~^e6Bjh7%GK4zfYG+X=g@@ z8~kwyY!kNI(>2lB_5p+#y}kmQ2ImvaL{(4@dm?D8jXwPr#h=WP($BWvr1W{>uMvD2 zy?BH8$|H5mD-t;nY4nA~6gnXUCHWF~N{}%F3yJn8Av{kD8FJjD5XJeH!*{nrT#I$X zs5xbg?k9!16>5LwZr2TGNtG_5QmQ2jS^U$H{mWwYu^i^1AQ5x8GU zQSdHBYw=WV*1uvU?T{WHA#Oi1AaSqF&!O&~_z{+lu2ex%SZdnC>$yE3sPOMQ@a9y}pf1K}x9 zd)|JS=6Tv(r1{me8B)=hGhIIepHEdmwJuYhQ*sLCqTLO!?$SbJfEn_8LDL=2_Hbx%UWy6!>kJ#Q4i2qvOP; z_>miRn%|(cr~GhXA;u|lG{W+%LY+|-K2Zy@yL5Mfw-98$m%y8W18o}PN7~Xj$?A`e z0}#9zc_q4ld8Jy>pxi1fG;-^C5Md^Eh`@bI0iDt}BaFRkP>W?5p5(>j@>a0Tq zCxeZVDM5(N?x<#B*DwrW^_if8L9{uPg+571kQCxZ<~cnQplPD;Q|y_rdJ$nJojyie zhQLf>XQCh%)5fWLq9k9xL(iq%d9X|_F+hpE2%d2eBdEl;{>wD&Z&55C&`OPmV#((| zi9YKz6_&=V>5Ga{={@~SSW)m|#r__BSY)I@VDz!mVp&)tK3=uEm3mO(A zHN#)Mh&01&FV1X+pjUrdHA7xAtZIg|=w~v~H49yN5L`bUR-6o?sXRjxJ4lDFh){<4 zVtd8^-Yj_IM1CW|t_U~7lvo7#hz@F3tFusj4ka8`Ggw|&nWkPJC*EcTy3>xcnF6e#)c2g#dWzRSJn$c2`tp=bFflt_q0WXF^qS- z%(m5P5`dfxumG_Gp|=$dX^oPaMc?-!f?XtVoE7@SJY&;xB%CePxp<6NE*FJ@_FMIU zm~R=}T-wJ;w*(o?NSVtZu;d?ku=w{@SY!pykvz|q)1##LZj2^mF3c7?Pg6{za?yg_ z3(VCIRvg95dDv9#Y3pdx1IuAE@La1s0HIBaj;PLBus*02RXXlKCfaAAeI5e2c$Q9q zI>IL!kq9J6fR8f^D_~M7qrM{ajEv7>eJM}SJmh+g*s@|r|Grf?D zgJrOrOVH5qnSCu{msGYkIN4pXCS5xOo}!K{)z81qrx@b#y3H8!kVDH5GDkzuXfy0JOzyf7HCq)& z6&v2(l#5J{%+AFHT)+7M&nv<>@%T{&yylDM93z5o3~aATnw4cvii^BaLrbPE?N_zD zMUDHhxr8Z!ITG7J(+-hTu_(rqG!b`mF8YoGd6Ll|VUG;T#n9%8))=nZ+!6P(T#*k? zDtr)OrWu>zL{`Nz$m1ZD{KhwQ1K2PM2MoYPS00F`LHruxNL6f zf?O=k7Rtu@-wNN&%EbfO(jga{Rxo%0!xk<((%eW8kJ6899l*>&wNR z4A_elmTY5MV=5Lgr?C{jzZ>?Gh%<=#$WC~vXk4B!N`jTADRlihPp~i8K0%mDjxo3h zeX0J7%cirlsrONpXGJe+{|P>iOL(=B@GMFr{f^l}Boh;|Fd-L**q_5X%L4f5B~9Mr z$U{hbSiMYJkJQH*y`bsK#gj|1kj&qTz$?1ETQcFvf`^YI{Em}28X{7QDEIi`z2;fH zjMN2&Yv@EJRrJe{;h=(cdML(y)P-tMEXQxf@h8q0^P99l5R4uoHG6qq-)@U7iqObe6RxvCS%19POKEh`{bL2iXC z1Tm8QUQMe|POjGkn*2fA3FKxiz6h254$X4Q-CF&j7R!B*J0Z729)PTY{1I{+lI)#2$csRk7-CN&q6jp{swti(*^Q4A)ZuQemt1KA6C2eJ?H7UW~dCy@U@KGLo;IRM!Y`O-8ze5HOX6HjNl<# zl4;6JlZKiyT4_qJ`Aw~#aa?-u@FG4(XcDQUv$KWzJ6~YO&`lo$HUv45bal8?G;M#) zmUE83r8vj9FeWf9V@3izYZ^&@I7jA@CcPUW1o=rGQq4EPpRLcjG+Q3!XR_s1s-e-B z@#dcc%ra<8SDP2VrqM7zEx8dI)l7Vug)djaM{)AV^FH}7PR1w5hdHv>7+7v0E*GEC zV((Lf{|+5iCd#rhx<6<@$5tp7Yr<@NS_2Q;{MKrcfw{QV&*H9Hv}J(9cD~n<`zm}ibT8{7LwnHT zE!babhQUKQWSy7R&lGn345gVLII4w-E|lnRwdE|(2cdr2zZWxFlf$g(ZzmH<)TG2u zMPw2dPD8--NH7UTfRy(D7T<>8dR4{+^kVA}A%g~!sH6E4*-)*^jU4&#SKD!>FMvFrn+-FQIj=@n0FLeG8z_1iW3#Cl(d zO;$T`WIv94sc|3sJnLg+0pqydiWMV#P+z=9Bi|pzeBw|nQZu zs>F(NM7C)=;`wY6;1~PpiCjGt85e8R%!G5?ILtITXPcj#tDO?|dd(YLzfQl%hu*m# z#rzfoe*m4m2KE&(fu(6-9aWXgIIBM;xcyCLl37T?IZ<0$)1%x=< zVQdr`!agEzj?|fd_RTcSnRzH86dZ@{VOUGm0hQ%Qn)$$y9EmZOgpf}MF=%ONaI)nH zEt`oBHqoXLq2Rp3xmPK z7P`FG)U~Z*iPH#Dd&bn3vP+{#hWXJzlD?*cB>q-`G0kr`vrU4ft8!$!*~>g)HrhA( zM;aW8C2b27hfm_l%?Ae3$TOJb7a3d4RF7~g-^xYWDotNJO1)d9*o-k30eqObP`)cv z4HKJ8N7_yJ;2{JahL7hMr`W8X@z)Ko>GY$EA>@uWPVn_{SduOA%`n}oV3in{-i^c~ zzcDA9iI^;2B&4+u;~BFb|)Lqg;738Zz;tR-Fi08C++J zMO6Q$e&!G#!PVws(M|-KtD|9{q0J|>($6AHqMycm>(AnQJrQ_OQcx3WaQ^q|xYu7ubQ>4|%j3gYni-bRC7rMaUh8 zK_hVJQd}|-m$JdK0{2hSmaS{&pm#ZPs&F-18RsLHZR%|4tj6s%iVP1eMSr$&F2@h- zDRvRCsQUTA<`5oQQYo3v;!>P+;v;v&_N@I-{!WHQNglg|IC zmGmaYN>^z%J3w8y0|jh==OL~|c)d2jFa!&3Qq5iSs$$ot-o&C8Ot0x~jS7?YBKVGq zS>2b%!RJxaa4V9+ItWu_htVufp3jjMQ9*5ncf4rb4uNQ(x)HZE7hjG%$9F0GF-H5C zRj^_&2gE;S_h`o(qZSJm=x%>2Wt>i%eENk2rIm8#Wv0bs9hLnpTSB>tl)WUxjtu&_Cqixb5W8y;D zvdQ4v@L&~^@I?q-ir_?qc)o;ca}c^#le!dxz*X8#9C|?|C%jivF^tMXpCa^?+C48M zK+S4~ZeFw}e~d6t@0i*m(;mo{PgdY7V+?PPHp>}@^2iA7i(}r54oGu`DN_b%t6ge-j5s`3nmh2{g{nmNIS2qW^Z(xeY8fyOu`Y<=FM_RTD1Rv z1h!37E!aosH3_~_6jfr$90W|`^!{pXf8ri_tT(LG2s%X8El1!E1aCpYS`;yGehk59 zP51Iv{P>KfhL*gFH7}y#O$5y8tfBwijc&&qtpj_J@{Z=-R~*2^uT*`)ME4N{*ryKadL7=qrgL&5=Fw1O%eBqGZbs_q2e;_)+4TO*l9>=M z+fay*LM}>-$PJ+^QE~EAVb_rDQTIybu&XB*?b!xtG>Z-4RKU#%vN;#~j6%^ll1Nvs zbG)J*I&>Y?VTO&Vn`~*9EonTL=ufIx$uj)JKl8wJCEb%c>*K#kN)3y7MfYxgF3iQ& zTxAg<*0HX50xfnx@t@`o{^rk<=J(^~ck~s0U~}}JXXpTL2R@pa`4bB`@yGE8@o^Po zS1uN293SuP1fh@aOx$>U7xWaJbrv&8!vxk z+s}tUPn`f|Y51QS#@f;GPd4k)3!j+%`~M1$D|?hUZ!-IJ3TT>|InQ0a`hBpR;c;U#})DgIKf(p*o|7mVV#RP zB7S|zQ(9vp*$^jK2g7G-3jF8W9~_@%t<>;=Ux~0h%5OcVSi=4gj9a<9;0i6JagR2? z$Prgo64{F2lbQsnsWiQ39CGA*E7wq|Ce-_dW)HZeFh^o*H6y3pl6;Gxw+0Wf5ryFJ zBQ0cym?jzlo3cs`DuH!1tZP6h#rp4>{TR(k=zgtwGp~DW0Ja93Ni84NEK}uYh`mhd z@IlN*cBBC;$80{a9{(AtEaGHGnC0Udyvrg{5xgv<)?yqhUo|>o!Y}ef2ywXpF8Fj7 znD}|l$(kWqj%?^&hH8RzVWFm51LgV(Qx@?A8)m26S|)cfLMv!Vh9F7VBwKDqHlohA zJER-7{6-(gjF}c^7n>$YgU3Q~Qp}oW4aTtmSEw8DveHzp%Q)kHU2R^uR;cxe$$t*s z@;JCPb`Tlg!DeG`As!0D(^q?~`9-2`i?g?U7MD?|O80oV{>}BTw-L*I9OJFUldOI3Qz>N%qL8G>9}4FT z>?40zOsUuBhB?U0kzO;&dqyo@>aM)yiLeEW<1-CAUW* zf@}1NgIF7Csi_Rk9)^uLkx|>1n}J{I$hPBizAWF=BQHAS%i}22&T6{6aMWK%2Zw94 zd5<4crQl(ekod{8YqB_o_Jv8j^Hnc{L}Q|TB`Kc~9JUL%{u zj#>PbVy6fJAHX%=FnLqo?xDid-0waAt6p$bn*t%_f_)ylvSCyUE* zXupbRHS>zK_&ei{OcZ9JkQ2%u=@SR}Bql*(%C!nljH*v3pP7|9!NXDd_53L6Amq4R z+axNOjWh#CjOF*l$>N?!S*zkC%%9LnrAs@GE%!kk1Czn3vP=|HENSjcO0a<9v;&2`@H|<1sd6vO8HYD?3V`Fo zR9hyE)mpN@XgUHn>*4+{WKzTxVb>w8qtMp1qiBUS1CglHd#;XP9A zlRQo|&`xqRyAosBL&DeDOhOLar)k52Z=u8BBh&)y;*>Ylwh)0?YKtnU5l|?avyvC%p>G41i~`A1pcmPWRePeFt4XRXfOR5>rVR z!QKKL0AB%}OlQHDyV&znwUC4F>)!)8!PJi``TPLWSmk3dMkUh$f%wSmE2 z1etVz7b)k8hDKT0lX^WZ zXS@TO1J1p$bGYgY2Sx}>IMBy-eg8Asz!u_IL8i&jcGI=R_0c~0)BD0c50;soXDTMA z0Cse6ey|ytXF8}zz2u2``HqFh=H-23V2{)~2)FjI(T)O+$+MTK7&hBknL(O?;*$pC zZ#!u~-aauPKV5xI?L1ghj4QbkHf!#`n2x{EjK(j6y+qlY=`J9t{5zq0kU{8);dj$sNQ7Kq#@?$xg>A;(zoQU- zuKmgOy9j)D%H%t>=J?qAEKXIJvG?_6?7iJdGw%lsythpn;m4&2GQ)7n5 zg*Im^(-%dmePIsEb5ed4nmq#VvuMWAcz*s4`1#Mrka34r27{|maR>H3qH`;Gk73Ir zT39*2j4Ma!IY;l2cjF|B*qha4&gk{V!odg)*J*kWlR`{(F$KhVc}~i6c_+=tk3Wh& zZ2LN_<1_V)6xdT&ieADO1g>9MKW-k<8=K>~Qp+_KL&!(c%43 z8al;ra-UgcPU}ZU^(ju#lo2QK2P=zc7@0DSXUde+N;M$T>HBH~uR@URh%acxp`%F- zhfkfgXEKKq`e#V|Sd(R43OhQdzss~zu(Uzr!1OgUEzx0o)`Wf7kv`McFjeNSex z7mkcNdHUXSd^SJ&7T+bZ;&0x9l`2JDkawE7ZYxc!46#y#S4#!crBkY;HAkow22t|j zU?rPdr#it-hBq0uAHv_s7?cc))Z&>7WqgOB=3~R+5auNS%sKdw-vw!%^@=Utqh(%zBf(?@_&Xj!<|2|Ie31{$fYlLUB3mE48HB;e>5MdfhF|0#(X2(T@B;zT zDR4WZ)TAr)Z3q3~D<}V)%#f2`Kk+x8e&X+R<$(QL8N^pOBg{6!lispUyy9nF zauDbaUh`FvC;2=JGTGc(7VpOr9+_cO3tnQzQ)nqk ztt8M=tX6uhSyGa&CwRK7iAP>@L|PyxRY#$D+ne11?GmNPT=Evm;Mzn_|D;Rag6)M( zm8YL{&zmPG?0HT(UEb~{|HzY)9_+J1TC$$$0CU@0#N77AB(S%I9x*RovpT>^lj01i zJ58=TQ^I6#rUW}l{^^p}O+q~+DP4M}N_`91WJp^`2I9}5*oe(r)Ohrr#O@}PsGGUp z6bZ*8l7J09wB=qBkR0LXLdJ>MFhVf}Q+y-_lbZQH$HBvHgC*G(dw zWp|!D(L*BLB_$3El2PB8O)1)2$jyG~XKRY)+0mb;m#`Ulz*kbOYq`UYrH%o%1oO0GQ<}UUy_7V#M@e|E@HP4`z*a~jta%uRbEw> zcD5|+Cg*f!W2yvt$i(hK1w3Nb7K+D>3CN>F^wFmouuvm`{B>?EPn~q6rZWL_Jt?|Nh%mN1&>z?o-W~Z`Gia5`SRRP6FW%)Mz+0~ z!W;kEQI>e|9hI~N`kgISH=*62bUlzRX)WMug=&fiKgOE_DMH*7lbkKE-IzP(Jb5`G zSwR&@PLn`43AaEp>31_X`<<=x#59NOkDPXCEm+Xyd?938kfDOSM{oR>}>ly{O*N z1d(t2n+Mbe7Nw5&6Sile0U=r;T31>dik$hP#3L&~*K9ZEO^=Wk2=P$(&^8FShMmD` zwcewcMrxEdKK-~RhK!8a+;}p46k0!0^y!G>@0L=RuErmKW4T{7 z2=Rz-fgVXnOv9QS)Ftw&MtQikbm}Hn4_Ot5>SR39Qm#vv$*p8@s{EKNODG#g_W0-^ zKSsv36kob5ZY4g$#vo04^l|1XleyF8%Aas^)R>b}gBg$*+O)QELmbx7N0_Z8nyqc7 zVdrtTRx1?drI&{OGV;Hb3`&)&TdH_-J06NqYqk@JWu8*by-+01lHxeyZaTm|2SLBs zROGXi2qlma9}k*tP8}TA%mt^4r==`Pm-tq4J+J5_TA+-zFs#dAB}m<6fVQQJc&U$_ z)t!+JS2EO(cJY|l0=(I>APqy?U_%bR>wtlYQb04(T2AXG1<4rLLjG=qTsqTRA*;K} z0|llX?OO}9?*LCXJ>LSZicE_~QW}0wN7G&63`2qmdd+5l9@#K~M|N5g7G>zIr%Q-x zIwQZOTBOU282^Q0!?mT$MX7RTEAbc$LqFwhr8>gB5H0X#QEIXGZ1q!iXYuwBY6lgK zk{_grAWP3w|3x}!)Ecs)IZmTJp(RO|H5p2LS%zBFK~(2d=}67(B0M8Lg^%ENf*p}) z>^~p$bh;E%+G&Wmk{nFpyNSYUNY!>VM^+pbCh60!NydDd-*^-?M^YR0Bl#IvNvS40 zNRe1_?<}d~SEwljZ(M3i$x4^aMpfy~h8ruP_#sY{a*4Fw&G8A@LXDscO9Ia#ucg$a z%MzkTBOA0oTvZ!7vX-iSJWZYj<&%2HGxEe?r4h)}(xqQ3>B^I$=q6!M8rpS1klrc| z=O-hprFsE=H|kZV`_YCBq?{YOL_APU$YGBAx1?r#1Q$k=OJ-U`HA#9;>{FC3`V?b- zR-~&yt|0!2SKa4%Srpx~6^rupsUSQ8+h{jAK-o)dfwY#&E=fTWy;~1CFAhtLoGj$y zTFLKIWrtst&ZZq8k(QE}F0CnM^b`~oy6iS=&(^m_8qSD0cOAG$)8oD^RaTRMTw5!J zc3!?nqtsH@#_tAN84SC$o%+eaPlVBT{}S*4NejFcz{ zH_M|pp?265Ga6_5U#gqoUR}D(ZY5u(N?8ZyZ z_>g#mi{}S;tp6lRIbG!>m?~?}mQWr~O(qclr;CTEl{@8jh;nJ<(74FHDBrcN1(_H^s-T9dAPz@P4gt#i2m*ZQbQQ}^xp0B4t&K4dG z!9UOC^X7Psj7rk$g+DN=O}qVlx-4oX->1r+WU=U#nqy@mXl;UYi^LxEeP*W~1EhH` zbe#zq@>1k_zupU7%#fjtoZar&z0iaW1q>O;UYdGhMPqR2Rf`#Nwo=UYNU{v_tDj9v zm-ekBEmf|gxDv4hlohnzbX&w)Tn70p4C7O|ay2Q8)N~idxJheB4*GRK5)oG-cPG+< z_gyRar1@n;L!&2tjkDxTMYGb5_arNvAcPjGSdc|_RW$NJ`9t?X&q6Ns;9YHMA*mn~ zc$zhySEAub9v4GvV*(uNyO-KQaK#jJ7X`@+bJZ8*n{y0x#(K&gm7NNuhk|12QA@d> zGMO%!sd6#x6~&v7$@n;0LL$#(A{IsgyjGg#ly;TyiBC)e5nT~E$s?d0FyJl7NGC(X zhb^=}^uPQ(|DcR@RIbuU5{1$x=pz_kB&q-}r0Jlb(kC(@kSa97?a8I?Lih0%#WfnI z&`oe*sd7%T3Xgu0(S0j*A{1{*9iRA#AB6h)6xy65#4_e3DL#l6DNkPJS*M7_^$?my zqf!2oPLn=}PbY`tKr>D{(-;>sP)(Cyo)j3hYC#9YTfS)?xiGpXAKr;(EeMhc$WPo5 zPOQ#L!lQSC=gIZvdhj$Z$w+$~1(=dUyg@HUqs^PZ32V<1UIR`Aq19Cnd*TjoQl`Ee zpAzPc-?VsX@DK!=Yrrdv+!FC}G5i=0E033i`}J6b_&<8@H_wRZl%M2+4dAEr26+|? zFII87^Agi&n&#r(#hoTDPh-rLl5T&0+nb))d#6CuADm5ZOrTvgj|iH*GhVpPgcX#ib{Y<-H(6~mEqA2Lp;Xz< zP{n}%Gt9R_gynn|oyc);9&D=!d`PPFjYny7R1+8*P(7^;vJ9xckf54C+JJDHI~psh zUVN(f37CnHPXJwl=i^v+$Ir2DK@{r}R~xuXfW%0d0^f>`jE0UBs9(fF2Eo!Wh6!K{ zBK@5BiqR;O36xv1D_I88HPgjV1&ng>W8vnw`DD~*{O^%?ij%@ThKlD9;}OUaq!|xO zL&q@lWdo=P;eG;CCZR+lK%1#?_X$jO5}?XC4yd@8wFEo{?f8giqELs?YrNiZM{t;8 zV95mHif$^^Lbwu-Sh`UHfHWz}kUb~-`03J-uAH9NSPaTuv*VtW*(#np6`vtFea9dg zD3b;Z(|5E{DDn;+A2}0;V)_olRRSljfxK1saoiDbdF0YD`%ib!Sz$0af6VwttvxMm z)ZkMenTy~?=tLMJ(85qR8Sg|+DJx^!;B5jB31u;5Hxob z?*!^BDW@14n51vfH)&GQU;`Ref~e?!Gvb%U@DqyNo_pRj>JfLSJU7$i$g>`DKw zW`yXgIXdOEB zp5#qTFhIB{5kUh|B`UsA@PY#p0=B4NK$=!etMNA2RtObZ>ZS4qLji3CZ9BzF6)kmI zZ6?-2t%?vO7M!V(UadumChhpcwnVFzL2m!g-Y3t?i`uUF&sr>22+wn#efHV+z0clc z$*ZW=n5mJMs^~WB%EC>G#wV58>TNeXXUB_y1>dx05mplaBWv1u7grK@s`NcV>!Iq* za0adomdO|mFAu97$$^9*KTsP(p$nBf+bxd(<_~n*-&~UVElGTTU`m6Y^@kUse@m4* zEleI0CSytRPyU=4V0p`-xYVj3H^FI+vdJm961crYRHk6PAVY?RA$_%!&@tS4q6L>Q zfvRE^)@Z9IDW#16YRdtUHcC;_{GgSN?B1k2uUpQ%=s@n&XV_!FQ#OA;nhJafkp+2F z&9RHyLnyGbcMV5;&RKuO{UuCd95aA*JXF%GqH>Z z8P4UVIaca$V1+W=UE<=>7Hheg@_w7U{I16??s{;Pm#KP=RlnC$gH2xV$5vKE>3ev^ z?w?W;K)V$#&OmRp?#bU^`5J1sx(^TC=V>wN!pqcng;KZM-4pUb{aILU(J#H;r7mEJJd0IL$dX*sb2o^H!%+&QWqM zT+<-q{IXMcypH6R`8YSQ488a=S9@Ot+A1Q$Pc)Nz#7w*CjD#XXYOm0woBn+-*IO~&|?A<-)d%uppoiInwl8*k&kbSS#CGA!eE2Ekua?rJhuzvB$~G= z?-i>*0ZSQ>e4=H>IT5ZIrnXwWK^~jpa?J)C@q@(UiUGn9Cgl)$zI2|h&}@! zQOriI(Njh<_7#<})S8BYqOC?dhY?Uu~j&UD&v%3$R&Q1*j6`!`N_j#8a z|As5NaFeQ@RA@F(Q`|9sQ7KAXUbjObtG?_lrQWpS z2jfTC4R1n-q@JtLCKMgP%WM!C1)EQ@{zcrsppzsE+6@aJp*=W9HbLd2uy1gsbGPz- ztg5H%LN3R;dxKK2JhB>z4dKCwSC|o4GTO2#q&!gKnZBr69DnoNaU@&Q;Uu>Y`}DOO z8E$54i6)$?5<((phLJ>da6J2H%9&v^2IS%TRA0@*+BsUwkJ2L;pOs7mbg*8raRz>daOa> zU|2->Xv8Z>LX}v8?>i{>z8)LLbd3IQFALWb$N2IwBJ%TnHv_8rsG+wuu_pdIK7Mb{ zh~dzU(!IUS6u}E98DfE>dW<6MD`4oYoACNE)krtskF=MCPy{#W; zF)mz*N^Vy2zonrcp)j9LMHD^s*1aWqxSz)K)fq?VF85`Im1*%&X4#tQ=qA&PrGa$_ zT$COi#&~wMa7Z{VZD!u?(V@LEEE*GvlCP7I`P(Q>B3uB9JZ4BZdPE;xR;H4}iGE<* zn!>e-1CGOc28029)G{DEaDrM-w;0`}gTe}IJOX#U)L3TD4pE2<7CYwg(6DBZKHguS z1(~Pf!dqZx$}!568_prHqI6h^S|1u}2Zghp(pC-)cl6dtm=HrW5;GX5;G}RCJ`4Gm zQ97!R7WXz<8r@5a`>Cu!}p-g>oRgTop zkJ4Lyh&OwV^)#w1Mu%OM;px-Et})@~Q2PMA?dTe3l<5aO%BGu<<-}sC$6e(sFJXFq=SHE#q+$YzvIgexJ%{(63}mXTJ*;lg?+ zz~a6D$*C0aA80ah0h)${)^fAtQ^ec=fLPGbux?QJBoS#G5DXiB%pCr+|Zlsm989 zZO9z5dk`%Nf-}RtyaMNf1ouj+dZ`Z-Lb9?%YmPAA$JTU;`q$oi^Gt^X-}6K*9vy~5 zhmh7N^EFzBS-8Fl!w;vG^@%sK*`by_;m|FbM=)w&=sG?$4h={5(K>_x!Ur&p3(vZ1 zR!9!0p9NBojT8H;j9rWlBl)^V@~;{izDg_$fe>xcAz?Kq8`e~Y=`LWW5{MwSOxTL#g$qoG`O*>%>aWJZs%JY#X(EuEqDmrc$AoK2!ytV2 zV?#k@C}GPEz&lO@+wo8P=s$q`2)#Zi{Dtj9nLLAER;F{0)^Blg`-Q%P!_SWjS9>hW z+#ik@3h^3%DqbN$HYm&^o;H~HiRHhH2+IQTJuK}J{lj!#@p%41=mC(tpygYL9el$R zG!w^=>4e~iB+&q?vZ(59SHm*L2MgbMx}>s?=WcrG*PcFD=_zmI;c%oAg$;!F3AUVW zxJ3}%g>_9vl0X~}gGR^(j2&TtlpBv!C7SmHr_a$QnTarz5GV# z@%1yqErY^9_-K@B+42AlgzFJ&w1hhnkE27Qw$9QeMq7)S09xfA4c`k zFhYn3oL>5TiKswW(pRtb*MOtVG?L~c#U9c@yi<4v z1)_^zLjcB_rNOF3gY=i(yd!tU-;9GC97f_#A?P=^pBAC9L?eCb)?RtD98nS*Pw7L5rz}=X>3a;Ld!~{-QqS$)}Tj7pzt9(IJKe^ zIV||1G&B+6KfOl+K*QV6%bu#(Ri^(wCe)K(K#2nh80c*=c~0Rco|Wr9Tt7L=c;QV@ z0aV8strqfRvA#J#mpQH;B}3Flzk)oHG?V3w2CEVSCMlNq1w`_pNDG-?0y1$rrxk7` zV%7>^k}OPFP6`I*DtF){Bon5u|>vlLk;PoR`QCm03*!||~oSwchv$!pFU^Q@mL z35xcyYRRVlqUy}l4SBAkR8?%KE8){ffK$v+1P``xVhix~Abp)`I*7+}nw%+%w31as zzY{`acNExwY8;TG3;q2YzTtf}5cVK8)p*uVnMH7Y? zj9L`?Fc2T$Rs|z$aKUn$Ftn)4FL7_$ppDJCnWz+rhOK(~d2R3}0}U^F^k%yT5I#yS zbaNl4I2kwdj8_Q1`j$?AQ>VYJr@iA!+kX9YmnPF&9Y2@z*{dJMfXq3z{XASY0v9-p zbL?Wpl-cIb;#T+ELvPU96{;jqgS&dWIX?B?L4pAdIIZQlZ|+;=;R+GJ;ldY=((|f} zch7%sv$Tf4rhYh{J5~Rd-|y(O5Ef1rm**zO2+3ZJC=Ra^CxNY;Ag6PVoYyh3!aP#g zqg3FL;nPOxlp{h140vRCDaub|9a`HJ0X7(_6E_MaOVBct1T8Z;;YzIE40%r%KFB(v z@rWaHNuYcvGn#gz{Oc5v_O`(OGNQrmPnj_HawZIk^4{8mek2z^K=lX*lrk(cN(fR? zKl4g74O7`u$@>Rsq&o`)ja!lPCulUuh_dkiqO@^6o*~ACoCi+qZSregboz(Up{_Fg z_4IIx%S?3i38OT}EfDkwq<(O47^V)-{G&gBbe3l**oBL}mAFxC4aH5|m@FQG0uC(= zmB8Z28Pr@91t>dQydh@hAy`_YNI?e?USQ+L&;-mYSN)YHOt?|1hVc;~Q7ptH zX%;+BrF{@!a&6fM(5mRE>_2gn<2syF_Ltcoq8rZN+26z;kQ<!w67@su z^CZN>GdK|_f)A|lkNz2f;eV-NMi#N!KY9}EA$c(NvJX`1rnL5gd?KruPXGg{vqD_t z6eubu`ijNKiwRX@ltR^2SmXTzb<{`nu_7^aUpP8c;dyy9KH%Iz-mnIGKUUqizRMe? zs$mX>`zd*SP1%ux=Zsj5`n2jkURX8U{8s!OoCPLO=S7Fb3(4TP41|PzQkn#ZiSFXq zvIUH1h#|X2m;XHh;rL5-7m>yfQRA>{e#lZtu1eyWsE^_+_z12#XW*$tA0eg1^mK9; zqqU#;>G)YBsY~fx>VGQZN9q4V2hkW;;Wz%)l~!ZgZ0zZ2@iX7zEq=oy{Na}q)3$60MtE9R@|)wlYq77e zFs)&}4hxq158Ki>n=0_%vs%5c03^W7%6!p2N?#HGCqMM^&sF;a{PN#uA~>yy3?Ku%^_ zQ(Az;r@xWbR=ROI21cg2`_O}s+vxu4>+l%AWEVVK=|HkVo-fYt`Csr1%wT|z#vMfn zxA<>HVk0>KJizat@)gd-qjxYqo3>US!1)LN`-HT<#RGn1BmIl)oo`k7A3ikM4|=8J z2VgidKDsz}2O_0)>=HLCcFIAe>7>9)CM>s-Q#KzhCf_!T-J%4-`fRu5{ z>72P+6H5!S>3upe`0?Q>?i-Eyw_{d4LyreuS?NY(vue zzC#EMJhs`vU=bdx^KJ5bRx5R6@&F4$36L~jJnR(1FrQmRzaR(o2!C<1!`at?0Y9w3 z7vnFh(t_W=!ngdHTYTkP{iA^={g;z{O@If(ya;9{CVK&@&!zRS)9vcC9s~XaC%~5H zm;oM$fqij&NBIhDoIgCk(E!h+v@ZAGaB4h!8jVx1n$jAY)>*y_&JhgI(z)J0^h{dp zi=Q+8`VL3JT2_cctkAB#mlxa3cQhJyc)4SY5B<9z%CTwv(UHKK;D3bwFDT`o=Q|iY z38;_zGpm!+0oot>>(%}@niS)o$b&_~+44X6gW}-H>qKYc(n~BgK+24qV95%bHdyW{ z9x@I~HmTPJO9oZ9*=1^mIWktsk|TN6Qtzw9;ksxBPx34w*%p@iw54WjH5O+A9*dHSW~* zT`EstSjg0m;_&xLc1OO7uV1PvzRTB1w|&`e!8_}mdLE-g?cu{6=q-aG%hl;FV?J8Yh3d-t-jTgcX{_}3MnTtbYNqrXOkTvAKR1!5#2d-lyNutt#_Z$b4mZ>k*ay!ZyWiM0?j?9Jm*?9%x%vI#}rn6P#E@z56t+<(=vpHz1y?-WU3D0}yOtPpXmn~&Roj|*ax(s!AO*e7G z#o;i{_iU9^Yu7Yu5=hI?ecmS)UzOELX8Tarn6%AU<6O!i=&Cw&mA<-G3!HvBHd&X> zzRliE6z*u(e$KQ@yWg;r#aSbnz5{IgNm2i{*Hhtqx8Q8+7V;OqTdSMmM?a^Of3Z7b zT`u?ehtB$uWk~07(KY!*Rl9ER^*0rS)98=t>lplSF)dHQcCy;QEJrh%_F9!@bf2>} zTlg*qu}WvH(b~1%$90oZ8>}0{0V)BU$C>lahYtAY)fC-g3&LOX!dY-!Vc18~-N<~J(;g^CoOnP$zyQ*|0S+b-VL|EIa|wBM<(*1*{+T4O0@lAHW9 zHA&Ze+`h%d_CH-^dDW%YsOT1@GFHcuq45RFwrS~al;5s0THQkGwc0vMKdZB;-}@cg zr_v9ZO!Rtvh9}#dM{cu03{?msrLfH;dB+Sem_J9+#dp>X76k&|4c zYNYXWs0-5a$E?*M$I+~Or;s>Rw%+ND_Ig}ZrUUIjm~M{tKpq*r(&eg49x zPyxpE1);zJ7)MgLI4nQWn#Glmm$eg#dk3B=cG=uyQLPru(g=qt<=aO?(Led$onQ_u z*X4Jp_tGBq_vjkcgTu!x%TG(XeGbFk$JIN+uvz5^81HoHbB+oinO7JZ_o_8T#4J=M zFl?NtRDu@ppI@haq}^e8hv9gK;Z`&SI6+f5`=^#F<9u2PqpeZ-_pM+imB6!ct-iHh zU*4qOZ%`Fu7h;2A^75ON=a~7Q*Q`guww*p_%JD;?$h=gTQfwF7Gme@4fK;oSXIVq< z16Ns79klk5faLHTk`)B;W*&ZT;0mG$VZ z;?oHd2GV(By=t94ACI!j_r9@9Ux`$a3g;Jx!tr_`If$o_6@2tOHS*nBT{cU@>$C@6 zn4mA(!_(78}4IPrrRM=a6V7LtfsY1y+bB^)igyL6UQ=}3p69!b7e(^KIR@5D>F zNTiCtCtwV&)2;-DKRXO18MV4;5tj9U@r|V#2^hDloY~OH>AIo2>?JR(ntZb2MqA(>Erc`W1!24fe8^PL(w@`;ERsxJGwOPzpVxd@rXPR zj&SOK!1ngpx(+IW{Ai+rhQiF7tx?#?fJY@27Q25dWSrSi@t^DA$3XPhMS4{Fv^tMs z7I~5@&%5WT8v%<*zs*|nq3*xsM2oubi0c0FLv($QqIZi0Xg9kAn}eS+UcYpNEZ{@6 zno*}46O{qX$jrs)Sr_A_i(QOk2QF9TQk~?;F?q#_0PsTsKR6Mk9*r;ZL$Tv%S67$W zbyW$gatih1^}2ub2LI@(-je-02_<|HW!+>0UaPP-mg--yVeV4i11=N(%?6F-J*2Z- zO>i7U{`>m3s5L@aOs6F{#Z+B7f2Ue^D?d3DD=*|wibFqw=i~L)L{KyXz;v5eG6m&Z zrx&7lbkP*I{}ui_ls0~sD}q%Y!*W4?ae)^sZj?UqR&dLH;*uM4JXW?Xk@*U0Md! z?bay=ROEzLo(eZ5UwLYpw!0qWLo`e2(awILO&PtmRR3?4PDQGK)zwO|DX>a8AB9`Z z+R&zqWAf21T?`}d)(;NIILNDZGpVqT?a-LTw{!Bj*0g`4OIpy#=inxD3FpW*HBZHU zh~wB7UwKfaRC&2PzoS`aBz$ytm$oo%VYgNtP%$5Jr0w(dU-9)L3t(pKPKabh#weuR z_4p7LkL2+^(-ae$^;5*MQ$>kW;1OM_*san7G8@JhpW}HLE7`uM$;fii6W*jUm` zyJjtA#JQtlWtXO4EjY}E4OYhX1~83G@GQJ15r}Il-MBy5wb@9sDWtLEbPadKe%ThRYp+!{h0kX9I1S zD}>{A6SbGKw2ZkVxB!dssb8nY!-Kk1PT`xbr zRjJTatt}4HzPg@3O@`I(utMAnt6@`7G)vFSW<5r%BRPu;y7c^RYyD@MltnO{NW4~S zwWA#lo#<$XGHsH}-91(!BRTNtDw_?-YhX!APdUnhP@c0+q}VbfRjn%PG~L%hI_==> zT(DO;ie7a6b>wC(bDhOu9R$V+AVPxZ?xwWZd1M1Cz&oq75)tZHs@kN+HnYz`^$)tt zJXp;-a18_CQrtiff$g}une4|@&AcAALru89zQ|v^$oZ5|RVD1n`V@F$tp3K*&}A8w z;G@BP6d|)hMw25~MiYX6!lw?c`GbzSLm~!#xY!RdTtLE|@#{l<3m6Y~Qfa(a8x!{I z-fY1@n--BUX=_1P2UOFvzt%M7u>jy3M@f}8>uc@$?Q1r%vmUU?l*9 zk5;SK=nobZ1RKZe@r`3SoYx6tsEgj6^h3j;P=5N}wQuHhM&8hCs)+OxmwM@j2HOfRYJntx3gEYZ zNnd<&oz0&$dy=;zJ~tUVtlw;w@N@uUd=enB>uQ0YM`~^U>BJn17gzN7Xx=ny(l(z^ z(5`D=vUZdxO;n-9RM8I13F3#@Ms<0}y~`}L0{K1TdV$*+=91G&)PMQ;ED?g|kJ1qj z&(lWN5!;sQ=_Fc(r<1ove}2lM0c#Q>JZH1T-1ed)c4~M*Xl5^kVWLNVPwKCIG=8x606lxAr4GW3|As%oZzc@`L(`~qLrp|({EBvF; z{yDE%{kN&;6@&0mhJ&yvU#$xT=VSnc8|RtrT}50Mfyv@7;*N@jbhW`*9s!u8!r9u6 z>T&0k$9g#!gn!{vH0C(I8wH&^P4#dVMk6~Vega7#^Ji)L9QAeZVsO3@mQN@F{Rn0v zcm$vSmVMIU8Ro}fc*DnmJP=bi*+DeT*0TF-yjSXvqCk&(E|?Y;_KX$G0O zmRkO?QSlvjc}iK_16kK&F!MQ&a6art1q8E*>Y8SYa9+RJXr{wy2D>80jwYhVt4$;} zfwx)ulas-I4)=T3lW+(~Yt~3t)@P7_cvEAXmkBq)IHmr|UnAwr^Re%n?aG@OfdtrR z=_gT)I2lCu5G@wTsR%-vr48(Ko`t(1B*>%QnUUZ&dJWp(xbI)(0Z;4`SFh!-n8+H) z^e(D^pkCAm53@*QbI!WmVzYS;Sj@t(ekzgyN3+?iwXFrAm3Q5I0tR@*q759xJcVma ziN~*)lyLJNq9S@`Wlr&w0hkYY5Yj^Hc{Tx)%^LeWWC&jYFD3dJinW$DzwJr zwfbTrBI0qs&v7S#%DXtVv+bBaH$)jQwH@96cC-9cp#l z936&ckMfYyY}0TA={A+UqB1Vof$dsV*V)}7i@~n74x*WeM)2&|tEN1R72sSJ0ZWKb zD1j<2yiR|>@W5m`g{ zr)-hHf`HQD6S(t$2PeLS7~}!|3sLDO-hpD!3b?|nh=#)7sz&)7tZAd89(@+ z>b>s4#Ut~R%7%QgUN*#In&ZCSJk84J=;VxU!YN}-*F>kT)c`-EU2U4XS)Xp#s~#sb zc>+Sv!dSo4EaVOsST5!or3jGTsUiyhKwaEIy9($R3bc;=@+gyi`)KM>wCiW7h!(s{ zw^b?%EilWbUZlHeo8D!!OHWl1t4#KLTK;BjykYgNtv@`|4tB9T{i@d~D?|DMsDaKYt-7XT@v-xKkUgvVJx zd(J`EfVA1#GFwxqGvo%JOG_v=0!*m9$EZv6DG}YHbC{)kp7vyv&nYa|jI3;2tEL41 z-`cF&cKyCf8$B8m6QpK>IrJR zFQb+G*Fzdkw5QzzDF6ef0F0xQuUD~@ACQeHgu0xM?DzglL}foHEpEY2vuds7g;Bnz z0<&+S94;Um<-xl0Y)U;AgJu7Da!zI4u$-P7@qz4bPVo}5>{}oCLsA`0btUx%z)aWTHb)ojTHZtP?_j&1B&u~L#HMaN!Np3eMLmpqDgR#GL zpvLb<|L@+q@Bj4H-T$YzZu>vI_4xnkt>4GDrrX4d|6d5{d2y=tM>PI(%y;X92>j>I z(}ViMul(;$>Oyvul=1Mx|K`sgf9(2?dUzkdM2JzH5~+8rk2K|%Sw&C&db(YPjh@TF zs8MbfJELs~v6#hPX#<*LzVc^eAHHijvbP^LAwv$@f=Dy~r*_3lN|w3d7gO68Z%;f{ zq5-6~cbH-DG-BaB%WhNqfvR1E?@(dmmU92xNX0T^xhle+{m(f}ec}!{o2m%G$4k{$ zge$zbh!UXU8Mmz?M>L84X6!LUNPm=m>mHMH#xJwvMya)iif(Sbw zNbJNBS-IJ2jysWHockakbbDiAaX1ob=Ac?ch(fq(J&Hb^s+(tONR2k4HEOk&(x#Zy zc7S7gZs=TH=r~DVVKXLr&_ZTpp{d=^+@?madKv-zHzF`@HA6xMeyWeeM z)@ila(a6X-jIrxqv%UmXU3!|x!w!AsO@*8-42)X{Hc&h4wG8+h72&rOW%+h?PEfIL zr%$!s_U-JYbw-UQ_;yIpufa};$e*K|T{7(Ty*!+3qy=Mjq4s93Ro zfI3f?3>zo)j6DA3Os%TXu@P*7Y!Ux+beY3^mBZW{m?=qWFtxSU$!0gc?Gkk?*9c(# zwmYG2T2}%2UHalZ8i-K=@Q)bqi(0hDkw0*qu6bT5hy1E`eT8Jl4x0xQryJjl&*+fP zbA%Jmd8;DSjWC2S8mkAfhx*?{1XnmS#k4pR)X-2 zSh)*zeMVOTJg$x7{3^$}S@tZfryCug%oh8jW{6FPf!(kvtfP?4X|Qpx!>;hKYdW?>ds} zZZpDIL)1FXXi~jti#n?CEG(i|!eQY#o+3f<_r(aj9OY3*) z(jEF`0yFFx@oesZ6UnzK!rF2A`&gYnK}8AVUrZpMK2rm0^n|C6%j?tulCM%Zu`DJ! zIq>Gw&C^M=;FzyH4*!sa`odSOQmD?6Je9Da!~ZCrGFx!QoVQ--7QN@;-+gWBz0TtH zM%B-m9+^p{!A@m%XmSE}H>f7IiBt(8zaktyPJ71cbS9NVdUleipJ_(I_*$JkOOH_s z66paT=jg?`y2RmRjM~i(BnDUqR&J9GT-Zq}Ct^547_pU{)N z{h25A_Bi%ZHG0HhBk9>Io-fWw46Dx3EpxTl!9!jRZ_uj?G-aXACD9g{-E~BLmXGdI z={*)dOtt8%&AOiYv~|`11fkkWXa({ELd-no7Imv2d%g+wh7I;^7}ywt?N)eI-!Pme zfppkQYuj8OE!Uu;4wZIlRz=98z?d=COf{OTVYBs#`MS43)3G!F2K;pDg^F;7PXJ$5 z5!PVqG9stbO1{4~&i?K|%5=b70yQZTbXSG8#6I;(2t zu`jR=Hqs2$J3M(ERzA(|NANuVHCz4XtGq=e?7EX7s&>P=e1@$|MRDGxs-moyq$l_U z$h%zNP~L#{CpSSA);K-trVB&F>K*6sJ_>sC0!u-r7^a4RYB5vc+AXT=(DH7ZSM?+AS91_@{PD*GJhclf3pg%J74^PUndAE?#;pRba?gWkkE>c^jF;DNkJ;6E3bCr-5Tt zJ;BD}q^k9p>yh_qA>~LT2uwhuM^gim6Hn#}mCsjxgE|)I*9*0b^oJW5NW2svfw3r- zVb~w(>h*f8MU$KLr#5|Sou8)XgC>({azhT4@rHx+%}(ucD|Db&o?AwY%48RP`D3l; zQq&y}Ow;>QHH<(+jgInyij(W~?HtJrvmo^9=j*rz?OCAP7OEoJYzb^;_E19t;gN=J z))7(GF>i}x(+>4>G1A?wGGc}iOT@TbO?=aVDH*FP99J2#rHnbNQE?)HPp#APEF^Q4 z4pS?XV>FJ!+y$L8Xl=ct&%u&M?6rqfJ$769u1U7{8vshTyK zuTvNdF(=zcrkPU!8CQhiecT`6$jm->SnENlQpt)hoT<=Z9d0-JN3Tr6oatWI1Pxnbw-#y zRTEu+ygbX~ZAVsl`N?iypi?uVwgioja4Wu*zs~M!j;`QyE&*y6YS>r8vXZj}%@ z*N!T}QJgzZrq$V^iJdl&l{+{?XXx{@6&mc?PQi5RnpNdLm(8#sF#3ppn$dW(3UM?_ zLchK0|8vWrndqP-$<@;sSPzZE6^y+?Dn8H9(i)52(?4(%5o0&``Yuqb3p(RdS5Ldv z>0(;Xb!aHqOBB=n>73vRipdY_fKz5q@Wjy=YrG%-DBZM;%Pem9qL_{VJo070zeWeL ziV3ac*4jZ=I;)Rdg5|o>y%UKM^-&%-SrS<>WU7tg!Z=@Er}`N^xooo`8`Qo~;j0?R zp}->v(IU)=R~*@JL({iUy;_Km=Ru>Xj0Mh1J5ja)F( zWbZM=gt^YZ%t2pyJS&q@Y0!TzP(8VC_Tq~dV}vxDDc;boue+7o?PNK%Q-5&a2{8>E z=iVZzAEcSb87yW_>^c!FmiWNbY0xnXbn!w_x6^~AW&ExLKwGCC0l^OIpWm~8t=PkY%%!1wnZOr z*RsT}xVKZwkZg|jP_1QtrWu<1)#x~YpJmmLB{TGGhaY{h%(rkS@RP(}Zw59M0(PM7 z8tkXJxLe=r)H%Re5f+b=SpuG+&UY`w9h<~KmE}7;DKZc(G0RMTl9Xr zHaU7<@V#H!sn5IkJ(o7!Zm6SJ8!_E0LO)J(wsB$#QP3e=*Q+YR@mTLT-CRgS;vo`S z%+9~O!>-~Y5?7PPb>lpcq~HNkx_(8malTpC86L*$=`@q6e1Wz2tEzP^36JoJyKE3A z$HW~py!+|ngdpvh>xtlb_O0t0bOx*hsqk2f((5$R1>o=;#GKjcLzmRJ*?Pn({Hv zI`pcaAjZl4?}?%Xbf^W-@s*~jzY{|hN+gFn+M9fa<9aK57`{ahJ*WD1_3F^(ZuRff zgHEeQum~b|D30Bto7#08#mNbIV}1}PVIzx%dpu{FhEKKLo~<=TDs1OS3MYWa zIptb6W%1n2%qB$(<+y?IuWnSEjnScr-R301GNlNRitrw{svR)U{@NT(XPx;fZqQ_G z6|d1EgHKaR*klBkF`u)P-{ro}YmRDU=}49;HU{j#5+fNsRbRtlarwN-Yl@I;{gDZ) zw3)RfLTdp_^ZFt-B)Zzf^tF z7I956)DoF+saYv7uibRst2T-jWchBm>?UVlaQ?!*uE)44O2t~t?`ud~iYf8=oE$#&Z@ z{g*3xI+b8yJ zhgv(muw<{L%s%J-;}+Q55f%K!i$D?S26Q4{5KOBG*J16T=+mZQhcD0=NM*Zyo$Ibwp;h4{v}%i$p#ok} zNyyw_jdm%YqY484s*c$W;tz1pFRjoN8Ax2{ff}n>f*y`oAZ~9p@fgmpa~X~gY6TK% zRfhHPM(eupgmx0hdPq5Qf^GRmFwqYy~lfvm!c)Mea1I9fa~p!_G_KwMnG~qjp`a5 z`HQ2{q`Gy6wZ1p&Yo3S?d>jP_&1}(Usnqq8!eaR)XG$@fi*BpSLT=Yt_Vbr>dX!N= z=+m_iVhR^N@4{+mja2e2;bfU(H z!mEOjnK&OwoukjV^e7o`wKP;%l{V;8MC@JNmjXa^g(|{e=+FxhcOg~U)T_NGTGHe? zZWX@sSc~bE`>xT61A(W@^5tQ7jF}nQ<2=5G zlY-iJ7}sn`C_F!*@Tf<2MY}Q4BNMVIQD^FrPUIVs^jtI=&){09Q~1+|Quu3Y^(9v# z2#V-CUq>c)^T9MUN%e#d<37TZ$%uq4Dtyf* zn`6e?hy*n6e%X`+iX7Z(eKV17Wr(J3wK`eOb5No7D%Vr@Q)LPzBf|lhkOj6`hw_{O z&EcvDbgG#G&HL6S&e2NXx!OFJ65355Hu2h?>!JMquB7bR2xP-o5J5Batk+L=pj2Ux+^AMAonE;g=$m*A}(4t2b^= zrxrT^h-IRMXb=(~Q19VXv(s&+!n7y9T-;)k?19uu3mu1t-4F7v6oJ?;1BOlrMW@;Dsm8SR#9Mi}ms}FNfXJce zaSDFik;m;?Xp4p7@e^YWgSoVDN|N;7RilnXhIePldyei0S9D^eKJZZTCfA=$Jy#HO zh%!6|x7VnCtxh1<|AgtINaPnfk=J^Fk#nKAdeIKOo5=8(eV$hsUhxAmU612)mrnhW z#ql_cCg|CZvNYCP7KWyZ@HLU>}Qv0`|q`xCPsTq_lSa&D?-H~gOxi@ zTl~pqT;%dVIbp^qKcT3K@MRW6oXxp`XO9-so=i(JmgRXG;Pi@vPMn7mr!W z*|<)>r}{LZob7G^RPE5065=_Kh)3i+T9^i#)gb3#6}gt5xKpJ_m`+W4&&szZGQ*`J z{K^jn`2%mB!OyPOIA6RQJRqp($cpe4$1@_)6d#9${ZC4QBkR;~FOYc1&wcY86AOq_ zv@QSJ`q<<42(jPiB*+zw9oFC(iYG7EAdQN?hX+zx@GtS#;57$(?<35nLXHc&)L<6M5O2MN;5@B;>;lCKsi*ZFde~NktB)`v2WDi2 z4HaPnjqC=PNTi~*5k+vi^2X>_qpeRDHAq8f*$so!m~mwI0j=uje#szC|1T~e&=q;g zF#2bP%xPv7;C_CA24BMsX2_c!Vyh{6g*#}O-q&RE9=M<){KPl%v5K&n>e-6$mx|C# z@gAS92v1YX=b|`ogmKLrgfM4O67~SCFSU?va(K8E;TQS3BCT~5VK0dTHkB5?gh7Vq zE5vHXG1@v>t!J8*@^@b$X4tQz>~^${KGTvo+mY}PCy{C_8G5FnRcRM?sxlPfo}{W% ztaBZkV%d^W*4hrl4tLZ|=1F|C3X1>$C#M7O7Xd(DL*s$d!{~}IH7!UQ!sCB8RfN^} zkE1or7o5r8{;erNMfo6yIp8)+odF_NNPL1;vi*^oUdhBj<{;~9JWuUMgBAV$3pb`) zR7UemddxD(t0WwtnRanFaCkUS9x@|ClQ%yqJlhg>ZT9lVm( z1^x#W{4T-93X^qE$T1b+bXTVQo>MChj6_VZT50&S73i?)O>Xz}4jBl5QfIzcVI7R~ zoES#&b43{98u{3W%+c0QkDmWfCd)QajnLP-OtW#e%zpV__BGf#z-JhNNAGKP#CFa! z0Wuj;$>Pun4gL6y_X5q(X`G|yP}mwr?+yJp6mZUAu_F$J&q3Z#5L5LH_M&T?=aq)} z9=}i#PV`vozzWmOyw%7jm>Q7QW%;^}4ts!CX$-5R@W|AXeh!fH?6@lLa7+m>SU!#G-5h|=y z(P(EY*pPsRpE&jZ#ni!$v}qLJ@H}63IDRVz4_*5CoDV+8cPnTy>_Am{WC!4f3jpSb zo(8JGX2g(Icl=!ZN?hMD_C;?Q$^(CZ#r(-a5XFHa;=CK)Dz~Ys)B5~LDoG2i2bSh~ zV@yyD*Ze4LC>_SGfzy9DJKuKKXN3BUvERSPN3*sTdn^pg+;=$q@BoW}G+u1Q6r?A% zH=O(<2NYZTCTMSRNC1)#@#+4f><2K(yMWYiMA*VO(X_7jUye-c)G<0S?)MZrZyUx; z%JFHClkB~GfT8^{UkW}g8Ur&LJuFM>p;1N&u+q<15VWUtgCl{1U^`GIEWl3RMAth_ zGP9%HUqRZmI`$6t$A0M$0`_EojME3VtJC_c3t?!6ukf2menw0Em-)xh`P1^G>I`SQOa8XkT>wyhMCShst zO0_E8z&NeOGDnN*%qrV8Pv2{>7|4w{8p}0_W-=|hm{ji;&Bjo~Fvtt_Q7C~)%$QW5 z=X?(%N5tl^a|T`pfhXB^V5{=9ZcXby(t5`kkpBWLoErF?p4PGad>lPkjOq^L?45W^ zs@SSBQm7lW%qTI;5%@cKEUo1U+4tg#B8JdHO~#jZ(Pxan6|JR9F3byQTA+ma< z)#5Ni8yM9oZ_#;U+SY7Lqzz}pe?81DPO=AJ*h*)vWhdEoMyB;W|H|r=eSMg(!0WuV z7{&oMuH+;n_uYSVUio6WLFj9qqnRsXoIa6GkPy=8^w1B5h$=iurDIHz=ea4uZ3xJ9 zB@?Kn;2Tm|OxEOd&(Y&ZuP$GVnX(Rv3B$452ih}VhEprR-G?W@P zvy0`d-Z=@OH|z><5*szXJI0JF$gD@GCfaJ`Ad{b`Vk=@x-;h$Empu{Gh)GNY|lQUGlS2WIKP573>S~ zYuIM42ZQ4W*lGaDvOEyAZM}a2l7k?pu)&1POtSg)F~*(eVf^6l5OY9gx~SY0hk!GG z1d&+gVsn_Pj#uf&d|%EKfV~)+jEIdWO_nm7Az7;3S@LoRm~$^@K}ReQ0&T{(5ASe@ zBb>_`9Yw1hCp^r#fGZRho2pC>eNEJ%4AB6?$)Q7Umtv%OH{%1x>9P^JY>X}*ExN0r zXgVNois*o|TKy5xlXNl8-FYTwuys3TSu38uLiTeklTPBYl;SwS)&I0pdN*S%5yt> z#7MJ;Y4*-Mg_Ik^AOUK*$$KwPaRxaRIqayO2)AM}y5NN>5Iz`er6W|Dqv310ST=;EmrfeY3>UqIY0Z#}siFtv{zVXXsBi>AD_ewi-I!oKvVXC zavM*B-_-NiX-o|kdg{bDlc+S5^k=G?4Jlwu25iM}aGT_kw(%BB-YGjZG>#5ujS7rz zK?qzv#wG{t!!j8Vs#uE8u|A)}g<&=aKOkg!fdK=aaS2s@^nGUDF($9C>W|So{O>>i zZth3@i}if5*oH4;Mrlv>(|wpa9d;iRb~BWm$pZatUQZ>X*pCk2gxNrG1lXbh{8Ab8 zzR$Y}_`;BJUj=6Go}vr-*z~l#!)?k2IO)|_3_Z^r?Nc_`K3n#wTZID@`sh4_{82Vo z$ENF^ViWhOPWF*cqr-|(p^6zs#Wq5MfeynN^M!mz)Di!fnN&@$hgPaaxF~x5?oY3CBVjRjL1BDlZ}m zWRz*@Dd|Ugk*BcF_o&(()3kT0UcA^M;W4c?2l;W`4&zj=N`AvVg>LK{+?>X=X*+~38Dh|jcf^8sogm0|VWf9@X{C@!Um&x|J zs81p4@6hQ>^;!aa3gErllG~SGPpHC<79{~8> z8n{c1z6E0AUrL3`io*{M55)=aFFEi(&Eni4zo$-zMaVDnAwFxjo5T`EkFKP?sRFAl2`uv;DMo{B6G z?g3A38t#gBva&?(X|?V{N##OF-Q&g);V0$-OzxOYlHP%8KiH;uT@JxmoyPOtjVhA8 z1Hb4EJ+njm65yXM2)BUtUDmlPcA4O|Z&50I#(R)vNj(x;oR#zpbu+2|lXO?*U6c9C>0%l}Z)SU5Ar?4_!19A9i zNJxSs76#V?JYMZXx_hmf6S9G4cVX2;$hlk${`M`_csLzoxDcvmCW~X7K9ZEd?6SVJgQIiF`KVUzkqq4Pp-bDI9 z7UoDW3C!Kp`2e$%{o4uH+gwhC?j5kdU#E>Z zkni~@$b0!Mp@SueOEqVez5>BGG_qPu8h9XCoYeb&5Om z2q+{TnecCFss0)%$9HkgT0N7{3|H;I|9c;x_GA~=$6f5utH~COY_NoPdo^rfIJd-l z08T9q_Z%LuU@crzsTBFXwAu4lQjkAa8Na8qVu2P*v5FE&NIo(oXRJ(EIb6J7O;mt@*;N14{k%jSW2-lgl!uO{hKFqWt)LVx{K> zbEw#1BZQ<%E3FO7YfvByItAJZMk4gH!@(i(%7?}$vUf4<4c)Yuu|kNceRrs7sYYR1 z#3Y@c*G+4+Hmc@C%=XC@P3`)2m!8<6zhOww3$9!DqZ)Q6omBBtPzp3@Nl42aQitvD zS?);aYEPY|sIpeXN2}v(O_2lKP3MQDdLWTYtq$NG0Ug~_yPm+`qoWhZ_jT#!H*|~h!srBklTHQJinc6i z?0hOvUCdV5hg8Ph zO+B@O@B+qCbCws@I2E~>eeYWRFzY6GASHSViwFrwNKF+g;TCIBu!VByxHhK05 zCigp0JppTvFn^&cZYMdEmK?gmWO}Dgt9&DLlBBX|%SRmt&9%7rL&y}0dSR&^juiEP zs*>Y?7nmPGsbTaKBK+xuqPnqNlXF4+VS%8k-iU|tj#AxrDAU=(yGrfWQO?o03?EAc zdP;xMU7jp=yv1=4%dOTy=~Qbz=KL&o_%5#WUHmymZBZ8TP2U0s@r|YWULsvbI5{1= zR=ab6-`J)eV>Nwmh`utWDJ6ookI59GKE2YM={FD(f#dgt@=!2dXH zblV9+ir1|6I7iThz?7rw<#+^?O!Vjohd)|?xoQ{q%NF}preqhrmugtDi?O~7{GC5# zPa&6(l7m1^o2J;V!o(Wc=DuemeOHnl*$v{|IxCT@+e^Y5sql-K9vWx&Lva(Hl57fM6)c>#Aa&*XM6mOsxZc&v<|!a`VO}j zJI%Ytexr5s``kLAIF@LGqpl1~28WRj>hz`Y_V{;h)+edd&9fU20Er=&o~o;$F&c(A zcia7-Ty}tpR%v95nQ*tX={XlQ1Z-dRT5({A+IAVlKrG?|LY{rGvY#wYO4hU+wnQx~ z$WmLhgcW!uW{L@k^VX1F+A8%0oJzx^cM+Pg}OjESZN?oS7L&8;}x2decj?q&u03K=?O`#|hd-cH6=G zSyn>K38QsK$sFb2yxCyEta`_T^kE)FOG{>D1}zXbR* zBk(l3{E({Zp9uL~OLc0Fg#Vsz0ZQBFO@#2#n0tb$voWftM5O$&UAvO|Vy8#?amyL{ zAqs~`*_a6SR|%n~tZWAJeqw_BRcVcPHZJu(6|Lbe@K$MM1Py!{jzfFtcE^@+ge{+$D<$#;HD0d^y3%F_7gCHokf?XV^ z))8uB=ufhj^C<=Ky;x}OLJiFEG_gFfe|{T}!$tiCA6B?on_;QC5(#g+aBBe}-!r=k zUfW~Z+jN>7C zB8uL;B1=VYBu$56ICOjVESO*OAs1jkoC%1Kar+swfk?}~RT@k0`os~48!1`eV~8$G zX8QE_0*Mtm-Q9sN`Bb=xJGFR+vSG&inVZ0l4(JEIooh+%q{0)$VRTN&CF2KiqZ8HN z6QE$};kwy5|=n^L?4C%l6P>)=*=a7TgZnf4hwFNGA zXnk@P(GZ=uTcZ*^dtXVIsF6k2 z6N3SvRGW}fQ9@254~fob@BLY`nOy7YoZ@J`~^=Yt6PCo5k}Sy`7M~;rURv7d;hG4Yj|rR6DYI%Gq}O zTD=abC&DL2L~qVj0o$!u8ny-11QmamKn~k3aUze(YmgHO*0Ed`206Ithd7jAZTr07@c!7RGyALcS>Xf+h++o3}-`7|}Bo^$Z{CK{eZP2|7G&)|dc4Lf7 zYUr<>-F9(+I`fG&dXVQE^y>vG#vZHDOn;sA=?L-8LaoLcY_@I{mr~YSW5wj}H)#0+ z>*4U$k1f{3^+3#$f9=+kaVo0O$7X2%T(!@)TTY)}AjXlb9td@LGzHxG<|)lW+F^#^;LH9IkyLLjDZK~cC|$NvC#MmeI6iAzkyZRWmz6GuGj?- zx|L{wGTIrjnj<7vwLwnptN@WXGEHcB!Z#J;u{61Ia&1I1P~1YsEitI*SHqnq<1gaDa4I&7Kj_KPI#)$fn0s4_=2qt-qdjM72^OOW~(xn!{BqDO9Pzf zN1_k|U=P=Cz<0|L2w4es(71B8y3r*V>f?~OE75>NAP}x6S8`fTST7n>KL+w_xZW|0 zS<+Ic#Sy~}{6-|0+f*MAzXMzYW)ho_T#4N_UE{NI9@1d9NetVYl*d8S%{4*$#zS4U z%yx_Ko#U2Ca3%OsA|H;)35eaEg!tZdRfusyIb7Dh#Vonm&*E>BdII1bP@OxWXcH1Z zMuCY1zS(86nYc__Ye*t9E|WWaK3!9yN=gz{(uZ^*dtoEKix1i)UI#)9f8P#W6@icT zAykb`wM$h=Oa}XKU@BbiF`0oms<6KUe~Xv6^hAJx-$nHJI&%!Oz^`}Ub6E*L(X@#d za-avB6X1X6WZ&m0>ty~XfPccn+CFg-_&=alzU#3Il;Hm)$VWIw6m&rl$_v&p76z0o@;iBO8FL6xO7EqX4B2_ULLN{u}k&Ty{gXg zf@_(txRsvWMU2BtIFyzkq=imM8CQ{CiH^oQD(V*Ty*?bryJ936JF<~vh2c-m(G#+9 zqzSSdXxhq+_GX#dPhn?_btfc*b}&CKM=%n^_(RM)`&oSD4vkr=kL4tT@G|~*C<%4s zs>ozy2_sQ%L<;RtmJ9GQ%AJq~ChR;X749px&ZSH@XdKt0-+MU;z3*Q62QL54%F_QF zA10Nh{~Y9(ITh_&s;d+FiO$8in8Ru@%8#1;1C;Y#BSyPVcjR}P;af1{-HcR9#GmTFj)v0DJYE#R)LmZ!%l92m736eWvHN|c{l^_|~z)T%G z&Mb{UBM!HV8XGiifeu?}eMPWFu+6UVh!!!Bn=v`KTwkLaw>q*yp2nLfrVFAG)uzlk zow(VW7?9Z7O%!O1`i)lqGsTr3tcAS!DsND)1=_#Pu5@8@(iUli8)kSPo%3~fgKCLr zIRI0A&G9U_MH4%;y;FZAHdUi)F6?_y>tjU5+>W|pmNneCZN8amr(vpLeKjk!&ZbV# zVggGEQ;H34Z4iwMmUXDT+xiStdlYNMT)tk4SfEQP&7D;e5Lz^B?GSB%OUPvmNz z${D7)qUWL|qtL<{<$3Tb)2x4O6QdW`{0zfDvEM<`5@w z@0sIere;piEV5EJJ94OUwMNQe+koP5hWfyD*R2URp*7Hb4D_^o&>DR_QQVirdyC*C z@xeh(7Ur$(`m2{Z;K1%mcnW|Ez}s$ZNVN7KmjgG@%LVr#M||Dq`26oA6J#fnz>fuZ zRgMsgsfP~$U%J@6sugxu0%<^a$MRXR?=II6JO}Yvmu5Gu)nBr9Amfk{BjgZg)@x2K z>(Z<@eC}a3!qY88@ltk_oC3w?=y4`~9yY>KiuK7^+~(UEfd!RYMd0(Dsk6-$Ze|mH zD9C2P%&0_MCZC*~g;CB!Dsl6>CnS= zyO{*3Oimz=)jlWXpnBshQ4i2VBy+Scf%(N8`G$=|&#r8pNx!A)oe;@tV0M>>(Am4| zdz4@n+lzACv;|%I!W)|C_69;G8+ZC!RE0_29DB7k6<#e4gAoj@?2T={4Ba+>~K5D5oYKuaq-0?hgt-haw=V+H6NbJ&GNenqdvvjwyC+^VGkxtw$ z-Bu7j0jaoMdRA;AR2V*+3ZF&kB#_7Uo^K`C?yj}a&D)2@TAA0Zr7;`lGmEUn}qkVcLVdBn2gfGg0c4fQ8ng~ladWw8f z0`rRnVHhxbJ<`7%P=&hyze$CciY+S_&4wpP%z4|!$(So{He5;iuS2hN>alV9F3nP^ zZEjarjW*GD0>Ls@!2d==j~6pziE1%#&_ONQwoYI0&{G~w20HW-pQh%2hR)&>o`75F zai_*^69~O)bm|QKYOa1ZUteobmp6?hpO|uE>Hc*()lHY9dAL(uo~pyT) zPqAT+B(CMae`>j-I@fkV zQu53tR-aSkfho3t$%L)7-%Td4R7%`wf~WsxntzSmIB$mbU1ifMi>|h>AJ(8t7ie}y zg*Th6iy=3e*pAVbk;~Hq|`6fKz!}&+yhV z9*g$agR1(TMaBquP5g~Ywt5NKi#FoTW;MRH)?>&xjGH;rCO|^C|D-p{K z5OF0EjgTFOx~_M)d?49c#<|DHShJS2=?Z9ai*^wlpotAc*sWF2K%VjS{po}D8y4V| z3H>xO^P;84e)~AD@OqvqLl8Wmx7P>KqyiDf{%>NV=R+XWP9}g$KJa@Rb6KP!=LAHKn@~sK!sHF za52H#LG7Zcc|a{K2q~Q;D#L@RLQ9rqvU zzU|XHPULpBbHRxgW^>2bMtMJ0IZoWUZgW8Mg!3HLWP7ejo^zP2P>uQrr(FVV?(2^I zq7xBD*If}ffoM$SLMGmLY)%f zFv`P-v-JOsz{$i37AN0Eo4X&zK?{fDCCT$+D%I{Lk6Ajpiv5;aDZmpdFs-~(ju^jq zr2QSB3yFjchk{^Dl8$8{*~oAeduRW_nqU=arUySBF@Fj+jTeEX?D?xlB$h8?0794W zvLi5E&Jy}LJ5ONAWd2(tsKtr8`OoUKL$#W@|D3$OdX_@s4k^?}XD%Q=oVQkuPxMv> zR7DWvGs-_V548!^SlpH2z+aoyS1pRkW+V_bj`9s9ygOr>IV9d$$0SJS( zglYjtctUX>2h#6}*!gRZ-1){|x~lRR!I?byig2!4j&44qgiMC-q{@0pvij?F^P8>k zI8n}UlJhzxNI!C_rc)oj8f|0=2T&&!&zb_$a#BIN&cJ%Z=MlH=_En`KCj*nisM z5CjVEziz%)Rbs5Jf)}Az7~#_%sgqT&YdOM0c?m1jNooG2=rij{k}2(oI0*5|oh1x( zn+-e#zRrtSg4H=YbqSH$GUQlUc9anfg`iFUWg$!*WZagtj7?W#yx=j5j&%O#u5lWu z>2WZ6ZjXa^Aq?b_)k&o0Jg*(sMa=&ZJ%zzsf)nh>*X+T2J$xqxuOmzLRnDK;Q$8Z- zNnNxnoa7&Y{3DqSBL8hVZR&jPzu&n6I&KR;1Y)V}oRn_Rw=C93)3Jn5=%TLpG#VaDhtRyL@z}w{HHWR zCZa(maq&~z1jG*2WV7HHc`eQ<5%`hU`f46_R^YP*azGb134_;7qotcWBss0aFZ|e) zBI%Y+xcjYI!;4AKwMf)5k#AsIk;;r~kKw;w)g+zi4BzS&;Tmn{lu?Ghn%w0gjwL|4 z1wKMD@?#sMY*%Irc5l_r$e&1yZC1t$IC_{R97Tf_VSXX6RBHl2>D zf7Kz%s2=(BfQp^j$ep~5mc6kVu#bWn2uz}o+)>8?|I9&LVXZhpaaX>>mGMmqwF z=rv@_jz}O((Hz0{(~>%#Y^_7|v9>=WkBr$-8NR^za}PBYhaQTAy_~GwCby&#u`l^X zXCaoT`OyhgR6A>-CK+H&(nZQisxrJ@51pq*QRm&Xj7=Grqxz&-xh8q26$X%MQcsQu zThN|bNDx+oq(0ovDn4w(I9&yA_dAYmA(BkjRrFEw%09(J!qYX1%E$QL2*ti!59KIq z#vt{l)-cV*1P4hz6(P6Oha{b>Cf}<|AWA2gG+YJGf|Ud`3I87vz{Va&2-w)Ocp2r+ zDySO|>=Agd$XNpAe-M}8$kJrS1tL&EsZRSd2Nm#kggx{_5hgWHH~%OPeTf4J_%f8r zoHodA{^gDoJsqLR8+7ycD<2XGgE`?{d$3qN^eQ{&Fc>D2qj(y0 z*ZH6Ah!E#*QE_e=&av~KB}D?mX?1ezBXe3*czA7V>p-H?G7OS9q}xu0K`fEueMBaq zwK{l=yzv3WV{@GiKd8ISP$@-)51kk}A_iuVnl5V4k^Fd_Hq}}vLZ=iZv*senRhRDh zN=}$CCNe_JrYCmi^$OFH2J7&=-F(u;oV-#xX&NWBkj!ELf=LmZ9q>8IhYSgyKm62= ze>jsD?We@ZbB@69DNVlH+~+axGnH{XmOD$Bu1m0BEHV|7ugvB|NX{NzMR7zL=`$7h<|a{?_SMPJOxiBwe25$+)2iE%?#zFs-d5@y3kLXzsnBEI z&c^)_Ge1jb{_}eo!pKrwU4~Pfs~Ud6bdM>4w0tb$s)l*X zpB2t_98!Uystl*2hrN&S`y(FvDDL93+#mRABZ8c_$#VAL47a^Ql|SdtXQ4$A+um2V z{mmm)4UJTeqiw#^2gMFcD>l!^iVEyqfPG8xN)tx63NDHrhaF=if&8;#%vL6iPp+2D$5$0t zzW|4-B!T=E<-V-O%g_)QbD#dg`XMYzLndmPV4DeSt@6CY1PRG#UxB*DPQTw>0rvu2%WSj`6pTZS{=JZam2l6;c@?M)%0t3{4!U<8-cUeAu=<+D z9Ao@~MuMe?Vpoy*n868Xf*JWbzRM$3=qixjsrS#o0-LNw;6&5Vh~+JaV(3$05;u@M z40A>9D@2QGzvnmsJ$(|GVaj3-<<@!6$okLhGRcH1u@N1(m8Zsxq@-&<9t(zvx0jNOOb;o?fX?yyqCNH4OWXdRw# zZoMT;sH{HQpqkbJNOg{m+Ob^#f3>3S7)3`+7q<@;?HzwpZwfQASg}N^Cq)=#o1D2P ztY&q33dh{glHMA!tFWAwMvW=rJlRMc#b6Mxc9%2fR{M5If!ydA zbI@)2ddzQFNz9q>6rHdwE+Nrd)u_y-?sTTw=1eux8LSTCs6+_v^P|o_Tfx}iJYtO` z(+)+>Rv}!{hmt%-)!L~*;v;9~E1kTE@nMUgFC!9}f5Mj$S=zAJX-<)*s|bZE)VB(U z;fVPowX4lT-h1S>b1l@aMh%2f()CYuxWX<+w|(eZ9FDLK*0&`y9~KzqffH0meTXP$ zEo{4Xg|n7sicPc$cA-Hf=M=YzSCS_$ThdWl^)lK-M0#pAdF-$@?c#Az=8sB#vnoOi zTqGP)Xpt8o`HU7_sfg4Pv-VIJE#k}Ku!Hc1g?9E(=pekQ{Utv}okkU3RE=%l+We%F zpY9llN%4l+>_kVeM(}I_J%z#lA^coIbD28nj`Q`4l=7`%ZbZ@1VNC>Wj99`UJ&SvJ zf1N8(uRPmPG4z0Tk8M>f&DN84{<8mEg3iA(#uTdN*~KirtPn|CJpn6p{X2pfBvTB{#)6E2twk^`kTGkZHEt9=6MxVohI$ z3X@*IbUSm6p;K*I&2zGo&k(I7XG#|Cj?kG_`BWXLGYK#jl}QJ^uvX^_eNO%;%@f9@ zF7<_}c}{gSk4TO#fwy3wU4lN}3-h#*`sd9y6zvoqfSNOWmj#-tJ6W&tKVGB%_j}9= z@|?~iMC*ke72Dlj!@z%SkS2Mphb88Jm=~U`Z>U){Lwtb;;xBNfY7N3W03}OEJ6& zdmAMrM@+&AWbVdS48rm3^Y6LYYtF;I&r!K^TQpqS2 z4LKi+DngK8lp-i!lW+)yJojz5hEx?FB=W2~v~3@Skki&DoMWAj_jEqaw;tc~v--j1 ztq^5Se^8aS<%FL5gs3-HW#Qi4pAJB;#y54b31B{+L$}s8etYW+fGtU5g_WQ2fR0F zf(L^g;i?GTRS63yohq*e!g`phpjvwhSe7j8=rtXw3X;_V@|`1@0fr`5-7EcxtU_3& z3gKMk*w@&QheFn!E%;a1OL8bO_jWK|vrzZ>2dfkGF}UDo2Dtec+))dUs5ZOVV36`POAtx`_wOFFVrE8%=Zc$hg39&+e z(0Mbs+(}C&#Jap4bOEeEPeGk{7 za?a)a`^dUd*0Qu7c_PBE@&Y!1j3v#=!zx1Zvg2W#p{pR#_fJ0Fv|LR@`ifmhIDE!8 zvlRQq?MQK!@UhN6%oxyVJx7z1GaO8{QfI!%&RiqKtr3&|8CK232@c9V%Cc;!*Vvi9 z6C@yK?$AT`N>3j-1$I%?`O~tq5Lp;gS^LtZ64epDwhqg8ot{FZPccYW(a#a5BDYir z${UZ>&i>m>t@~BO9Cqfvz7*@i*I^cd5iftK`XYS_OHsCJ)b)W#h^`bR9g1+g+Y)^t z`O%B8d^`H9Lve?*=dC*1haF75!)=O`*0UZEL6jxlzdCZZM1YN9ZKDV(RmHwJa;hpM z*6aBvh2$3P(_s;m62e26S7t%DL+jbCCj1NIa4H-+Zr5mP8AXM3ftB{=Xmg^@-0ftS z(Su_OB_qW~YxsmcIK$QhDmILOgeX48_q?kNf3sR$m*CeW9Ee=d8lwC?-CIC`x6s`I;%;5fz&tM<*f> zp1i6Z!tY2<_=_X!N0F;xUWCYr@?$&#rbb*I;r>ua+9KQ^yX_y*Aa2EBhfS5)GWcZ~ zcn7sw+2#!mMMGEwTfK%}WKl|R?L0iLY3H9Wz>+4!t;0>kLakV}8xOIV!x+S{^>#7V z&k&n_SkvIASBOpDuoMN#EVVYur>@>A&(?nGki@D3ti?)JO7RjwRE`1=1LhPzIo)jPV11AElS!OMG_e#-85z%25VbP zg0$Zdj|DN8*;Y+BLHlA~wi^ws%F7VpTz$n{fef7k^T9@WOx7q#`nr|}JyR-Zy+a3< zdqSp2TbyTX#i}<1BN6BLoZR()6=D(<$P5V4L}H3!5Fx$mDF9896Q1276*CBvK5#-p zNjA&E!lYun=wPI0O$o=~9yx#(Bu)GK3;E{LyKiv&lG`br{S?7E7{kU9nC2Fd*HZ{|CC7VbudpGB@|Bk6dj5<~`fQDk2zUgDDn;R`ZA* z1HHZsUs{zl_h*b(5pn1worKBJwXl}dq80KMel7Rk34^ZDwe(YB-@PG3JyFJ6nj zax}nx4TIk2D65sKZ;39#2AYf+RWAL$zjrq&*~l3$uVt;A*!f=_J`QRvF5y4CzgJY{ zzIx>RNt%Vgl41l<0&(Ifqk0hG<*fN_9IH;E5W@oo89O8S8nlK!4;2be_B(9L?tyZz?q{+~m8@AM< zIvkcb?9}i%{J9g0xNHk1tmHCur3s*+<=su7`Q2?^-~wDTNQ$;rp6%rrXtPejLHP=IY{ z9BH>i+1N1G#|x|TC)Y8?-c}WuM|A#AMa-YKRn$%c-c&65lwwgUGue))N7j6Zwhw%c z@@}>ZEzgIpeI1#R4d2vDyG8>0; zCaz$L#ix$6PuR*5JH#SE6N9pdwb!9RtZScrG?JZH{fIltGzqJ;|Qm^+u9 zEzYl1^<8WUs+V+}ib@9!P%&mXi0-~4>uPSIw%2mLmq+aRcj}IfUW)y??E~tt$63O) zT!KPVJ}z#yGVf@6OOBnaN~YCbRTUfap%hYJ==R`|V$bE|SXHA5`8IL)W(~ugNlK># zi|6B1ZDee7+Py5w)QUgv#+&1C!5Hb*N67aQ8_v#<(gy@f3+LnYick@%oeB(sO*o{F z@7lM5PZ-F4Jj-W*ghM5`Za$(bkliH6hZ?V3$)bF<^1q#rlPhq+0vuS1PZV_VBJ;By z9mv{^uN6)4H_y~C95EzQ(>E6gH0n7YA1l#xGjG#6Y-yDwG!j|FP8@N01%gb7YDMqe zczhgsu{&Og(0ZFlZ7s!g9f%-=iC&X;<9C^eV@>rqJlzAgB_epdam7pFbYCw7`@@?f z{T$1awVwj}B4rp~mNcvK4MSJDxiZz%B_P;C%E+^&MP-VKW|;zZ#E8QkJ#Y!XEtw_L zQP~@=V@=?A^HvHbuv<#HnV4##SjD|R0)vq>h=sopWJPyA9_)#vOzHJ-fW_?-q*D5) z{Dxj8o@LDWK~|(^1@%O;co=e0(8d3aLtS^g-vis@(K=X4tTXE@c8KX3AWa`OrXXq< zo=i7g94^I7NI-L3) z)PDW-5GXCT@UPl_$-@e5qzz9DdsaIq{g8h*wf&2bQN{bRYP7 z{a^N10q~oQ>hnxL+Mo2RzR;SPGW1i*~ogmTMy`xMp&*UF(PGCBDcGNG zcBGo|DJCmH)_a%>>4VHcsOP6fNt+Kg)Nl=lE6D@~8Q(~3<%y)2H~0;yCZ`9c^^tP) zEEgZAyLvn8&vQ|%md~R(2Gd5-Q!}+5bfn;<1a!G!;^Y_HJRlwiy9s0=I0(LM9uu}` zI~#5V7w+qCe8Wu*H{5SL%t{?r5x_L0w)FvRU zHz+U<_dxClmcf_4DqKQrJgR%J+lzESzDV!38g0@fUdscGiQ{^qJ=7C4m-2(AIxDZBfmHqGWc28Rq`vH2AQGF@6Q-Z^Yv7fP9*n)*Os@%Y{}WA+N6^RcoAY={*}>r$ z6PsxQeN9q^ER$C?;Flh7Nh760L12VQrl9hUAIEkWV3&cCVMyy0YSX}88?F@IbX?FM zzBp{|j#zCakd!C=pf(%-SL7PV>nOh#=j=QP=qqD+K1lhh~-u%QjeJnM;V!FJg(zz zc+fcvcPB_6#@IycNE1O~%f*;MFl<^EmUfo-BV0FCz%Kj*OiP@`yiTA z9fSd16QKMIH^CuhqhHP?mMSIB)Ev)p7NA1B0YR zR6RBGK&`qnf=8gYjBP%&!N*4Z+Qmbqbw$@GGjW)iG{|&gp9`LXPH+A&97s2#N1`hZ z4|fOqmFzeM{dl9(Omcs7QHEhZEM8cT2hAyRcc{;KUhX)=s3_9J)b~c;W8{7=A8 z4UdLuq-ucqfN#q3Ow92nEd_53!;W-wNvc_!fP+1-H5r~hNbQff!LkWFbSkvGL}~58 z)+GFR2b+56p}W|OgBB;<9G_|yCcw`=BVJ>8pgcbAXPlVJY}%>*+JRBmF3mxXF~{~Z z5BRkGqAruK#-KT%ayZU-*kVbWZJCjvHTNYWjlVNOv^PcCbv$$R<(^XHD7{y6o)p)faE}rd z#2ve(znPR|>bdO$Ojcg#Ku;Qm_H^^Dc5FE_0f%W%(-5D5yZ`U?ITw;{o=r8YQuJ#3 zF`av>#*4;RX3o^6JX2t&l!Ad&q^X7$ZGazLoESBm2kei-eke7m<_6oKtp{r3!G!{oDyFNIjFJ2yu<%4j(?gl&2Y(7>tgvKkAVyR!UF4dft!m9_9ESpBTip|3Q zZ0y(C_~v9O9boKpzp-Gr$^<0DX7Z(Fyg3DC63k?nBAC-)PKB8YGX>^!m@{CQomB`k z9p*PMXTcD*&4f7zZ1!!=ggG0g6s8R3Jb`oOT$uA==Y6{o+y(Oh%snu7NaY8!O3ELZ+hJD1JPz}Ow(xsUDw>+dV4i__2xc`*v*f@V+IohQ zo`YF04>!zTVV;870P`2^`qu=r1?B~qO)xJ>Ekx4@^CC4y#%Vq@Z;eu1OV( zpV?1fxhn%{K5V2Bj5D!>AG~pXG{>W}2lCS}n|+9q%oY!(rGN;Wz;adsP8>kHV=59c zf~YCc_&6LDBfnqqkB`=~Kc>(|P}^ik1EU&tr1DFXBcIOU?q3^GFZ-7~@xbE^4b4D_* zXLG30?DA$FtER2#hkP=b4=2P1kWI+Zs5J=;wrV zb2!x;NHN~Ns6R$_>~yul?CT^TO>OR!2!l(rPK8F4NN4KvMCs~9738CV3-4{J*`0vw zI2`B>R}XY1Vq6*yvQKnh%;=4%e(=#Cvc(=}J{c*`tHd*Z6quJOY{<52DhgZQ`XV=3;)TzgQ*f0~&j{>4t5l zV~Imjk^PJILR&TypR@9ecmYIoybkJoQCExK+D8Euzl{iivr5Fhy^xWHmcDqH6YG!1 z2jh!D(r(f!-v?r!)4mka=1W?I|dTzE96oR%{k0NX!-?CI9Pz$G!BF zwIgA&@%1&av_~8<-Mp4+cBKf|crnk!$KhRtJ@53uJ84+R2IzXw^!|9tfi!ar*OFwG z4lqeMW*DJ@?ka)nN5k-&bkmt?_NSQA1nE*oYfJR9N_<0USQ(&CO0S1Eh_;@P$s@OH zY>N38F+WARJ2HC64Q0DGDu<_*hI~1|Uvi8d;{BR;(AzV?ni=Hg6xpiRK!}o(> zn4WH)O65t(LWcFnMLc@CY~o-_D{vn@oxb>v3gkGRtO!p_f8$Az?J7tYz&|k#ZzLj? z{!udWPN1ZT0RNSHm}*W`!0-y4vQaRqE9745>iX&JpaqYnGU2Ts1y7#&9kG)KTT^68 zgb&J0Qqu~PLnCDkG(Edv7^;@#J9a|Xe$Axv-2KGEWjA^HJ4t=Ct)%^v7MU6{-Atpw z8ft>W#7iotI~Io)WeFw?H)bfiQi`+idk<<-aExlX>rzZg0xqXJPk_M*&{G&_ddl?_ z3QM2@ZFIWHkiRk6bjj&yW{8iujF^Fb=J5|m+-2|252o6U9dul6$AoCOKBS0d^MzlRmuLNm8>C2QBu>mDENn;wHC>+D4 ztJKWB)e=!(DF|^9-f*}+Vukq)Nj*(Lf&iYROq0~t1T%!w5RL6qlhjM5l+;^#?f2>FQgMj)09B)=*iW`y`_Q9kKXx|lP}!nABmJMyc%@n*TkOqc5tx(EZvb6Pvd zcD>*U7$ zcsyS22ya|4OB=3*h4y(7+Q`oGle9xL<(9xH&M)nK`5zxEjpbGKBUP`!@~lwb;a~1( zrhT-N`-z(3H3?)RaFpaz<|jHt!5PXDoTcQzj0jHPe1{6S%wYiLIljMj{O7}5ss4Vo zs|lFr~XAJy2J654;|?pt6y9YZwh#h`c|cY`c3YR3dG+ z5ucqXj>u#vLlp?DP`FFn^8X%m`7zUv!Tc)SB2JYXSWe79h$24-h+0JG6(?Bn-wdw# zu~5Mn=@f|73Xs}(+n6Uc7aDw;>Yex^OJ&M9?dlR zxf&>;`)P*4|HVnX;n1O!!92D){%C2Fc|4GX;i|taG}V``FEV2m+iW zoqc}E2u72&c{=xmOoeTmPOkoVQ_c?NWS{85Gf<`B6*3F~kWZ8CDk9{ziyDIX;S8z3 zJzDq>ZfQP2phD~T{w6-MQG2F|VWW22rJXI>iOijd1NY%9T2hrLV%l;Q8*Tz;-mHY1 zd!)!-6k}3KUH`tef}C0`=; zEJU+%%WT9}!&imaMPQ9$79lv$tR>pY+c6HZIsb>2tU9e zEIkXupihDcW}05ysDVP&@(|}SLE=OS99T5I_eg*tXqhRNTrO=VgSmcOKx&#di?ZrRCp{PbzUs_m$K!|!FXSkX zBh!3Akf58u{o%(wsy$91Mac~(9!obp7%<|{gmXHF9Q_@pA0)9*6>n(xn-vy@dem~~ z`L>!_#E}==x!s*yh2HMbHWuv1m%~+DGcZ~?_Iyles&%_~QvA!rkP4biKb_er zs=oGfK3uXYc3vy~@Epgb6RIcPnrZTQcNhyEDW9$O<2;tLrU1|-^vR&#(aF_+TBRB_ zgO@ZBbyU|ReyPCKr{)KFc{f@QnzCdic05?A;F^=+8G{z4dllde0@Na0F%?gof#5VL z0?D#(=M+npUBlT}9!@JgAJG>eV=gvbA#ID9?#OqMSIx%ii=}-YbJz+h&_#e+1?GU= zaUFs;V)+dS)PhNy&C4)$A*L{^Z8?G~5L1V1?v=`xH{ODG?na-xkbMv8ABOKiWIT-- z&mgcGZ~Pg)M{(&IWUdDv0{m?QzS|=Cw1;01IefDb9W8i^PYC~kRXb3y6%W3O^WKH$ z75w8(6flMCJ-FUL?8mtC6KvRy(?7+O_mQzvN?-c+NEYP-?I`;ir+tajzCi&W5B>`Q zYK9I;r|h{Kf9F%XLwNEqN*N~VLelpLx=fG)9%IVmjq4zGy2a_Hv@kb=63B}=g?Gx2 z9#kyqdNUD7(l)5ZHOqoZk0;9$;aT(e=wd9)G&MBPPCve`Sp4Y62b5kFkDD?Lby}t$ zBlKiRY?1(x*nygrATxsect=e+zpz|o=1-I~wKzc?rFM|yL{T>~hE9Y)`JYAe;YV(! z8LdBLCsNfQKwCLlute)1u`<+e{F%Q$jRH*SINkJ1l{h{pv)s`?{2(n0?f6-gXA%oK z&s}u5bQ+ZTB>n<>eX z!5cS6Q4_I*mj+Hj8i$L^G$$)^;{-39@hTY0^nW@3 z3{?(1@xSXb`OOde(a)+5KThIF=`VO0up@L)#n_`lS|&}}fWjtF)3VoLm&feMG&{PR zF6LS=xUWj4H>XGIkb!Q>?Q~?CbJ$>(}8x$Q0bZCv9%c?j?_{dY4eHQkybgYRqHE;7apC8xKId;X z=r=?$yHcmY&F2~^2K-+yue{!OfwH_@dfE9;9AzgXwj?}xMjlBq;_CSK*HRub6q|{S5T|y<}4xQNTqb544UUh6T_*QW*K)wksd|PjE0**I&LiUg%lByO-<3;w<-=U zM!|Y$@R3n7>JUaq`%1E$kC#HHVcBDbliYMqh#?6WDEUO-3UsS=LmtaC)4AKsIORv2 z718d52pk0x(eccUH114>z)URxu)PQhQMgQAD8dOYVZ7p{6nZO}f^UW1A#!8%ncvI@ zaq&tTlP}FQl`QPw7uYk6iiLMmbv|z><4{wF3mHYNNW}P7Hr^T!zPOA!88u^&F#%?* z=$Y3}f;Rwf0lfL}PDIox@J@nfGHQwtbsD^f;yk{VK8ZrtIJsqCkz^iOh z#!S?l1Mdt(oeA&R$S8%U3>oK1Zgc**@Scy1i{QNg`IjN19Nr7zy&T?Ih@Ok6-y`D+ zaj!yO93?YRF&R0nfw8Uj@%2@ZOI6mGC|e?-TGoiHrx4|1@eI zgZCNKJcNwZ@HWHqXXHPMnl*@i4jGo|`YSw7!OQyFe}VUTWHcdvi=>)(HX)ys*R$|8 z!t)}$eA>Sm%oX>(h79GtYBnN6>934;;e8w4zX`zbXsSugdx-u!ysyLa2D~35_bqtd zf%j$9>_YU1h~AEs3K(u8-k*87{sMIjDv}GbpGi%2jhx zsrp9B9H@~7`_ZAHqD(ph%oZT=tDa^p6FI1^2bzspU^~bSe)O^v=4`2L{CJtTdbnh* z1ZZCv8Xl%8f4T#g(PvSMPgS33<51xs6qlwJA&*L4ZSHEn=gTYsZO^xvXCP5(cKeq8EJ)25ImbMYYt@wRd-PpcZC1|a670;%_dKU1kv zw@g(MjC)>AjY>;oS+Y!%q0Sl|t2|XZM1bSyj_L37X^ku(Ldrz7qRxC`;XbhyvL zlw#D+#Ncxf!^%jkvU?s9E<#*65-x{pmJ~oY3lNO%GXPr~&e5}ro(GdSrX^jVFVKO^~3?Xpi^hxm_>@Dtn}f`#1wM*IOZMVW*{IB!4tXu|EC--9ow2_f>_ zrawa%JUGX4xnjjw1hWy;8=5~xlfB1Az;TKB%|9#3@>+rxxbfO?!-B0uY~uv5xLx~*vY8w? zN!zZE$k}cX^`6RWerrUi_r8eS>^uHfiP_IMS=oOEt1g>0p?vIyzlQR$J%16tcNh`! zrcfI8h;T%B2WjTZH z{>aIu-VMXnp9E|QZja8Rc9N(Asxe${Num~|G0;R0HH%~AXjlgS%u&3vhDp@CfXsND zq%;V9%yXC#P3AdOo;>i+cxFiQwvwYBkDFOuV*>V_gxHC2O~uZ$gb$C^0DTS%WL|tc@g*@Nrs_x3IxZJx6D@^NLAU>-iwprn~3tMQZPSwHhkyH@&kP2J{vg~BmYVS ztC6N^WEH~%%Mh~=JsaR#j@WxKu`la^Hh#ycYcy;hP}JZ-lQN zJ(uAz`_!7MkFRKNMZ%p}bU%__-OSHX2VlDL-D$oMm2*GrdYw-yVlHOs( zjaqB{H6;B5{vD`)6{A_Lg3EkQ-XO$o#|xhz_EVvqd9rS8(RJZxNcs-$Xu~Er?)@OB z-=uHJD~*X|(b0GlP4`DnIeIK&PeObFGV|f)xfEg6RMbq7QouEo(iu2ZiZ)gXIv*}B zT$kKF8y8VHb-nItOj!c=A}kG}n(M8Fs~+Q)p{@a!@I^e=dnb;+1-^SQ`9Vy22uaQ| zgKM!sm$_>TcD;m+PAkdVHtRXsiX2?yCsHycO;>l>9&MMi59O3wUELI29jm`4#h7Wj zsITp!c-OeD0u)V_SJ<+mY4S|w$5MP>hL+5IGME+IPL9NOLf$S$M;FgE&DFR zSAYj6;!aLuDtgYq=(DAUMh^Frf<%v1=MY5^(mgEaO{c&#pp>Q3bPm1;;pU)g5W5x^ zZNRK8_^J{6I2?sxhcr|TPzJUm=Y2W-xjRw24|yz6vLC~{V7M&@vAIC%m!|PRS-X$p za_u_kIkXEwOrv6Hz%*w#o(}!xq_s%efTS(RYea84&b&(7QSb>87?Nv4-aZ6Bz{y{M zoy3EO;O@dIUZsPw&y4I)VBD<3!zu(E==Y(qVkjzHAiH?*Qn2O#m!|9D!yLw0Sh{2+ zo5OC$>NW&ft3jJ&apnP52`EA~XFfxAtk}+6za&5lwE}M4qK#AtyiIIIwpy%AzYUny zh(2sd#vZS{1)QPEJg<4Ve&M8P2(n3sPO{K`=WNt)dduWHlWv9kPPvcnwWw^wt=pxF zcu)n5-{2~cHnqe^h1|nSLE(=LqL4+HZ$&H>GW(QmKyo9daXCDiHcZ)vc&>;?Q6QHq zpleAgLp}@X>nZT#pK&29Utl++5vzAd0Mxe~l`g4i9AxN|TS0EYs#gBVmU&uPT1o{n zMn*hV;WTm3qL+Ny-@)LNEz7_P24m2VjiL&$x(GgPAX03_pb|GWV2_R-d!Nj_S*u~6 zs>NET?!l61lgEYUnnSc`Y_B#(y3}n_b>S|+wj#8$c+pw-N3lfl*ta*8U#lfns3F~2 zw}HXCIT6!axD;15Af5M%JIoQO{kUV?=M9K%3bnqS!1@fF{Kq2I6lo8yd^_rh%D9{k zu8rPO$Xc>Ogk{z7G7rMz?w9iGA_PMx@fx-q%=0V$% z5xm`NQOPU=ASaFkrrF%HueqcTeCF2+tBXiFJxD#6t{%Ha8UwgT0x%^s90sUG{~P54r~ zX~(aVCWA8wt+&I+^U`nrbT_`#GrDvfTE;+XpcP@77W4G$_m3_^j}o|5PDBrD0o7Jk zSY^enrI=`M_nq+RDrPo`BAVZVz&a#tL#z%#(9*0!xLLx2Vn8jYm})W;GfI#$4>9_+ zz6vCB=2gq3O+4(WoRE|SW-s@tdm!YNvBXwe8-z$*LDFxxS z0>k+<;*l0`)jYDEx|w##QM=*GHD@!qdK_-!k_$v1azEWA$e4q}6<`I|HOv*EuAtIm zL9lHo(H*nP`FJ-*a5+wDx|ghAt|4jWB7oF5`(>$SCSAf@(>X-`Y|J$=xl%4*8~>GI zHOKq##c~?rfVi^Z%0skPj5(x*zh3u;daMuBx+^nS#_b$|pg)(`qG!PhU~@xdsS@U( z-;w=@UkPI74ca4ys7&MR+!nUHVR0~9@y5sME02n)vi(nar|6ooP*S*z;Tv0}f1Kz> zTY!a@#VNzs#iB`ju!Y7YqD=AG(Z-f1_3xMJVIEb9j8UF+5N@sEbGW-1Yu^EJ)p-&! zraVyNvB=DO&U#;%7^>hsxfrd^lG*9?ZPgN7Z|m=+5%yz8F19*|4{lIA!#Xo|y;?{5 zF*Q2R@fJLca$WDN@OrsenzLt}^7VuS>u{YenetYx#owq^y7nMSmpRL+oHHxhEahd? zE=6{YhvH3dnj5w(^q4EUo1Qr)I`W9~5ai3Lh;l&WPP=%^3eXkq-lEsRwzXx6{>9E> zZJE|Ry4d{{A;ZxbL?11o=5or8u{6+q?1Z9=J*puiE0#+i$tJASicB=$tp7%%vydy* zV`b^}mhIYEj0f6|mCiE9MjO_*;WCpIi_*GPyUlwZ3DXDcA!dUw!n~uv3W~lHrJj7X z9%ZSsx=f1h8IppvW2RIMk{V4sP%UQO%`T9Bgd@P+b&VYnm9oBN)59A=rO8^CVX?w; zF0&)zOux}H{n)P28U?~U`WNA2U5E3Xl`iKp3Ayc=wqus6rq&MJ?zB202=u70!NqpO zn^kUen7!J&n^igHOueD>4Kp2yZFQD;kZERmlB+mytcVc@tT*vy2dhWfP7=?I?8DFq>G6Gc7M?5qxhCGv z6~&!eX9PB^@)yQ7A5~*j6}}H5mRE8qs+URIN{Z;8>ToFH>_q?EE-5`Zj)-z;@!FV` zBFd_yeYmu+^4X4SK84#kK52g4j<=mnA_P%mtE=@ zv*t78Y^nObnki4Z-uIpLZqzPsUG$_1aX~Sf^c)w3&ygT-E>^1Yc}*AFSt0kawFgI)rf02JS?amBVVht z(t>Q>2VXnRch2$SXtP|;akNvYpQ?4SmON$6!^Ny~9#NNUm$R1rTnh;bmDQGFR6ACK z#TygCdWMUdD+s(o*VU}+Vkfl@h9GGrwM_BhWw>6Ck5+PtuIs8>6#71lcrJ~;_=uMrvv_?mn)S#!BP5f)8&`J;OT23NwT}5{b`H(o7QeolAeWqj7c9-WEdYZOhx__^*=s=QDRG6|VKqzaQy zqnW8GXQb|xYmok5NJSL9An;Oum zo`7y+(d{I33m~xoqb6eJDM*}zZj&*g2#Ke`Jr&+5NY|K7=UKR@SV|CIcn%WjnVyYq zrAVZ6dL9y~yDq|j%aBrzL`6!6evhbmm@r53Y-jxz-DbmkF`_QjD9=?$tWbP-4HB!R zeHb-%7m3-$7;pokYO$^!Df9=IA(O$K2AsDXSJa`~pM;iSvJLCHMBgHb@r?D5srEh~ z`M171(CuMdxJurOMQN-j@o^+Rfy5`_c^Vf#gs9a>{WE$$iV17b>p8gBi_4byS9E&{ z-AK6o1&Q=2w!p(!PZI*0FyUEYEoj_d+>CAv_iV&~t-^jxejVKy@!5`9N&+XgVSQv5+zbSZ!iL9 zBZmYa8NWM)?>(T*e9ca<_?7Qi6O-=53kncFRp`6CGJNh3h71r~5=2`)+_xfLX-S{5 zk)8B72ye&+lZV`gVI+QYO^}D1Yxb!Ls2YRd1PoPAfvn)a6_!*eXYx%64$%ReD}kil z^Kp+JeRUPyr=DLXPTxQ3@KFMX&Ngx zQVx|cS(1$Mk+kahB#!Mvp<&1j*3u(g1FB0Hy!~@?j=o=_&HIA}p2zPJ=l$ zlzlOtkzAXAQHV~)bAJ;G=~5@NT37cOcH)~Y4>)eQHUH>7L>{c`23b~kF zbMPV?MMXw<{}77E+aPb(RiSEKjLX|Z=DK7fJuAma`R3SXeQ-E%uUE2UZ&b-N(3 z2Yx!mQdMi||D}Pt$WicHh^J%DBS2fky%W$(O>#7{MiQU5(f(j97%EAWoy3-+(b>v+K~q67_co;U@E63HO8IH?hKVkV|0V z$0p>dS9lAvKA2Lw75m?YYa90eT^gXXo;i8xAzV=hChmt})Jutrdbio;F0;}JOU8F{>EA)5gNTEr$%He$@FZWG5^qt1L*+MO)Ihm3h?lM6SWdX?hIGF(_9u{`pw*Xewcneeax zsa3czDB{|zJ&E${eAIvRtVJ!mHZ0u=~UA#btl+uwBGd^*n}AyJJcv^r%SzJJLi){IA5E9K1@90LF-goD3BLOP~Mi-r)emtM34D! zRp2VE@yy&o`-L(jr`Yy0m~Pn1TFK zEOh4l;XLUj*ItEz4Op{6T5l(FlDA;B#hJ};X*6Ng2JE9iwxAyaH`Mdn(7GMX?<2Vl zm$qZM%3;p~$W}QV=r(6~gijU)#eN)P(3cNMxM>9l>Ot54MsnWrW?Q|(dSbyT`$ROzh_2=71W1Aou|^)OF)wo zmaDDF8>GkgZpB`mv$qYWaU0t;55EKVl4Cr8@rN;~OT?9zh?75$Z$0ef$w;tHDS--0 zoLOdL6s}(Z=5I7p6*EJgMVq^umyVTpSbw#(3pR}e3ZNu3YH)~*OY9Kwm7Th!P+o>|M&YaQDLopxEq)R8>^Z{8#2<%j-dRqAHbp1E`{MyH!>VNtCJ6}wZ_)FGSVq)LxjKuak_>RUB4yh5d? zN`n;HkOQdK8+Wl1eJ+|~`gh|XVLlyhO_AKSJ;k_2X}!8|lPm?0hn24Ui*@a(D3;=* z1`KgRc#MWWsnRQvJN2w5vV)RdI_g`(gTrlLZUEW29S%G9u2PXUD9; zH()=(!&VICC^Wddp3$a$x6rD*XhYp*n#XwUeH?>>{C>amG-oOWq1P7X@0H@IGE6PO z1V^WRItL!@PekAO&kZ(i@h;GDjTd{r{c5TOlZ7A))GCk8Sjcl?R z@jhqJt_hMmY9U`<5e^sPv>8Y!#f}o>IfItY!E*M6VVVsQR0A68P)f)b4ih|zxV2cr z^dTyxM%>+kS`r0Y(YXyDZO524+|{m8^$tXF)vAbE+=e%T6|-q~zT@accv_X`-6WJs zB@Ol89C|K((!FH?;cK*AR8ZUf5KfT%5#-|l&e*m1f&@h)1~PQ6LvJA3+>Tq@@B(|J zD!lZ?7`A1h1=+}P$DmV>GFXHcbllA|q_?}=V!PFI@rn)|ti+Zov?$ zWL~jFDgE4x^_yurE^}IDO$cR}JX45iGtg5Lw?f_G=3=2P+*gUW^sKkkq1^5^vm$lX zC(!^|n@-^2#G{Fd-`|Gsw}YdXQpnqp=3JswH_NMu*fDKX0Ga{;5l7p&`6SeoP!{QTiJ+DT0j)k z%&lNjpN8I!*%NX!Zqg$liU`sI4w3u^@h_CN630iUaZw?%>F?+TzpxZ3B^aS{C`htW z0%v<@tW}p%5;DRQAS$y!;wedUs@6kk*@{+9LzU{E+ECn%+74m-?1>P4#W3}kbBGm5 z_(?~n*Y6{S(b3aNQO;&_&IJrqiXFT-h#FpbU3&<_Jc=oL>pOJN6I2r3e^pG646X7o znQ!-aBKsusA*g~;z??DG5787pBCE;T=D5yoNSp*dn4jD^PI9`$(99DDidimWtHhfK zs6G0UZ@mj8t06EBX=9MBNq`>|V?`MrC_y>}cpkP81I)+n3N$akx++XsiVGU#=E|ut zN2Lj!jiRZNG+A=TgK{@c2R9 zPsUbS2)z_PtS-Tqm&tE-tAJ|(+)EMHfB?5kT{(_;bqg|S$6IlC2NHLK5SNMTY_Ufq z%x4rC1z21NS1~@NDVdM>3T$F>P6L8XC~Aa{{RC;y{pcDeTEeIE;V89D$XSN~A;E5e zH+B5iG4kSL5*vUDgfu~RuVxydIzen3#2xT$Ln%GtRuPg*ccXI* zx-sjl7;)NF`M^9}QHj8OOs_!w0$jBeF~n)aB#l@?jki@Iu$`^a?vTligbI9+!WFgX zgTc$@B5n?nW{02xBad&oHyydAeq1PL#x`bJt>A%S z+~!g+;@QsK<{T6}Z~{B$qtmhA`-9RRk)5PkiC!EeOaS~TRUhqmBM!CT)OEOKD_+to ze`OmMwafY19-zBu%4u#W*xDJnbqsnGNZP}9+GV&}ZB4Z5d?E)9?VFF6ouR)AB6}%b zVrF<9{;kU=A&|d8gu&v6{6=a2EectsQ{T&eC@WkvWUXLK6AgH3o? zHQt)7*s~2gx8wOXw6|lL>bJ#;F{tXr>J2tS`GQ7efY|%GBDj=3>Z7jWS}K+jjB-Zb zF$d>X;uN}OR}0js52By}b?S*Xgtfiy!0HY7sS!(+4tm^?foW|xowv9HoB)q|lG}Kh z`LAoY+!~?XM#tR$UiExRsdznS+AA;zeJXJ|Ijaifu>=jNDm{V@4sEfP13akn|4pPv zZt7W40$0%1yeS?Et=^P2G_)(L(}8Xj0Ed#IB-FaiEgqA>`HaCe#2$JVL=VgW&CT(; z6>Ta8hv(pxO5`iv+n^?|M8Qv_%^k`foeH`V=sxU#Zz~dX0!-mPi2z;?;vk=O$C&ff z*a_aVUDQ*@tu`@fbZ!q7cqSjWT$jEAXSm-@8I?qG8D$M-tdA@pHu? zKN87r#dpr=m+H`W5^6g4j`*jiBLKhG(T`IX=4GW2nW}xEW_89pV(`fvxGGU|F=Bb+ zbOLE?l&Kb9wFbampcaYS*4cAch) zB!5DkOh;y;$K0t$|3eX~3-Ot5`|Ub9xzn%p+{ddGD5=EMD%`8LkD1JDs=hRQ0W~(q zad`(1HDPiiu2Q@mhB{pP%zfg%dG!>YIDmM)jLeE5shCFQgmYl25c0Zox{0+)&}$yz z)Y%Wxfm$G$#`iX10ZnQvmhQ%rxh9R|?W1sQmhh{K)GK*clCDG`sF@HlZN4Hr zQYbd}8~ROppz;u79`?<_z4NhIbBw7~*6>m<#eG!lnj9Y7fQy=NT}#Lr?6qC`*)62% zo`;p>gz1Sh#L0K}^xwxxo>gWEg7Z*7Y}14^!YwuZRQ0R)09?D>{j(Ssl;D|p@XwcW zWX~=Tbvm7d03Rw+fE9`mz?{Rx=B;A))7m6Y*xiA$-C+0O03EQ*c)`p|6xEC_frl&&^@(2R?1>0W)`UitYNi~& z>HwRk9bnTI#8{>M_pd;|It(MKp;G0AdK5FY*DW*L4r|6~y~3N{@H%hDi)}ccMr|LG zoDg$fjM>ZPq`78=gKC7*SH*r+Ar2Sgkg_6=l_?{^Yo)-ZneIcxQw6djd)1E9h=+2U z6OprNb=q7wzh*oW)P4>Xh zvdk5UCA=9GF@dv#Si2M}i6WdGFbW>H|LztnjsP|>bYq>NYZTJ-WYTGj$#I(jbYaF| zkuIM>0sG`IMh9Ip7yWhT>A4s_Hl+MFU?`!uqrIs^c=djIR_+}y!r^Vgu?uL~j~FF; zNvae4(`3+eE}DQPI?8JGl$XpvZyhCxk~tS0&OonMVXekTwyL#yIIOiaiE+~gR4GC6 zw5D)Wy&*5Z_$)%4jsrMG>5Uyb~$kPd!tFSnTk_POm6UO+cO{PSBy%is-eJp50 z+8${JDy3RFaQ6Xy#zkzPd41%C`Y>5K*Bl_vrs1lv`h{ zCSMWtWkK~9bSpU3ZOopr4&&X(al0Yg=7gy(JCxZ;e65F0z0nS)B3e|NAcvXTk+F2e5)6s zJI!cE=t%;rX7wVA!YssG^8$T@3CO2Rs$g{IjhNvmSO-GHXhbQBTqwhEbQx6dt)caC zkbI>snS>$n1HA@D;;40fisF7yIk~zGgF7%qm7aYvaH&#u?~^bYgYkMJD3&An=w=3z zN$GQ+8MvH_2lO%ysYC;#mR0zBP}1PrSBUNNGGwXnY1^7m4D0Ksi* zb{I-&2R7-IoUGxA+o|hx0!b>F&4oBtnbH;KYy4C<{L@OjNk_X1$2VZ0 zA~#DI23Cb4DHg-fm1D=}LiHcd)~INfzN+KSzr}ZsR?M^j5p}|P#?8A4<3XE9k|AzB z9;iUm0(5J_%vRLz#`;{7$(+w|sAOGA4WOS|i2sYNw}J1vuHOHz_ZVY?34=({)W(g* zQX7?(6v-H7o?xnwBtX=u%QXAUNhNnik%qXozOgw7{E0K%EX#(aN;)X2>c< z(`lxJ2>UN%&9DyIzd~XiCnU%D9AOnxoB)dZ|ID&JzanNjMp& z#!4OAzQ_k*GS>!;MnF-GdF;@#F73Znr%Z^afJ_=1wWvY$^K|GE4TTcvP+qyp;XP=` z24&>p!AjHu&<@5hv~jqmNz+@kV6Dd6rvvTyNCBjcdDo3KdUj42()W2LGhAjDv#d3; zW7Ngq#aPyBabZA5!eLrFPS9VN9FtdFGXfy-c6=dgMBb1Z`I$sO4Z3b#Sd9Eujae&D z0Xg)RCpBP8jBU&g?P-U_ihN?vxIh*a4W!n!6lQvh-b%PvtKZjY3|@nLf$jF;J|lSK zrzs82>1CJ3BpJE=>x^$9M|0^qb!0i3^Wj_4Z0TnGiKla;H@3*|9!x^A<}y-j6ZDeZ z6)o4`;aYEKy2s(2{05DCR<)e3kf-@M#zuC`P?5s5kIoMO>2!z zx@{%C)Twi~#g)w|-*S11Oz>ZOs?^NtgX8rW`GrbNuF~J8smnQ|>2>31p}t{GV@z4{=6HR_tGw~ZNT75h(=|FGmvLxa zOz+n+-}g?vel=I+P1=!*b{q`mK}Xa1y+NfpjYaEpfyMLHw54s=6PvVav!>w)07%9SoZn8bdbnTDtI&ITg3)=w>Z5ol?Qmy|wmtFuN1vzgma2>ve2}pL;%g`_fD_pRl(n+@erudyN}-Sj#g29J(Am14=g!p|P5QAJq|*(APVNKD@VvLK z(~azBM;vT`FJg7u^%nSNlm59`-!VRXh&|9hi4cB!51YXNpp}S5pYrxAsnQKzbn-Zh zyzw5%P+3y1qvvWgXy=NkX1dlKLSJNf^E#csI%3#ac7NOb)_YT9OSTSuOGciQQ>tSg zuRnDe+re+u_6O_1U@l zWs{zUs4>{MJq6(z&RMulw_(Iq>l$Z>c3DUaT?TDC^;JFeRxET1iA%F=UCeV)asps@ zye4?9KkxMDt+g6er;}&uZf8hYZp_G7_@c9dEcz8z^oqEU^i*J8pB3lD-*^UUB5TCMqo^EV;6jKdPg4e4KF!^?4c=j!Tjdjs_XyB&EB~3L37*idd z@e}r9%gL2gYwsFuH`0LmApRl?)7=nCd&-|DFVTZ!G@vwD5!;^%HsM+gHrUu}v&OOI zI#j+@qsx-T>pMDgHT{)(@JotP8(pvxOH9JdH=?1&yO*W!t`YQ!}C$J($-xOMX5 zAdUKer+dt?GakT&o{|PfY1N3ovtbk8dL+ zCoi9D)~F7h)}_Umm+?`cy>yxudHh8lKfdW-wjKnqK(z|L0@MdyvOlQFIjnU~U{$rp zvHchI+8w&ub~}d;0$>oBS85^UmQ?~^z_ZPo9g+CLM&%kbW?tkpb4zpzst?$YR^_#6 zV|%2ia+?EI;mc^Tl2?#)7AM~ylzhJ|Ihl;xCLQM8xE0HjxwCsnGtIOBa<=C93?jpL zDyQQiknPt9I#!tRKto%b9g5phal&eSh)Xq(_p*%uZhbxkX8+_-tbJD3MjDrpykA-J z>yPsuE2^~K`4n&t#LMEDs_KI=cv)(@((T6v0AoP-bhKzEGQ8CLIDMIBb3hlrTpL=H z6kXg0@8MW=MFucv8F4@_#vrLdgg!xFjOmpz!$Y&Y683>q{&I%FRLl;<+jk7zWOkSq zbSX9zt)30ARwJPw8>A6K+|jv;0K7Pjp;qSG!Hq7}Lmw(5J+?b~opG(Rob( z_7q2a<3RiMS~OSFnndaGKEzqQk&a)A?_ar2JNW+9y4C<=7v2r%xH*`XZ*SAmw_}ez zVv?2@fulx_GOjEsH&Ne1?11NOb@!>$epW{?rR%)MlDS$>KcyypC8wiX^p|Gc+^4K^ zsS^YovR{WS&CQ!%ixjJrSViRxU!jj z!^ z!oM#tgu|PBH=~bAG6e|EoqdjmGsTek`GI6U91s-WEQd=e#mC_SV^mfvKU;1rHi!;j zGc4QizhwUB1+?3p3w6PJoao+puq>wq6iuLHRday(benZ*0gk4B2r|(Ef#L?3yxM5y z>Lo}dCV``EEcb+;4#wzb3agPv&()dEhhA(hg^bLR**>r6GykdT`#ji#c0IL8$9w62 z%+hyhuyH%v`v9ZIov>v1Bm34}FcJtn8Z>vFP9sUd5pLD8wb2%jNh{o{Ly=FTd3JOO znZPVPU#*sz`f`mna1b0XKjAW7fWaHqdpzHyo0n)ANfxK7BXFbF(^{RmNv8w4I`ooT zQXuo7YvRRjYs@CBKvvNZQpxGgq`+Dq>6FkHm&D1wwL^NBqdxDCXkZN6w(KgaR zXEJL^sA$^ER4fDkpy(JM96NC9WNHoO2rC3daC?KUprbgs3}Dq70cijp!`ABiAR_w7 z^CH#Sqb|}$%Rn83LJc}?o{lkiD1~$Dh&LK;A#sQ~t3rZ86nV>>xRK;ZqmCanX(&$I z*r{k`1WGK>Z&&NswHmNVt2*OL-sOm)8vziG))ZqWa8cgTfgW*%D{2IoVQ%-Gr%x`4 z5G)6b1WK*efHu8j`wXp7xXl6~YfVdv;6!bF-Mz<1$B5mDGgY^u^h_Vr&(<)$`G zZ`TK~nV-`w&RKU~smq!*C{41Y*7&aIoNySa7Q`DiP-sYQ!Y|M#p;HssrDih*VIRmG z`HXN*zvFPb(>V^DLV8Z`&h#7WeMY)IjyK+HR82!q;}Lv4j{ZH)DIr*vYGIicX9AK9 zZE7~5vB8AKJ0>)0GQ4qGGj3p-3GS!3z z8{?zqC#Zesu8^J65yluu&8N4i1<+-rv5};Jox+~kqemNK>}_i~E8%LL=Ebjfvr>DwlO*W0cU~_a!Bk|GCj$q*q4K=k} z#Uz+XU=nUM=4nc)^(yV@k+gdxrF9~kfZr$SlV%h4m=hgqAE9K!S)i@Z-qjoLHd zG20O65< zLm|*PTE~-`2ZwkhrG4gM81qo089wjtI<#y_Aw6B!V7F2*;1_30G_@+{U2$%^&VtOL zEM>E1cdCk$p4yo%y~sj%hvS&`pysG7c|DV2j-94<=W&KP+4N{S^XBVrZ&FiE%jtO5 z3wMn`#(MQ(vCt+RkOmPSFNnB4lV5h&`Ce5Nt$|9*k^{MV!>bSU>gQ9oV499%=51bj zQaOI>!SyaS@z8X)-&3#2 z$iEH$fVi+awaxmmw^^^XSz?%tm!ICQnE>p~nvtfhd7_Q^;{0S^iWiOw)d!v(THx3Q zeH!Tkk7^1Ek6O#WCH+(+TT&$m|ujzfNeRlRfn&RYcfz>!l<9TNkhCYWLVy`pmZne zLLTS7z#)^%lA)BAl*K&)1I>QGdXdT*$N1-|*fU%KQraBnsMRQ~}v}yuf-0Fl#0x)Ao zq3hrR2s(;ahTvoju(>)6^_&!1G-_^xUYHlMRaCM$gRV(ywfCl=*8mB?m|4u^G!+B< zh*J5K%TM|-`E5~hLc#?y=SB-zpLFB9e3%xCYMAONH97`ZVmcQ}WT>UHW1B)}o3~OU z{W=Q(S{84N*rXNM{|+refxlI|JgcC`N$^wzbTw+39odI{tYE=M4_L0;qB^%@i+H{Yx$7!8<95c8>pYSf7lApAi-UM8Q6HaO<%FQ4}OCJ0b9aPWFG(WK47DJbWZ{Wu(766g;n~u zTXg|8D(ZqqN(HN|!q0o54t=YdLnT|Xvd0kZy++6srxueDs1wzNnGV zp5`=OG~U>kq-Sc7^W%n#ar;EJ_ZpkTpW+B?5^(`TW@`$wJy&Ph2L7;FM||AzEm@}r zQv2VoA8ppnsRX?s+0Up0b4S-}d~RpPnQX9^d8Ag&2%cwZ2Z5*UEf|}p+}5NeCX)v> zXMHujppa(G_%$ym0oQ*w>1F4Ri_97kyj*WcIngV7T7ELPEcq&S-qnu3uT;Njv4si@ zby!d!?q|G(fI8^k-t`la9YR;)Rl~yjp-}hlrnas%RDW-`X)BrQluu3zf&bA{y7+{{ zy@A=&si?)IaIXK8;n<6+v;jp~U0nA$32xBaIPn-t=c=kntvQXO%CbdYYW4uG?HL2a z&Wd#!;}_?=j7keX?P7l1saoTlS=3Q>=@UCN)&l_QK;thfiu#wvRR8IXz0o>MEdMua zMF8IAdH9FNFw2my!_3lgu0Tnl#W2qUZtmZnhh3JJJiw;P7?sIbesHFJ%|ltxjs4%l z6bESY`iZLa(=Rl!PtDPnYjt3a4xO#hel+yXsrz}J~FkOg@K%?vk zla4+AoTUqMH)*|L9{R&lKb@@2q4u+5@IDqF{%E|$9~qG&A}+=BGG_7Cqi9h^!uVB?DCW)*IUOFT=>Comy_|kpLtpV(R%6H?f2BN1p-Dr)b>OlAKP278$6XBhcwxKXd}9d$@LZQ(-x_xVO$Tpaeyzitm1T1V zfcYEMuR&k1Xru;0fc@VZhMg#~$ccl8JAzO6w)+@gKy8lU!CK2O-h-o9qP;tmkX5PF z7MKCl4j>ux7!?gVXr2~Re(nagaNzjyshhNDeB@ci*7TqSSdM5~?woZF{X>@uw(226 zg-58RrKAFVhy{m_)N6W!9&3sf*`pS1Gxgf5;)w~FN(h{GZKMQvo9=o&Jl7=NbbFxLvx}4WIpBkm_sf#! zPV#kK%T@7cG+syn!>eh!1k7MBM&ZebEWAC?+{K$Pd&sR9h3dZ4Hisldu31A;JqG1A zX-sF9HDxf~Y13+q|7Km+sW;4Ekf%O4GXewhlNZa9B@?uP6TyU=T?9o?ueza5^l2>4 zfJCOxSdc(18 zo$j2UA(TsbmM~NL!v0Ic*S@YGY02of!k0HFAC}=4K%K}wXdY#(JQ!u%O(ml6$A$G5<+!TxKmS8 zZ;zIj$d%TQi z09NXjwxEno@Qxaqwq@{hfiX-tZj^8=f3${<*YlK+q-ScHrqyb??fg;M6EDPax@hqz z0x^ncngb69ey}GcH$?ceKcp^khbMfucM)o$Xjn}J4cLh=#L$HaClQau?joaVVJZU1 zib}Om{Y|3QV#{$}<>)|yb2%##@i#jc^I+;<`T(bYuG0*H6f_UkTFmFUZuKw_B+PT! zw?4nb=Dh5tvgFHFc>WddFij_VhBwaC1KDfdH7B0f*{Bil^DWV!as}_`-Fq`jNxyv) z5j!@E$vlX41Q*$tU21TcYH9j2t1cA!Jjn>VPHs>FqI`=MD`r1%+{1Y_G}Uv2_xwzg zM&|T|7F|ZW?`ECSJFy$WMiqN*pKH~SdxPM`lIOWFxx*RuP1|~etZ7{a??_vf=@!+) zKBYOJGQtfbhvRE?q$`8)6hkxcu(4hz%+;nQo#d5;S!E0()6BAvciOZ(Jhc z{nBPlC$ZvFR@tiGvoY3c|MrOJ$;OW15}6MDZ>wH$vB@)RD%kk4Ek@c>w(a+}S8}{Q#gD#t=|C(39dCg)))`j;x*C1iv_Mn0f zWGB$b!jQEZg-LnP2sm!S$wdt+pBJjm3LbBVw^8PSXZ&YuFKhJgimIqH(wTB%<2B8FjOi)R^` zpHvqmPmsNG#mCREKep{TDt*WZ&(=ltLT`ZGD>Vc8Q1i#iLc@Em)5}(pm$22H`>t^Q zcOYnij1pX{{RjdBuuy7dyQgVItuC&M?er<^+idMY_o^nnO@}(KY~R42pqtd&d~@29 z+qHU=e$5+Yu%wV61;M<{3tgdzwFeg^gPcKw=$Hsp7~>9LUg%Q$>-<^`nyH;>l6|%< zJV$u0E^pFL+z6x}{8?it#Gak%r zPv;-=T4~i%DFJxab6}P2Eyw{rEhT;nEF68INk7kNON)NlthPR=>6_kfU-ni%&8#m- zn5Q51YN+X;U-rS7rjV&9xzznezGI4eUPgfIL;;>X3;uSdYc2cRaUXI~)Ht8n=W)zG zAdQM4!o#8MYW2njd5(XzhibTDhYs`O=oK~%6^Ea=UwV;2npmmQIreK6I2W>_--$e0 zNV4r)E4;x-sf@9fV@iwu+5Az(L|)1OJPIfms!AG$ME>&3QER=m7P?L5?I>M%g#P9?P1^2V!mUJ00guI1Z*zUO;e%D^5a00XLfY!iy-Xptq0bf1{s>aA{`Vf=Uvb0P*Bt$Y@f6;93&U$Z%i-C9BoZ42Ric zR0A8LdDl2cpaZ}^FeDUEGd7GB?LHc{tRWD{1Xw$QxXq!{TiB&X@$BWAW=Rht#(p20c43*u|WcoB*U@t=h3RpwW_!FkEPBe_fVL`ARaJ4r!mKRD}CClQ)Z#rw1iZ z^XI*?R`9e*Njs;Zaw$k|rHcqqb`RwbQ{g_U* ztOL|tsOI%?D7yLDiRwUeLc*miIhdI=Sws|B+87#!k{MBoLK~Loy@r&6o*}efqvL3Y zD&7~BX#8u%zf?FwQ}}F)I&M+k8?nbq(S$T=68}oawym+shn7cg8A7CA+z{8#MMOq@ z4J*!r$`Z0ge+7k>>t)EIvSj6vAvh%R<%2BESZ@+w@TFvh_y}LcLeLb>|H{HGE!Snf z#CDc1y~X+}E)Oz!sh<_lTIj6^!>zDkTfaTSAow03(NthFJ9e#(Nrf@+W?S{yvZSF#A9|m! zfQWOX!0#~6FOZdE%#;yT>qyi-jr!(1O{HfJK!SO`E5QF;9$FOhu<#I($!%@aT|W`! zAq)5g8w4g~ELxp%enGg5ySWpD6mYq|n(5D(w{d0358Xcyv3E=( z0t|lVi^`3PcT89^k0ghE;C`!ewXprXOW6Z^dmHeFbIU_OUd)5o=a@?JyzAWgb8IXP zPdv|)Lzlw-jOv{D1!f}#8`)sZBC;pO=-kD3wRkT+!##@drUy(iTV81v&VR~Qm zo*c_IF4s|Pm>G(bPOTYj3GWUBn``X}R5_Br=g(VXmkypDIB-42d4~@g9y$Fm36rtC6HW=?1o1UqAV2-JzpHR}q%gR|uyjtedCX_1{gcw8 zql+fgO2u{B$UOK9+1>>+w3Hb*B?eZ&bn%XVcLzvnLp(W4n9tKASmqa&JgLGr1Y^cq zk3doL&1lb9&>DlJM^N;Ttct;u0P3ENA@NmT;ORcd7z%d6v%Y7aDb^@Q}KR$v*;@4ir4Pno{>!$kS`Q`d-xwiTX`Gt+tPWoR* z`BVS$r})i0o3BMx_Qh^$k-~t}eCu2(DFEaLltpjV@d(2#m@JtW96p(M?A2JGvOAbe z_rNSYEqYYWIwKCttm>Y1xq~JkH%L|KKDS5A{b%7(cg^9v#Hg zteG0y6w=w!{t2pv(wE~tV&0D5VZev@fS=$=+kcu~nGrnm!ru0qUXJ~F9DT9)<}1RW zE&iH^baa4$LAJwu&lH+=HiSy#UPKq3D5m#k{{AwbDW1r>Fq13+Q+<&Cz(U||D)&nG zO=fkvXJD4cw#cXNQkrymrTC|%9xA4rui_hU7T7vAcDFyv5BVTJ996FQ<(gcsIpr$c zE4gs5ApSFX9`x?!{m#;D*hEjv{C+&hLAB?Sp%BfWlJ zc`zg+ZFT?a2l1>symQ!KjA5bw%piFw-i5L8sj2?!6#t)pV%Q9lAM#oLFxFql&-wTn zoCUm0gBH=^jDx(=i5sei8q|-35%dvukKb>Ip99G%Dr5dj>ovO})^QdYnfg$(lr#i! zos{cy_>lt=j>hrKgO`9G-r`9fdq9$m3)sMT_&-a)07mVV+~h@Js+lN8xYlp+MgNfX z>huy!^*=4fzxZD#mFpOP3Gc|a@z?YaEh^XD>57nV!qd&rr88A=Vi*=)GoP12W`j+D zbzyg5Ir({m#%=Y-u~Zy(j1_PNt!8RKIQTYM1OBeT*c|tCJ=XUWv&q9`*%%fu8=k=Y z3&T)jU_qE8{vIfCmJL@0F8dTM`CcG2#NlKaO1ZF=Gc{RfaGHE%mKez|@c{(Cff@3i zb>kPOBcr~rS|!}_+#s$DX0aAx2{~?*g z;|0d8P#!LT=2UKWm)8}>KP*441Vjy5zz#QrF@%e&P=X?27G8kY7w(0zLTB97)t!*oPs_4*Jb6pxID(0^BEcA(=m^?8?Vn#3>9ku z%Z71*d(4wzF{Qh(bGZ? z=aOP|e^&V=N$w!ptR=}I`pbUFn!$Q*50w_HJ^lG1%G)m~9jwC#>ds;1xaAy*sg&aV zvja)aZ|IS2Pt7U0G@_p0UnaC4iz#(+kMVK0sc=K&I2PZh5;xa9uvN>}Yv}4og1_(w z6>_GyYv8#!p{1q;Fl*OS72ZD?%KraQ)M~M3PNqbCLhKW)L20#4a<87(&kh0{zs7P8 zZ_)58tn4}~7R{c|TPH(U3dqKea%cR}3VUa?r?u29K&D!m~(ydmEH&K)Ye zMY&bc7NhVHja(mcxNXlUd0Z==(^Zefwd~2uD%ql8uc-8I%DttEw{<0{@OOfR0F_mA zt29Z5Q{}@HO*bC;djJaEM2Wi zIB<`D<6AwQ@eZem`$NqwN{*$7o6l0m2LSLj@pq#E05qBa-@!gPQ~S;qA{qGS?z2MI zH{y!e5~RiV@Pod~sa&bWYxF9aac*N=9jz(1`-!L-Y5TC#x4xFXtHS@HO;Bx0#$ff zORvzCEUU+PH3@8WDH8(04|z@WwU1@Ra55y~>sq=M~WHrz>u{ ziJL@*vEsfllXai`b~McY*t$b}dFhY%P(RF1z6}NXb)5|7R;iu56llLz+w1hEXEo&; zU-S4cGt>A_0@Y?s^sJ7*SrscSBg~}r@L>O7hSx`N848y7+f@2+(Cob}KwqN7sedwH zhc>(@(#P`N^95-QltV9+cLn z-^7CYIQQQnN<~6ioS$4;l%$0rqnx{;5cPsI|DG+=7C-1>$T#N>=3J@%>4R3P)cG0; z;ooZ0tC)_>+D48teFR^gee*4hzgG9vMcq@X6;|68RgRkeW2tnchX~Y2i}Kom3oqW^Qz^@1j04LuJRX7R^lw1UOiDIa85eTdl7- z$5FFc-+=9ZJ0hkfJmN!Du%&&D81N9Eqxb3}W+aJ&bD=1BMT@Q^W9!k9vO$Mj10mqi z-iPuQ0XCa0OKQx5)V1t^Bg27BCz&2BFL}UUAa61=q|TUeCBa0FH}nwG^CtT%(yV?Vg`~()$9k z!K|s$zOI(y5bd@heK4Dxaja$yG z{sw>&LAs*Yniu>=O~tf+4|hE-2TD5R&EUFdt5btA$VyWywY5 z+GQ>RW5|E+@C*J!SGiCoZ{XwQ+Tow1KmX8w{!zJZ;tpS^l>)jz=o16fuTcH=(A3dN z#t4vq5pvDU;5@@e{MPTe*;IFR&jro@=RWbfEg=ZI4T@{{s66vVGJQT`-3IyBpeRV~ zAsE0~rk-oWQ>$pdfFWwI1qj1vXtbkK9j~h5twpTLYS!a7ME>+X8?jjieut66b-IV*XZ<* zsX%d&{n9tOQbU^cW&8K>ew`C|9q^&AnO^E!n2UKS-pIP~E9}C;FF)JU+bVr2Dqj|4 zi}jcErGQ&qapm*?uh*T{grx7)#SJV)$#1ESJR~}{9shMr%3f_|_GrB-NV}tr$eLu-Zx5viwz&&dufQV@hw$u z(@WVJ@h(UBU2bwe{PW$k>Zzh+KJm`jBtesqy~TyuTReagqj$??Q2GBHBC5_oS>#H3 zbvUdJ;cD9AiJ0gYDDnEd#h?8ad-E2lDv71ZPEf2%yCIWL&~`7%MlPv;E5sWo@giMu zbvvziI6((xEAk6phdu0zQJn;Rk?yfNdp8u$|AX(!w%Lc(A8oUpz1V}AuWT*i;Gno} z_{bCt>peSHilFJ#^hK_-4_c`+(#>|K#;nq}ABb9eEJJ>_It#P+x|jEYxaQS3M~?-o zAG?l{{}(j&#kiT}emA$lRsf}X;3BbMS8Ts8@v=mS$VlJ{vRD4+hpNa=HWwwsDOn2tE)^1=lI{P-*XomVFI$<7yV{k1;JcZ!$qFfU)v zo8GI-dXH85j=eAVsAj#O!WW~6ugmYqI`-n>4|=aS&)#cxesU85xZCul>-bXN*Xgwt zwb9jQ*T)UTzx_H-e0(47lf4B;X;q)M_^01ubMH}dKSOMI-yJyJ3smxA$SOWa?ZY9=`Hy$}VI`ktK^ln$GHHPeRr{N7>rPZoLPXSfd`Bfr)6 zubq+&`_Fi14|~JMhyBs1pRFy~dHFx#!|rU@E%dVXY^ON+K#!T;_}lxO4q|qA80LJA z1BwM4jkI9RXCH&yy-nZEhP@%%Dztt=zJ(oL?GB2-3@9Tqq=h2_a>O4<`tW~92j6-> zhI`-k6939iI4R5g3tQM!<+>=aT|xYGF7vEu^pbYtz0(V!VBK30V%spNFQ*_NE8eq=GukL!U*$|vV3?@CSjsS2-+ zy6j|S^vo}~MFR+LpHjcqG=NLEcBvmCOqz;+s8Y0`MIn&mc2pAC_Z2I5r%Kl=_q@2! zlYhz`qLRVEN{u@ydcSusPzAXFqJvBH$`x@dLB&tik2soHg;pXwWGYx$7<<|l@%s%b z0a$L*q|L$klSkfFK+LXT15{clBqF8RmWaHGw?qm*E|?J|JSlTR+%&e=kQ#(gpwuu| z6F+_o=_YzuW>S7~rS@;uu0Go%5XdS$xF)ll*_P%oF32p&;>}t>b5U>Ra0KAbt}u_O zFDUY6hIRRSQPRzHeI+XBO9!fy%Z?A#;a^lGR~wHP8b62#qMqTE8baO2kstN9$<5Hn zZ>wLe9y&hsMF^Z0a!5}L1=^=GCgJu*-Th+?YS47LuFa41(?4Ku7HPy{t+^stk=tlE za81O{{izxTjetXLQa(2V->F%vG-OTOQFRez_>aV`(_gz+w<9d!4skdulruDi@cuu- zt|7_jj@;jrN=BExDJqN#-%;_uW4-cTimZ1jjjH-3fUZc^WJlvDd-hKiB{y=fI?nyT zxVv^S*Rrz*2tO@%Oc!*Rm4{!nXa7!}M$FWz8m;fc_B`C6A97VCujw{Nc^1{*#jCN6 zy=rKr7rUJoXZfpbQN11^aO&N-y$<2uww|&_B89iNN9(rq{8AYBO}K08A-l&!7vJ+L zBO{z%K|-IjowK#Vc4jVnmD}Y`>ACr$lZMq6alO#2UoF>M2E#~9T7F}V-lNHb4a>D{ zs&c=h6yWvi)UUSnXfo+vN>sRDdt8D;PTm8W$a}uiBm5OD<+=YZrx`8!uWxi6nU3u@AN3VamDoRPRRst6UQMN~ z{c3F?UEZ$e9t+|9PFuf)>4jzQgeoTgAAtn8I%Gt*1`%o%BzG1^zfF3s9!OvGbj)e9 zCdcwW@o2ZYRN+7a&f#q13{Df_(qgw|K}fAW$f+HM99z+%TZwR4+&*0~DcN|O!ctFW z7oM>_y2LMdGD-u+ZHZ35-`b`@?87cKdu(Y(?!)urQjbs5C6diHAflKYy}_$=80qpE zZja#2R*=;qeT9LY)YDw+YEL5_U4EC-cj-UeqIX>EMjc05Ko-5fhZ?-up>ZfsIlfn^2dW}Bns^Rp za@$dzYHDUsJ#QTO)YIL}@DReS3Rz%oo!<88L0p=e~0>UI*^6i!M*4%9NbQV@dpbnumuLqmU?W1YgOrrgZHwP zP1#P$sm{&m1bWeUU{II6^-a{j+|{QLU>~pPznQTYl)M-UnN3WT?dm2UQl_ZN&G&Gc zuez-4zDk{5rS0heocT@abg1j~Dj7x!q&-Uu&rk`dvPqD5A9B@Zv$ieQh!n%vZcyE} zdY#Vm^8K8`J+42Xp3dEUDN6bk^FtoNB*QRtfPLa+YAWw>bCMqqO73B1z=~z-jm28e53n&iovbpDgOV02&z~zyMhht_L9m5(VxcKsSP?TNl7SzzKpk z3(0Or16Hnw>Hm)^U{|=L;=|(iK^}DlP&SMP@J~uxV2HUT=4IZ#DHyXDNcr^*+!7+pdKxHYq4O_3plXe zV9?+{o34dMs%PTav)%QI=kWEM9%@mix7KGN5S)=2;RRjp?!jp&x7+;(;D-1Ext}Txt4+>XpMW!laeQYd=qKA@+@v;wsc3 zHfy82vi$%y0LYx4r8^BH1UfQ8tl2H$@!5S|d`6VWYY%r}LmaGxJs2dq`IW+5Qji z)AZz=K&AtCuDGGsN6$Uu zUswL&AsgGy{DAN|Sx0LMQ*?~-jtjIr)>Q2;(SV(%{wGJft>LFC&r^5N4}+#|@c7^S zUI4-!KZ$hksTU}3Q3TPWmqyigh2QAx%R(@ae^Ffi8}D^_q}LC-(Liyfue{;B-=w@d zt$Dcx;GdXv@j|34E9V22jl%+jM1zF7#cT_#K9nntJ$5ct6}RJEzM7 z+wOr~kfKD7ZH#^5)pgoZqo&i8%Xavc9^NY+8xHYYH^ogMO+|Xj+=TF!b^5l)HmohK zaV%}upR!3C#X&O(R^mz9*cC#rN@}Unz^5R&$#Ls2;?}Zc!vvKa9&YiR?A(=L!Hx-Z zeIVUzj`wxOG>odzw@!|R16-0qwls?0>(wz=x4^X`BcC6dpYb673zWY|`HOYc(&)1F z=0yP%Z|lz(5_bIX%OKX5HSF)3e@p zz2FDDv5x*f_iB&T`fXcuYygSH!FfR|Uet=sI?wm%pit$!kggiFLj~E~&dW~-rgj_C zE_m@ItwBZ|G^9dT^8`~mTl2C}?Su-jal1dKFS9MLi0u3&S892) z^0Bnp1{l0XYwQKyZ;cKoLsn~3n^yS#zO7E>JnZXp}!}Xq}ansaU z9o-E6C@A5kKPSMxbXze;pY}L4XHE^}&8XuC*plup< zek@N<*Q-l=+ob8f=`!lkVbPIzhw7 z2jD^QJiIoV=cDudz&R<>ucsT*{1-^$bmv7adIp_pb9jN$bz0V{GuLWRyNbZl+-=>V zep__~%K1jsmnDay!E4kstLRT_)VQ*wVtgc9hoLX^7Uo}dHCc)u=8;O!oX!xE%UW&ktsI4EByxuuaZq*xHSBD>= zC!xy+4sgQ|FnPcN8Ys{fF^*GUjE~<`}m^t*-Tr|LREfM8`%SaTq>LxqK;hUo09b5t7&&3SP>=tS< zYK2&=67ZxMuyGW(PKY9uuT@0j`el=Y6*=&zXhl`=ttj4M%*&8HTTn`F^y~(`GEdhd zzP&_K?A_2lXOY3@c1zCM;4Ymj69Tb28Lh%R_>64MpWs^l?^emc}*-`O0Le(Brs zvQPd`gZ~kxtp2?y47>=(R1_kpL#fUAj0!&^)PU@hQQqPI!VY8@jCLgfP*2iAJAzBR z!^u7zMO16`&IUJVIvM)(dLOh^I>aOl^(G+I{*KG;^__bAqh#p+Q7`CmE`=tNr7|~7 zmv_@1Dg%W7sZy&ijsd1_sMFKEP!KNHW8ld5W^EOz(ERBBbW+By)^R?q@Rnzq7?4&AK(F^zAj zv`<+#932r8`5M+2d+&f!GktDb^;f^b z5>yZwOGD0LU)t%LXNaI49;r*qw(2PdVkrej3MneT($N+MQL}+Ta*GNYQEt|&)wk+G zOtrB_Q?QvH;q7y@(OQMIX9dvOkke3HNwfMkuDS2-u+nT8TuFtI0CQVoi$vi+*Y{lc zaRgY|71o;wF%7FZdfzQhXwfsaDA{|^M?u7biR#brCWm$!>sMK;pE-`$o{g=`)YgMd zT935-mFqLzpwsuy5J$AC$dh)z#nQ}MeRjFeXFD|uBg~|A>E~=+YH4@q zQtw{IHn=#6dFy4~h#7tBO(YzvG}entjB`<){!pXI+1Mr$Db#EGT>S+SbML)>T&_S>G*H(T_C_a52*C+ zQt>M7c4vbH_v+%+dJU9fQ@|XanBrCI(-uR%vHiUFO5H(pK1;ki`Y)y1@9P{E4>gUy zQd2gpO8-scFLq!Sf4%6Qw=-yqcbf7Yf&p3cX6p)Dwlw}alMPAR4M$<86NP0V*hoS! zBCoXDwu7(DD2URq%h0PQ=JIu%Te5(QGB*M)h@eZ_^oO}V@O zV4^L*=^_VQ2n#0@bJ6^qS0?P*rD1&{uS?zJb}0#$GUd5<(1Ttk_76Nmy%%V%Tin1s zR{Gol-kyfG1yP(8=I)P31ee6s0}q-YU9nturAYD)72XzX^QDHn+?>TL{1vef4Z${P z{^p= zig(Tss&_eh?t>ixLYf4zr6S$`J{P;Jk6nG~YTaQ=MK39OV?y`+nA?tg+>T8tP_x`$ zFOF^`^h_Wb7yD$qx+c1c$3q(Dx%v~p+tYP;oklxl(~;TIf7wiJrf4OnV_P(+S%2(3 zWyCCQ4DyiaQ0(W!7C8@U(?FlKw>PQEE?|rc>gT_zJo7D4LI@R)M>EmE@HR0pEI>h0 zk;-U|euHlT=0S1%DC&JEhpag(YFlQ_i>!K7H@5~1ejHp{mRtzZqf6LC{ge*&)zN|N zz#1KHjQUu;RyOKhf*QlEpE&Vxd5a2tBm=jTGHVU#U7oGPzuPr#lW20D5G?|$wraCs zLn%}SRD+98hm<9!Q%p21DwURF0Vp={@-aNfpHgg*VH<(QWGab3o8tqBo>Cp!72fDx z_u8#!R9%C9JWus>oKD*hUK3Fvv<93;;(-DG@@Q-9d(%M3Br^Irrrul~S4W*^Y7 z)}hZ+3t*&5Ssx+T)ocUP0@#E=;Mb^tx|P;QUNDl1E=@P)A4#hsW}_;uVOlaHZd-so zu5gnXY~$l|#JK~;1JtKCd47D~K_0WjJF<=|6@25mScUbggOY;It?AG}d$bQgt)Gd8 z8->M54xNTGs$-E2J5BT3X}YC4+$#^d1dwLL_$u*EQ*egMtvM_Nx7-#0jQN3f8^aOa zrraA*Ig)!%wC7rRa8SzcHEJpSoGA%;B_7A6E%@|OhpQ>6yPd=0-a)g-TXAf899G3= zmTcM#Qzg3`UX1~xUfa{E7bgynN*8@>$aYeD7XY8RahKRm|Xxg%CXa#^b*+ zgUvv-7n#p(s_pX%Bq+hmMEbNu19ea$^m0-1BdXiD1E?y@OP=lCFsG`kYV^o#Ey~t3 z2PF2zg2c-U8ImpVOb0&sz<6*4U3zTjmT%I*7&^12h5w8KF%BuSHx?i51liGmLvEP0 zn_lRqb9t4*_F@wTdjNSb$&NK{_S1~dmey#qjBIj9lU(XxlWAI)7kJI_t697UzpPccm%gST-i&h zt${*9lFkVK4r{^ns3NY`WGc9rC{DE{(Dlj zL#2jVb$q1Pi-_a1Go#FTPuqbzvpsW=+xxtS`(}GPy`E-yfEE!6vW1-1oJPBHlT?tX zn1#YN4e{9GFnSSJWEqx`9U9;nTgrCY@e%49LjzKLIy-8#^3WU}9!O(%N}^V(F=N#V zFvT7)sS22f?riE(D)kBHh#OMFl{Og9_W9B^!91KuWaru2-0$%4|*yMMUSlh=oTtu>R_|Q83g*b7xcD<$app) zk|%2OvTkeE?Kea#lAcs!N|st`ixh-|a3w ztV?UQiUctPel8pEkBg(uh`!~s21O4FY+W*5Z%>SxH9T70G`)~ni4)wE=2O{dOFi20 zP1-xBQ+#O`s$gtIPn*+~J@=GhyS8|cr}}h^qx9q9IItTtl>%FFgqeK6MqZZ)^Z(>j zOKP%ukmML`JY2`8H;8dblh4H|URiSM$ZT+b?b*QgF=jiaNw?QL->}lQw{S z)i+^{Z3s1OkY_m;M??ce9j8oafh z@qeoZW*W2Yf29s#Po`8Sj)S?v!QG(}W3~&3s%-Z<7_9?CcG}?{aKF^eWjUh_o%+y$ z;UBZX_p+Tu+`!(OA93n%b5ZhhQitQUmuD5XQox?CjgDctnW4$NX?El>*U!~a3_GVG zSHxC1qS-Vi!~0pdV9GSe3*Y(cxN`SACy9UuW2{txj@a8=C5b{5k{8l5A z@50+=I-*>a_M4_t{dC%SVP^&#_ioDYz@NV#U50XJ1Aa%d)z0eggBv1847-fngE78xsi zL`vQs?jbESMMKnC>H8Xf=^?GAjHuc7O|R^^3hiJ<sI85JKsQE9{%?yRauDmIXuS9R3I-$ZXcHXD~H zov4zOMoS|dyWKyWqJgJt1XnOL2qq_&(-UxLm#Fw+4TVhw(wr9?vc)A)nDp0DWSB}> znCe?J;x1LJ5&$e!Iqtq+{eG`X!<(gCtKFtcfH%)*90`}ll=pOy6&)|A;6>4)0<=T) zTJSgJKOd^mONfTvR_W_1eJ7}labBZOZH-pgK_Mzh{U(tSFarpLpXx3QCU)-({}9n1 zi81#MA3rnt2Iprhb*xwFRMY~#8vBbzYrNW{&3dFaT7fQDrTy3FUtT6QIj``eHcf2T zP3cvwoApGe9_rG*TQx2VRoFA%F{_C-pp*+QCTd_Wy0F!{$d}}nnpA%?8_z;VD@B-4 z+s{u^sDy%Fy*xvEoBDXzS!0S%+0_`*?|WcyXt(F0CGu*+hRN6g z5};TlQ>Ja}bWdw&SqTSzgPhd}HVivM%48K#*0*od5l~WHk!%f*V^oIB<7hO%3@p3Z zZ6xi}tD`5tRleTtrW!r(^Iq#E1NNVu({{RJakt>Iz%cL?J>6Il|OPX>F^SwNWIeH5B=Yf6C z$gsIVBhAW=w+nvM6ZJ|uHq`zgYMhJ1x_j5@Zl4IIi|urpqo-Y2Z1P&C_Mlm-cRRSE z)XfCgRHx@Bbos+6@4@8MsNC45qCw}+3l?H=tBUv@HwI6(Y9^PW{?6cbf`*NcHchk4 zIDMg7FU{1|V3Ij{wO-dW>O+^-{-Q}pA`6yiBmHEYkEq}jFq3xHQ`V|{laA~Vr|(TS zvV*j&C|8k_>Cl@DW8u8m9{H^y@_{+*C=cW|+{g-|;9u1dD|ND?%*a6=E^5$-d79i5 zmtYe9G`8u;_P9R2*qHFvF15LYwt9R>0-kG7(>zUM(ZPi1BrBtB4g|<#UZh_4%#B`@ zgByZid4F{P!msgMaG;+xsv%^Mg{(5!|MAB8z&y;t9Gz0Hwg#Q;wl<3mkQo05tD{pB zxSb>*)n_G$HVxEAY}LODl01S4!UyXx^9Zh8JzZI)#m7VoIBYCK`q^p9{ZNBo4(A45 zEjmjD4Wj$mCdgHv>%yO^;sKSciGFC$x2eF_inVMCoqRBp?}>gku%O63rwWa+fQ2i%RZRKMd(-gFp_9WFwQZd|)9IxzA+#^JX6$Bp;1eM4JT~nC*RcFroXlK#PYk`)`J)-4d{-uub~ZQO7U%yxD2LXq#dpSZxN~Bq*t-hx+#xvU&pc@?S{C~kAhu8RU zw$JacQhb;a+Ua!R<`8YJ(pPDlX&PiA!*=k#t1ar^=eO(f%w^-znmYAA$M2ZFo{AOI zdWlQ<@Zk8bH&|#_V3o7As9QmB;y-%Q8g<22xeP|>IIyhU7z zI8)brH_+qc)AZV`&~Y7*LAb|H(W|Gce`DMOz9gr2Oj=Ihk^-a}&3YUEa=H3lr=clx zVg(oXXXP**@F$OGGSIh8IRg?ZDRw{M{zcD63Tc0${&!SLx3MmD(F)8LWdbrQ#zq9x zy0gqIfx;tJXq?{J;-<--T#nW@~yj&U9C-$>}KaS9Kfie{lCxy$Y^INrOWClUzZUF$Q`bnP0`O&4;4 z9*;85?et{hx=-8lZ3e(;s5Qh%WW6!_*`HFm*J&Thuvj(rqxk@Q1%^(_v;=Q~Ij{DHnvfhXm7`-e&Si zR}3vrdmk}TpZF+J$g9%%=mx8!(Ck9XhQU#(?`y8tTXW;uahl6fuYr9d=mY3p*~=MH z70Zs|HndC;`ujHh6YJWpE6jeUL;6jpN>RM{q*M@?_YWNo&;_%D@2gGwb*EZ!GdA6o zI@I{KW}5b|4xz#aP^@lQ4_EVw_Z3}b-nHQ+k-d=6phd6xP_u%uwKDVqTKI9RhI>c7 z+@>em)dX4qZ$BD{lmNMiQF*S0Ii5=60&vSdPOo&k`WsBOz&q+Nk1A(Ob+`f5Jk#S4 zBlpkNyB-)-w?{&fH%2?$3fD(mwK&p(xfY#GjKET+!WD0@ajSIa8ZB|;jv$aKk`kZC z{?6y7vj=TH-n3F_mwscxGx#c{%|Dr+{4K>--u491X;Nlnl`5y{9x`#ZLWW3 z^SC5Cm9+APuXsDB$6B<+%lb+$p7HDvSiQ1N-#3`SaR6pcSsQm`th2`B_Rb(N@MyJN z`U~Y7wsmRG|8(0&%rZvfXoSMlS=6y%n$}lqvCrcjGqt-$CxaOn&uks-onPIgcXFy} z(F3mh3K~D-Do3o*qgk%%4_K|W`aBgSoAfj&)Jq8jJ=Ys7-TA=dQ9HD6uW=}{;To?K zX@AOiG9kynYF$z*RM2}n$MLN`NfruisI}w_VzN%n4q%{SN1ez`Z|B;VR_*Vt^$8EA zgTZXpBc$NHqvpft0`TCDTzv-S+}^hUGfN^D&(SDYz6QO|iXcIRLxb|#s@H*GTSD9a zD_~M(jQOJKp4RAu^R07qPrc4+R7-<)&eKMyC*O{-0xApx%c*SD%MfB~b#!}VEx9oe zwi&eZudO=YydMybblxmPl^{J9qRHi@WP5Lkyz{Je0jrKH*8w2CDlMF*mDOP%04fL1 z5lTE1iFplrX`XhPw1fs6)2uUKM-hU$E#X1Vt@FhEu4vNo+Jq2c!k7_$o{s*6H+4oh zR~|nv;-6^939hTwGw7yj^vinDEQm+E%k}XYC3r9R&CRiKKzVr2Y2hnJf|_g!9%0xd zQaiMwODiya<5j@#5GGHHYx`ErQBto{4BHNAP|_4l?AiPkPVcT=9n=AiX8LBgxO7VB z8(nd2-}fOjP;_u-u@#khW5Ru-sGNlc8v!<5mn-#IvdqUs%Ry*)UZL>#aM^F1s00D_ zso}LJBDywlI%~f2QCwf5yhS0p{*&{J=d@@(XAtT2a*e!GQ*qV^tud-=<6^8Ck4F_i z{!=P=T7{{^dP`K?4|^@r2PJPrap=;5WGRFEP?bf=MqjVo{mG*3=AW){=V&Nnz%Eg3m6ga?{REHp2A0`Z92|TEDh9^R6_pi*cKyogPe@YEMxI*U2HMph*Q6 zP4=XlU0IJN1fp`gLEQo$UWV~tr{>tErC4r*gOr{`Gq;cR={_-(kx&BWIRTEoWjMOP zmJNJPt~u@Rlp41;3`f8E@jUpI_*46Z?iRJ#{H{u8EQ6!7Qd5(Wmb)GNj$Nne_eV&T zf>OZz=C$hbU3G+JDeE=AQ|G;OOjIhVoqydOA=mr<_t-ZTtMh* z+n7A>Td`oj-w*RL0F9+h^Z~_5DMr!+dkBF%y$dV#R+XAiDOYPqtu8T-dPI$Sd6hIa zAJq^%&)sC@2!EP2D+K^4wlrI{Vx9hMFIEhAV^mhEU%_;3)<{euz7;tKz3j4Jd|#J= z+)sleusTUh`_=`OLb}FXdvThoqkI34vP;T{J5q)ao(IzM1!7We1$Ax3RA9-+z3K!j z>*lv=?7bnJ-jmSS9)C)Dovy9)9fwOWZ&_cg16MA?m8<7Uix}%Zqd0{YFV?2((z=D>}6NhwJ#x?46=j z0tMyP>C>&c)++bzcD?&}Fcmx5&sn|R(XQrZiUxG)>Hf*otwGI=&G0RQ`Rl>SBu`Yw zL{0JDuCzn%;09zWDQq{cc5gKJknYnyVlq?dGZ0XZ@F2HX`Z4d7+czj)V&nOWvDyht|EtWn`^bHPK#%1SdHG# z0)ZzzQDu(qDT`)Tr)r{}vzD=iEZh;G3V!GO+KhjOMmVaWd?2po0t$)U)xAu(t-4PXZ7wNTwKkfK2a?2makE3c zE^5@DX%pv8fZujvH`g3cu#!|P*9Y5}m-)iqHY3T$t^8k5mORWO%GC;7;R+Q{>x5W4 z61Ay9FMsa;$Jd)cS6N;GS? ztBcZ*bDw+I``XvO_TC|_jE;^vN60$NFIf++0rr0px3Nt7fNzbX;&)0RK$r!_Y{7;A zj8ClB&zp37v-V8~E_7puZfNhvY^6|n8`aViL`_qvf8qDc3N#%X0>~_CQUl`^ko0r4 z_KV6ZLT03_Rz2n?Uv1KPApF(AnLd!Xs(1upOP@{y+EI$guKEz6qsv)8y(oK?pHXq2 z-g5mN3?UiSo`$mvw@Ei4f2_=gIZ&I5YM ziDvK6F79xR)&W|Q#}#Ls>nZIAqIBpXSJ@1~ zD*E0Mi^0`kbGG`@e(ZU~G4>$Ud)zBZxh3z-qyT(fQwDOg@}eVrWAPURs&O%RwQUaX zsLL__4-()skb3^bAk>6j{YBGv+eoh`)FMp=ur|2$$2aHz>+Z07cy=n&ft2YjIxRs# zgL_oIUzGF`Ecu*hkfihEs{Z<;D{C|&Sx*e)wj0PDHfTu*8fP2GAs39P#bcIfzjhs9 zEI0W^HN?SwLdFs{9>aoT2=|1dq_ft)z49XyYH^G^uO91omJoZO8BUwo#bS%oVs^0= ziE>_aW|8hF4RZ=$mKIlOk&|L1o!48UOnjYfi&TLDUl_pBlMFlS&^8Zz@AtgcOSd4r z>C$2!Yx>1dp!(TH&BqpzNaY=6G?EORJuJ)v<#+%R0}@aa(o*lt%@){vYGwOAG^Knuv{@+XRc%IJ zW00Al(P^f(-p`9Zq413*6=|`3*_J*Nnys2_4^re#)PV*m zRVZ;a>9_wJjWw4Qc*q61JpmyrEPe)H*Lx+cuDBT>vY>5OdKt5gtD_A;)q|&)vQbyt zQ=}p zO**$(@1|9GT~I@9b!MS3Ij^k56vU48Lf@IKiT`@ntGdwoF=(RlOn-%bK`f+kYoI?+ z$n5&W(A{oOe;aPm&#N>HT83%q)%uu?@-A_UGUo@KQs~aqtP@$7ame&`Ess|fOLHA* zwj^XSl~cg%zmSOW+CC$cj#;D4dX6xJQ~Nj&6fb5+vp)&$a*?-ge^%&&`T48#ZZtKpWWbF#PO{%S=7;~uN%ZpHQL|q0ghkv6)N!k z!A1pgk6YwrP=GKJ)}571)MA7G9TmF2GVmUxbmJTW*kd`M(bwt8N8<+EF}LFH_buQ!D-IM$ec1l$hWq6#nX7UTacf^?Hwgpi?p zvQ)46dLx{`e&#xhN)`YoKF)j9XXEobW?VMut!6!x)>*#f`RziiJ8;PPf2L)PVW*-`)teVGO?=D0*RfrMT;ucGfU4^g=Q3Vu$ezsXCxNj z8Pon>Ytm!Q`d(Tmbm$dpBd;5@5;%XF?=?CTv3Nr$Bc0?j?QOP%mnX}J+ZrHx$xF`b zL>K;ikv)MuW>rvpB>^D-NEqK%!NBf%wKwS>79`B>(hTg|#voVA>(j}_(bpL_TpEUt z%m%Aes;^3wb3(NOMFd6FdX2A8r;ls(lf(gGOG)g`Zc|Z5$od_Q(^(z*kChv+U9cvM zBmirz^JAP~%=Vb8v-j~3s8JNQP^|z|o2&G^kj-WJFu{7w;O7Iq7sFiG{^ z1lu+@lzyM;#qb!j>NT}Vhd|Jo;vP-_jt%e9ah;lM@m`vWiJp*D;%nZ3Qm9We%fgJI z+10wk6Bw(TwAcygsh+HqHlKD48Kr6+RU4*bOs?0HO<~#sAc|iLdM6bQuFJ{@*vYLU zFB#Q`@mt+Xl)g<Hd{iUNLZm5gce!-IA2j42n*4EKA@=%2Lv{-5 zz1(bGHmZ2Dd2~w~a=j)|qj#6eUsU=9E$8OI+PxVP$}yUPE%}(-#9$igER`o#{qONJ zJV%A_r3tj#zD#ZH8U~t*{he8+Pdv~Zg6@^L&(H5e-hGK{iED=qn0m>?ziE@<0+;;tx&3?naw=sRT6OLS#C zoPv_KQ#Cumd*rn%lRPron7FDM1p@Qnsy@HAEKBHF-Yo{khw$zOOslg47!W#f<cw~)QL4`j0S;{e z4tE{dV+-`S-B5I?3NF*oCcWcCz`P}zaCu1B-M&owfF*9y9yf;?vCxm=hsyeCNUQ`& zq*r_KgGxUZhN0uir*vtJacN#>h!^(6Pi<0-4M?6-!-elC_dV^A(I_qeZ4PsyFYiz~ z743o1V?2}V3U@J&#&-C{N}Xky7BGWgvkLuiavOPoSw45jjVqf~m{zl0)yGc4_B#cO zYQSLsyH>B-NwK83sN{7d^j;Md5PC?smpDd--#|r=Fft73u#F2~ zeEQX@{x%w;+$m+59cV-1K({%ias*1fown2~OmeI_fh@D=2ITq5VYi;*0&gIoWdl~Kjjx+;b}3*02% zrTvIZcp7-mz9j)X|EygT6R=w7h2hZKj1oD!Q#S<35CH!Ejr!2#Kj=@!SUGkU?%M#h zIu-TCJ5#EU-5HPM#nY#T@L_q4Hd&2{pyI$}PmXH}Q>wSQrr`N@y%A#x^BC(5w=U`m z{cW%t*W$Yk-TxIwG(DQNS^2bsa1ik!1=oEd1w6^y!u;YF3?qnYg=afcuT-$e8|(q2Ona1TOJZZnaY?y|HGjM?VGL zWHf>drfJF?HL$@K zgOg(fI5K%oRN5B={qF0l$^tmLXw*-0tLf{A9$TR>iEc zl6Db7Dc|Hm#)~)~edXzam!9d+Z;>@@S1r)g_;#-5=Jqjj=ugyw)zCmV1{VGtOb4j&&4+_M#dki3O*6iN#s?*iV=5fOZR@XjF7`)Vf#HvR}0O z3(=6hquobne=;?U_NxfnxYf9ESiR~&$ZY`?r#wHUV2w(9kO$n@huQE-)#bOTwi_$5iXtWS_}H9nz>5&HGVEGR(sZ zz~rrKmE-sRvLS2z@54tE3zOX6l0#qXFxicLZYa?aESs?I)S%xcZvr;H-6sh4NE>6r zLt^~XSpGI$L(;ZAoZV#emgiy6HflY1`2E1`Kb0R*S3#t$B{qvOXR9tnt(&9 zbkC(SEAhn}?1(p|LlYZ_{U5dK=YXzTf`)KRV!p0)+lm;>e4ISM7{+|ISyvknGO!z; zdw}V_8wh_WQs#P>>CU9mWSm1+=)T*WM}A|Fj4%d)*Sq^rf9lpfG}d-#3D-3Sfh$!! z&=an^rpn@}XDLG8$vVG8XTX z6J5>hcvmI2BsHh$Qq5xls5N|DuVXT@Lu@WB@qT%=<|d#828j&FukCcWx3oj~`;x#U zlZel27eqt=c(Dofjsab8(*WiS)L*C0F~L>tVM8&@o3L5qh`W>476Du(qN9`tB?O)g z?=vwZ6mUS>D|B0>e(t6g72c~A$zJ7I)pjv8Xf}z}i$Z|bXcAxrtFku?>=p%Fu4sOX z_Sao%GqCxQF%|R(eg}>VULrr1+ig+2hg&jw%tk&YC*O8no67Ec0TK5+&HmkL$G2Cqs$cpQT+^g4xhWq+4Xf(1OfkGo*5jiDba9ktcIo;@RoJPU zy44SxUsq0#-rxkC#fSr(HjZywQ2VS-MQ0%O;o7^kbu5r>z`~Q$nWeW*50EvkuwGgd z7$hL{7n6m}a<>_niv64Q4k9<6fa{n*(FJCn!}4M_HBslTT&us58tBxwC~8RB?E(e! zY&!1F1V|4d02M&;l$l5GrlLP0O-!&BYGHshU==L=RTL6Q+nA`NeMrIuQpA#z>u| z2v2c#XuZDIJVgm(5cylQpQ8wx1uS=IKR4B|Fi6hEr#1ZQ&{!|JPEl*{=2P+MF1-49 zX&B!0B=GX~klq?~qoVjL|4`8_if$8@JdZIJcPY9*)CuN2sOYiKDTQjRXst>gQ69n@ zMh`ry=!rmDZpMl}6*2`8Lz!O;oeW>tr0CCzo(08 zerPP+R;aLB^Z71-VN;!pGohen5`eZ!`;dl1$7o0omZs1Z`3Fjm<7)M6y*l~(E{!#@ zc^*657p6_6`0mD&XcRfzG@!IsGc!7waE7AkQmuqR~W7UnKumEW%h1Pv^_t}P8~Ho+C42`(fQ_c zg51&5zUYHm0LJEj0u1>1fj=XVDAFnXzf0Bd%MeF@jUUL1ih-b{1Ncv(dObQuraF}`LZiHkg=as;XK@74qT_;zO(7jgvSwss z&^Z7yvq19xnRJ~Sz8SGG2qA#naV1(IKUuB4>omi^YC6)9hl2)SHVHNV zSU?=G|v`_dy-M7~0!rq`p zIJP*-FVlOiCO&;m3X4}D2k^_^eS;^zK-`FxS|cSLL5YJ`b=?D+Nf+^C2yfP8uOR%%ZL3z zgohzW?9eEnesMIKAK*^%iN(=;EPqK@=hipEkEM#E#a_=M|IF8Wwab4P$4%x#dG#fs z71eZWfG6;m#Zl;C!lqGW>;a39Rp>$dEK{@R1gN>t2`?zbIBFQ>AUBR0?rqaL$gyNn zGoIq@wYnK7?(xAzo}LNiLg7XS#sMGEtA!2*ceBVcF(@;qN>}jv>H9s1^=0zW@A)PC zb7u$c?Nf$}RH|Q3QDkHFZ->2&&@XfK*Q7rKMQYF0s-dC3O1;EcIqpBNlIA(_^xoKZ z-M?1%bm^y^+GO)p)U9X9ahSME*^z~p>&8bUmC$2vsnRkhwzw@Yz&(tZY|yYqZS^S6 zAza9`GHuEuJYgJmmo`$LyGHX7mT%NoH*3!=I;dAju(JDty)7!!{iOle)4FPYr7m|m zm|Xd@jk>yC>&(Wd_*HiYmKbxiyh~Fb57-IQSMY)+(Pao_SKMXj-7u$eBZ<`Fh>E;X z^!~NkUupn3oq-6Ax~M+H+U2|{L&xJb>J#v)A4nUophTxv1_k(f+ukN_dz(&c*D4l( zaEPDm3G*5tpty(F>hh9s1Zku=UN%n>Ac65j=fKbB@bmnzO$2vxeYFZ}L-8YzJxEgd zP`*AI8x#u*!cYeM9g%qXmHu}4j^b!J7?>xyK%1#3ERJULacG>(jc~DBk)RhxC;M4( zfw{&n_%*dKf6DnRejbl=SC`Hqc%k%>OQFj?p@+0p}Ou=Z3C!sWiK)Z4*m@C2%hBPc0~nzLv+%IC5SIxTaQ{@gTcz$_O> z2Vy{qqr>aNpHnlH|2O5nACf_=WEtWE2AbBwR#4^}MgHQhQ zWU_?#Wm~y;g*VgT#EUO4jxO{TvF%)~pW7U<>_Qe+98K6e!t?<=E~omypFV-H9m&DX zDA6ANe_20&F7gg zS54a0EOKxKZ2{uWrdHJOV=`~Q$G|nC-FH_a1KhF<5hAZKr`5?yITg}W3?&&Wh z!o}D%O1!UKwK{K5 zJ?wVRl_n#QOZ;vgJ0DkKQs~O=ko*2&k4~g$s8`L@R`U!UBK@ga->g$tqkdg4?_u&SJwoSh=X7N@wc#SMh=skbfB9* zaQA*f>48b$FGA?K$@x-DWv_0|XfAjO;uj$zf!i!is0x)ke6^W8f17STv9L???E(;K z?Axoc$iw(~O!2JH+OY*Y+^AEyhwVBa(~a6;LqKbwodYxa34AZ7Kw`zikqa?^+*P#A zTyligLJlY044hEc)$^?J}F`MGnWx})KFI)GH zTK#M}=)?U~`2*L6$Npt#YMy7cCh!D$!)$0apCUduZTus(@D)|6z-#kAd-VoTutWz} z=}6S|y~XVYr4N$sFVjo)`Uc>~m!A*i&TuXhe6@yP`p|Zk zgyWmfh1@&(E9dB+;WE$m?c(oQB$kkW$lq+_yI{s`bNF8T;Sav3Ww3(&M*P_565Z&3 zJJmO~V()0&-VwG7W5qvNR}xxo^3kJn)snAgAk5i>5)B99!ZRD@%?H>YtrR?%<+3dD znt@+0#eHt!w5M?Hy%&+3<1v^BYPu)91yXxjCF8m*Kl5 zs`kmY`^YAg=-d9kXcdk-Yqt)+92r%$R@CV*R^FhfUIb!mmjp~Wxl5-zfJHb|*sEi0 z$5z>Yft_d|d~IWZe8+IgUAhRDF;{P**+Wd?-+vU2%J1HB(38VRpSU`_<${bZ;@{%q_&u3G z4ztz#eTuu334IBoOn8s%FkA74|9~H1i-t{#Qi&^J+xgGiCAz*u&)KA~b?Fiv<@?5? zU=R7f_<8p2CO`JY#xUh}9Q^$#|Awh0y3D_m)8Y6y9sZkt%OCh9lkEZjR-!BXln%3R zD)Pax9)4y8cC1XT&i|&lLi7{%o*YHm-2N3p?~vU8uhn&eZ|5L`hHNF)M74$SA0R74L7dq z$%Ht9kKky=`|7e+-z?EMu6n+%IV74oQU&|c=#TPD6&8dEum${iw3vU9J5po#^9YrX zjZ*tZDgNItg>uNTdqkk#b2_!D~`Bc zE{X%3YMl60Bk5pHu3k z;DUEsbq0{Q;&m0g9K?R92x4f>bH#F%@wfsd{A8E`#1JRV)rA47WHJX|eu`2FcIrU|Ss&J&zBg1{; z9;Wj36Ns$+6&a8lL+X+4K$nD0;+o$#9qXB^J-Vn5-^teE! zwtcfQ^aFW6)c7+_`e!Z;J{n>lL9#z|w?8zU$POQmKi6L;*Y+oq)^5I0Vso~|pU(HX zEhGV3;ylQzz-dpRv;+y$Lt5fB^bb`hPRIWAXt!s$oR}4|%&6McH+d+4r!lap3pFh#p~EHoaWg@xIM ze>2&f4QI0UHvPc#Y?gIM=) ztMbNp8IxJVY8~MH-|izuby*Bo?~h)~NAnD~Y2V$Jw56gSB24j`TGCEC(PX{aJtX?1^p~P4G2)^p(vzgpKuYzQ$cBSwAAf zSXdsP;|TxGA-rOAkI64&17VoshKlq^KimSC6O(M(d!!QH(-*9n%3$J|IQ=aW^HtjW|KC`j$%3M%)F@F3!YG^45L!! zDjuiU?%#HKhqn}ru~z%|m;7XdW--vsQnyf>v!lNkM-kt5c4)3by)a#@??}F8Ij1w| z3&N`X`sx1GH^l7d845S+Mt~^fYKi7DL(}Tn-DV~SSGey+ieLat6t+7N68zYH^94hN zQ%iJ?zX&N6n%`m>f(nak*$}pc*qlc}zG`TgMyIu;%%;~G3Ki4Xm*fF6%*Tyo++A zBvVZCnw4Z8(VlEb0nyzKs#-N}ZqlH6oZ z-Y(O;xaVGpmbmJZGkV7>!D3MBSD&op1g~UZL-urAOIGrDhhF!^2x)zYxUzUz3Ub|WGyd@ zCy#r$$+27XWb)mgO!j|hnVL%lhL7I}Se=`20pmaBsH{e0GoMKIzlhIGzBMGzLaGMZ z^l1E{bRkS` zE_^PLDQ_P_GIkTo8+8ZYw^;-(hcyqksCpm+|o7*!^cN+jggt`5R~!!Uz6y zJbziDPfJ4U!uW6c|9he}0Q}|BkbI|UbM#2)*hBvlU|u=RyON*Z<$t@*|C8%P|IhNo z?Ci~7`nxj*Jyg`jhB|>mI|urc7!g5zfJw+vg6-ps74`?;6xTao64$hAL1F>+1RJ;? z-5smNF!b$R0UD8Z0iJc+HZI)c5wV<3;t;_|%nmNxqMgazKm_{~hlN~fYY=_|U9L(+ zy$one(z1oQDvTSHf#{Mibw~20UY-_mE1>~kRlj-1y?)|b6qk3s;BWN)=f4qC1||df z5<5?%`GB2WJVB#yrAql~?qe1_&Y7FP)zO)2c83k60`lKj!l#~ z)Lx-CxHT{OEiszgD)OZYs0KYgDfOKa*pK+6b`Ek7I4m5*SNTTg{Z|qh_iX9?{FfRv zsAmf&HKJXY416h&RdQGt<36m1-{tRx8lUqbw(a+2{!)cKdcvdhvbJdN7RA7ZNT4=wBpwJfNuNzcX`rB zvva8~v*7c1f04^D^(*_Q0{}|@ksJ-39Ku&XY2$CPE;$--BP|f)zFRa)4@dKHh#a|c zck@L)34Jhbje%=U^gkX95r;y=4u#2gIXU?*O#_FLyq$qYIz%!3^*Df*&wr8U76rbS zVjNE72kojGI1xCFm73yfR`y`P*cl(n;73GmokQvM*FnqoCSZGu`pby^9S9;^>cEM_ zO)~5KcDPr3g%|k>CxqHns+@k4e2Wkb84m4P<@dsOsRIA!*BSQZYzypU?YzUKyX) z!nm;v(;E@V_oy=&;8>g7Iu5Jdx2_c#Px6|wcc?HGU6~h!?l;Lv!Slqou8r?FJH-A2 zHsPwdy3{s-Z}+d{+x=fwuE@uc0V9nx#81xnX`|w}Ykk`U6S-t`{ZSJ7OFY@Av^5UKe z6vY&zqUQns6ZOr>K?6BpnQ?Hj7!ok^|GJwHeGmFB75*;o@?D7C2Hmb(uh-Pr>6jB( zxJ*>z-gUdCC$1;WH?lhMvemkpFV&@ZHt1qstyfptEh z$9+JtF#9~8&RMI~Gw5^>W)GOWy2}z)Z29Xb5EB13DP> z=41W&krAM{KWoSfp=dM}cV&M?g>P%fJDz-s>E5XnD!xyJBOQ_x&CQQK+Mzx3qJxG< z9~DRU92FK*aZX^2b8poA`;~rL|LbY0>DN`rU;?OYy6w&M2xgQ9z}mEK1lrxA0tVl& z3fDgUiWc*1;j9QG_lqX)9TgrDB$q^v@Q2g*CvJ9~hMzBviuivMH!;C?oQX2%WR8p`#fd&@p3)>-Vju9zfEpCGmlPBMtY$o;D-F#`&Qx`@(~Q1pKx&Bo<5Un zdgK8ve1f%ZU88%twZd)!LhPe>#^S&w`8ebp?}Vk{o_rG2!XuM=f|$A7R&+k}^-z@` ztn!I!DN>6oQe)BSe#A$|g-wXc!+6|is-kJiJ4w-WWqVX^_Q{GMT4x4H>Zq^%SCxAB zX^OrPN^-_CB=K}b-wHKW*L+E%KKhO8MmQdy*T80#$TrBf>31Le1)>1b{8s}0Yld)x>C_q zf}mbuqBUBiz2boaHz-;ho@9iDxm*zpaEs|jc;KbF1h5Si+^*=yif&hQQ|L=}z>Su? z-VshDJ664}u<-S#idHIGuAwMkf1>D4jkr&Fzc5_C%WBwr6+NKn=h};FwocKnHJa8~ z4=Q@NUn1M~TSdQ89`&ui2?!sd?2xCG_bBzN8u3JE!$OaU?8ifvdB3MZ;pC9#6uo58 z?D{Z(=FLsk&B9SX->;s{enHXynzH^~&=KKVqdzEmS4@Lh`^jAgy*3h>Cnu`9WY|kT}`Mx5{ZnL*5`XtQe+h<6W zy|Z6`J3i6h@_td^62i0r9d`&wTmwxyJZd>GO2vcTuvd`^hU#azu8bx+_T!pNUCAsp zRRyXF+!FqP?+}9w9+vW?g&}DP|D%Av(n-DRb9$X8az_>bg+3; z;>RvpBP}zA5#C+0o)YjiEX(CmQ3@yevrdxY&f6oq_h1_`X4fZtS z&fwb5(c&5v&DDE00T)wChBiNNLkEDlwA^d$8rTl!l) zBVR910LcNV=-Iqbr?$076O6LTOZD$5LAW?!mL|{9UNzdA-dV=&U*HhEBx-)IZwfuW z?)GB-2WvJ^a>Zie3LPilTJ23(Rr^C4*QtGBRDk8{!(5NvKZiyZp)FpIrh0khD1kG& z4Jk{UUr0q?AlXE6flc#-wXr_O8$9~0#gxu)4sn70hXNWuqs|CDI^Lg=th;) zTNQYtC|QMOS=iOjHs9{O`c}7zRxbu}=|UbUS{J|5k?75~1?r$6hrC;qWn5k1o*1m^ z&ZIQ~QO}sF{_)ePnlLpIpFTLIre$%3pXbE-;74v=82aPAme#NiJstO@jNx>=f#hY} zxDWnwYnN{IWg{iMygQ6;?DabH-UwOCnY|jH(T_~SR+#0W4w0o&s&kev+idccFyXdx zw-`dqsL?}n!(Dq(aX1;Cz+D>ZSHQx)4^`!q z&1Cl_xXWt`RkR+|f^a3S*~#kD@?--UeSTjm+9NO8G(5VGh%)L$(x} zJ=c;&;=#N&om3owmoUyefzgNfP( z|C!q0Y0ew`rjFw3+PM^`^}OFT8M@Qi^Ob7#btQ6t$Gz?$zP2*Vor~I|UK-84tCo+H z1>peCkdO=~?odN2n$5;4^uO6o}Ikkk50sgguGrscU02XrglC6a&{d zOsjHp;I9vmX`Xg^`iENYVLkCuiPV-lz@dqBVPX|`d!pIRSe~*VGW;RH-pekWs@!Q| z2FFZNc&93I&jC#Lb#wJ>weI!eANOg_@*C%Y6E&0hJ#fI4tn4p*D|jG0@Uq9ks1(f6 zXS6+kC`i$6p{{|7yk|p=YIsVogo2v#f2!cEz%22UKt5x4DsQ)j_?a_0=eE4)c80CH z$ahhxt}bz68V$K6XK@kSq8dFoS2xp$DRC}SJQeHgdM$4XLWNZj@5%M+h(W?i#RLnH z6a24_Ve8jHrwFgj+)Q_4b|oIiH(|BtA8|Tc)jLXIYW50kH;hx+V#ZM zmm!%>(B~;~X}g{my7Zcj=Qm6TWq0ej*e7mQK%_tj8~LAd2eQLl2Ba6{*5Oep-%j(6 zCaUl#(V}gASx_DKa)*5r7V)5{KdRKB&MQ6V8r%6E_xXin!Ee?n-?-wMM(tf6x?+At zf&<6Yp>x}H6M-b|8@tMye;%L({K*?@^`9=??*&sgTHCEhldx<2W*w8P_Cc3Iv@t-L zOlwY?E1c z0YG~KH!~q}=V0d_59+?hZ0ZZU^^15LlhZGtKOq34Hz3;!`_#Hkqx_mEhXgV1%8M?s zGfJ20eWr$0=(Eb8McmxvN-(wN)1yCT(lieuF{- z*vKDwVZbDs2B$Y@I#Oo?T_9!L84nPCFst$=S5TL?O&z|CtsZD{6gzB|IKmf~>Z-US zij60rH=ni5(XBN>Vj4)!VY_ICZJ{?ZX;cH z(`_FARIii0OEj}B@6d5{JcutLM;V{vJ^GH<^Utm@=9oB|T}lm_M^nhNwVjDuvgGvX zV3y>K`jI;fFqNikctl~#?xD)g)x_$6vD1yprTVX4vnXfl(uQ75?^79-ET{pg>?jR= zjDLej+hw7j7`&Zf_x(+;_8hm~oyl`{*Esz3QEN2P)lbXlM8xLw>gRpBqd2OkPzB*D zbv=}XP`Tib9#`+;h~`iy3m_j-7P{oogBm*JN|$sMHtL9a9qNucYHwG+C%6NK5kT#{d^eW=vjX^d%)64h;-wR%`NVk>+ z%^nr>g|ov-MsWl2h8FCVQj*zyZ;b~%EH%F*rQ)<);w+zmG23+7P-yHogOoq&*dQupXg+6nbu(hlBk5Vdn~|s zs7A-mRX@)UzKYi~2=Z-pJ{o$Eq*%xWC#bKGr0{C31A>woHdnB1gC26`D8_#DSh&JB ze4y3v5<63ZaUR`&Q_1vWIX<*fjB`e>OwkeEv4{Kye_IuR@mV!GGtL*E7wXDmv#{Px zNFE?nG-$Q#BsKem2_`(vnD79EpghlknP2zlVQ0R^tkq+z+_|AR9r>xTYq|Z!(jL9! zZVb>JFU1JfSv-(oE?>l1JOFmwaG3aoohLVRhdF$bl5qIbOVtpQ3=E8}a9q!qO5*we zz--?@T#!h_6zh4UEZ*s@9QsSQD+KaC)SFn97!SDf#&TZ?qQA!zLJ7Bf0-=Yy=Mkmm zTARcRO_@j9gJDwnQAaA8lNaR-4|o8!p|>pb+_aLmEj7N-uOwkB`W z{`OOZqW??2fQb^vI~0vCClJ~p^9-9`8aF+K?OhN?H?m7-H>#{&qioGqAVe{0`qW4- zUQ%$Q-let1{P%RK7G%<`?)qqEyiDI=2%Rq> zR^?htc_$fs!m3TK)~Mw1ZpJElRbOk=P6K8XBin437P<}H0Hwx5e%tFQ)G7x)^pc2oiB)7R&PmV}GCQK znWJtD@?6ccf{157Gp5(vS`g1Q-?EBjcJWtt z3iy%JtveEDdKZ0_w}fs{`1j}NAVFsXH{@Aa9Q_pf6oJ9<`he+R9B2|EVPIiGet3FF zYQV{pyTPkYKF=0=fi8&`*`QMzbwYf&-k;f}@ec(s{EiXP_vt3lqkkke zWW2G*sZ^%B@fv5^n;)gkn&u4;9ZSOS+_I22-#R7qURys)tyMbN5yXY=BLo2$zs&=a zg?-<{OtZ~T5{I?W0N%)lF(_4s#0fy8<5@Dxvyx1C0?%v=_L`jqD1!lc$EJrhjr*A4&;ssMf9p zn(to6bRK|?>uu7}&H7cFUPgP|9<&_9<72z@pH98uc$~9lb_16%)f*;%Jpq{p?D0p0 zx*>?N+rlfxFGGt_=z49H=GTPKriC4|abv-Y>eXT4-za}XdTl_SU*4#fz#*GMBH*Ts z_UltEA#k-O`R4)ZOE>B*e&>cz5HtgXhKs4vujx4t)mg9KfT=+5Mxg*RI!F}vhQ^8I zK*1^%V({y!r{e}^xzK2D;4nNzmg#5mNak4c4Kl_Vabgt{sYv{gciI&6**@k?TTshup znWk9YJSOp9K=j=PQ6SIB11g9F=^^J>S>La&fy_ki`{}??u+`e(UR8t%5R69~+U)`w z_2~Pa>{Gr))d{mlhxV~-teHJz=gWa}rJ75=fMrnRMWNp`m7mx@ruOYqgR279HPnR4 zM;eF1hX-6YPvu5LXy>*!X#OBv2l2Q%Q3LMl_QD4l(FL8!ZJM3923nJ0iV>ghYg|`u z!*Uhcdsj^cSKFgIJRPZUAWv;M({vWkekO44U}vcJ^D8E$m9k|{m+I&#N|VguZB~WW z_ovgT$y@sS#DA^irC2zPkJ!}i4m@#4c#kIedCu?Bxm;$SCRJUdyd=^?Gq7?4fFu*(w7X|IMBZhRmyKOUe%SebY)e4Tk`9xb);7XH#o$}pt-Pcwz|g{ zr={DKH2{H(Z=vAw*Xon5FcSnw`QCCz-@`u5XlsiBe*{5N2&@yIY_iCA^W~eqE275|B z-_AqGb(rwJo67-r$q|fZ18qIV*6KAh8*cl9m#2*SHx8ztuZaPJ#rR==()PyHy4z;< z33>>0YR?3ge|@9A*Q05hb&GEuE^>cdyiaqC@7q4HWhjoOm;_$#u}3tXWnmYfSYQbzw%!sJ=5aiW?(;&*SF;jcIwnLZ2q9T0Bea)mWYY8|7X4ZC%KS^){%x zF;o`6)~q)y?2jD3ywa{iZVzK$LR6Jluoz(UGM1}T2d>f7Zhf#RxV&(bh@I?YA+=(8 zK$e3nY}$<7q5U0431$Q6(lUkEb9AXrupuo!CFm=@!$WWcI>wx2Q-UNX+N?co)_0HZ zhu$@ZE(w}&LW7-7E-#LQmlgf$+%J3Ic0UqI&PUSzagEMFuhOkYHtJvT@SrXI-BE~+ zxn`4+YgUGa(0 z)vJBlRUAFornz)_sSj;E4krytaA)&%CY1L|$`6i+IimpjZeHbx{(qmIwm2 z0sR0Ukxl=)xw@U6V+%CYnD&=u7t86Vx9qi)nqcOy#$@pz`ieTARC8=cB_}qXW9N@mURd zL1XBpjF$cx6%fzi#aR^=9@SUlwMOAUTL9N}T>+V9&^v1MmnK!B;PLdwBW_;T`SV ztex3mHSEL^I(rM60^pVU4#e5pn9AF1YJxzF&=sh; zs!f>=eGOXd$L7R%-56?G;W-eC6S&O-30&k`J^Giyz?TxF_r*au+-Hsth3z1{x`gIB zu|jK||0395Xp|nyiLY>;@qG|WvOh2q?>DG*5XReT72=x^tq@-^Dj4K^>s*PCNo2>a z>kPH1H-bnLKXL-B&WFJy)+Fns-)!2@B|iamMfuTGckZ1%JemrC@rC@1ZLHL$naZA_ z1F8Zqk(IzIEH=(AyFpVamu^(5UNf7-;HLxHRO93b;r1I0CPtxji!NEA21E!ztOs-q za=Uv%{~}_gQB*H>X~`O`@79)$nr>6MzDE?P&3Gr=H?no$1lRxy0dkSGaKy~xp3+|? ziTv!tS-RAyFQ+CnrJtF};SdQ$2V#H1hgFE*yiu1JS-}Z|Sh#7u=2uCSoc^w^$>=@5(EAb( zf>bX*K0agP*U4u3YVi=(`1mkZM69t^irDtU1|5WXH+Ay|hc;(*Xx{C~VpjHhG)&f? z-2rdahLEY*!|~Tj;;*EMD_(414LM%J1bcssZLDB~!+Z{Q%O^!?VV+_6g@)zV)~TpL zyXtjblTLsPY12?+@{sL~b$1RKh(@1s0{U2Q2$5iUx6a(CSA260cAaB76Fe7(b^q>= z+{bE>ecnLN9N%Y@V{z4R1Er3pk9yrdnauc{1h&y3q0oTj?~OsybA#R1ZImbT)_xQm z&2~gbV3I+Agu{Y%E9=r%P^)^r#%a4xhq02%KK8c{7%Q*swt-8p)742X?;Fp~u_RH*n{j zaps6Wq13u743fCXp~Mi+Ylu;L&SqT?uJby{;vI=;We=S>y%^1>C)TYm(s~!gv^dQa z*HpS#1dl(+J2tbY}nAI;cnG_^1uphC{_@8*_J*; zY)I7=+Em{|;+{1sf3qV+_hZ;5A%-bi;T4umJpFS;4GhyvhxTnt0 z?DUdu-NlSH6tlfLITOk=7rJlZB>L;R)!w)llm#(j4IY133ZwzE)rWEbEbu9;r!-u} zd#ZGe7l2(om@bj^I>@g45_ikQQIeEQcj?Hu%aD;|9>0ODFw=D`$xWPEedsP5lD3cx z?yb~o{cRP)ZR>TS+X}JrGfl*OtviTz%1Z*zmQ@<0S*?KExmwKGnFHscm79{io<-7R z$;{rpSlzk0rdri?p*7h7^}4%FBRZ4~&gs%AouQ%YA>AUdYV6f>j5zPp6dEc*<$0^O z@)1~XvI8}tX&7`+KBgOH?-K1v-;(n~!^0Qr^*>FT?LiWNv2`6J519^?-Cs*AIOhhOohnz#o}mEb&!?v>0tiy zn}goC{I;NG&uk?&BKIPl;TK0G&TWjxGJgf#@r3OvR7~dl z&}d@Eq5y+8xlRrDcg(GcjF~8pkGMzh^Yp1sfde~3-OVdcg$udM#^Fox_-t;n&1q_f zW?LZ{f;)pQ=$EIwp5@N1kXQs)Z=WMlx(ny(l^AO!MBo2a9*FLd5l-)OZ{vKfCEvSz zdqGyvWxMV2;QGL#_0A{ml_0Cz5~A-c_6X3VYE15^fi5iWbtLyFT&G4?ATrFE3(n3CtDKl;X}Ldzh2FZKhV3Psl>0N%-?E<P-uBnhaR*MSj0@RIMM8=Ip4M`&e$H-rP~o9Y|!7G z=~%c?H~y}_achwiF+5K!|KeRCW0hy?`ZHtx40g3tuf`a=QpKLqg;9lSEB8ZQl=#r{ zI$da9YhJy2JUj^W)ncDM7~~MhW2`>w(xHP84g-RBdMBZ;MUmO)*%^U|wxY@O?p@99d13FE z{V4SGx~ybjxwZPY7gpb>br%OSKB`$m>~w%by_CMjE^=l6zQIpIENt!4(>~V48$xzz zy4l?|8&zxgdj4kp-Ax0kw}qL-iyeBLn2KhS6~(X|scX>r`%H@}b>A#KQKd|5LmM@kj*d;bs977X(^&VH#vreXx64O)I{C*qk*ioW(uM?pNA@IR-G@&L? z3fS}C^Bfi!w$*8=3nfTn4ReAXCge*UB9ry6&z~vyXO|)FtWG_!K^+!rbtamDN3Hy? zF#eh9&U=Mh)som2^78P{bUDMb^Hi6YhA}JiD>ZSJO6F)B9?g(!ynRGWQNra1$qY>FYk++t=v8Zawbo-ek0GFsNQ0;96>2v9olQEjH4-FH!$8SFN5%$R3`~S#tv& ztzfj@xild2uQsWvSwqtr*&1ji9NZtw#kIt@(cJH1pFVb`#VMpm7_ZZ*m(hB;Lr83A z(gti(DJDvGl8}Mdv5rMrJdi#q_}O){!Z6B%ZB-CpK=u-$lOh~rh&#qyG~2juz0PjZ zIO=rT^s5d%@7EF7ORr>{&E3%+I{7f@(K!54E5NrWM(@6N1F)H%5x{0basvqycH~E6 zQxQ}->E^M;(fc{kU6>l|Oqq(1H&p9K@VbrRc^=TD-Zpg@SQa{=dAakgkc#`E1*%k$ z_j&G|V7K`e-I&KZJyEMxXkn-FAEKtfL!3c4Ls`5H^>9;#)7R+vZaqoZg|q0@uln?X z(`|*ulia;D17yihk=W%q_=7fDNXg}2Y|YFuMQr$jB!OS^(qDEmg>AI`eBkcN0R_A!6rXZrzTR#4SKp!P4)Uhv&x;J zf?ND!}tr>(}Qg zb+%IHy1kxXNuMJx|`gTQk z1nF1vW7&tyFA*gID!X`$mW1{Y$V^S6g^A94B%BD*&mbVv)jX`KzuqjB#$KbGIp-GVZcB*@~8{C-Cezuj(VItBCM5B+EmGdW_es{elx>Ebr^ zQaDNdTB0y)U9At+Dh8!s32dc@QZ5C!DQ8DJ|XhLCAW02n_~vMCEpBGmJ>dzR~3@uKj1M zMjSF%Z`OTVbihZ!qs0{^&?6*La}!&a>!H{rk_ao(r8=-Oh##Uk8V0A0`SrrcK@b%_ z-k^(^G*PcNn)LhTpnHdIGM#s{aoP8xPI-_EuQfM4yEqS5DpWt zAC@1TW)2PM!fBYY^h-6V}`V~bjVF10C~sTV`*`i*`dzjvaEk^>KoD={a*8d>UjU)IncZ-Z4HVSQo_M|A^}XOl!n6Q9-57ofY~= z&8pT#$y2(|Bpdd}y0kwMyG|8xf4cQOhG_I? z-#G1<39&$YTX5oVu(R^Q#E{(sY*mToJ9xn)-c=RKllQbbacr_4JWF=1?r6|8jX`p= z(3%#a^4T5wxLx-pi8B)1Su81Gf#B12cj*@#97}R`8aN6%c8sthmF=xNN zps(vl_A1-C(Gj4aL5o~!Z9o>)6IEKzy{ge(=4L^;1o#_mHiy;f_6GgHTX$NMMmKA} z)=(<}Z+vg8h)$03nALh~tsZw@bu1Mi1wf3y(J1#g z>jWNA5w-<<3rLX@mpeLz_UYr8nOr!`G_zgh3L>BJ7* z-L5C%V;!WyI7;3z>nCQC>mak8FB;}LcyCXjuVtvJs;Sr-= zdL1CFOC%PP=z~F|Fe8-hoR>%_|CPitQ<4SWU?~XoP{nR;{HPE8>uvgiSDPJ!-jzB% zc^5HDd9?w?YS(E(ca)P}#CjyG>Eev8GuIs|CUz^^u>6}WmD|W7%QThawxwNBsVC^? zX6tdI?HF?6*m^8;o&x&4vr%U`e7=%X#><`&u*Rr@&lTgb$1=PBU5CmW5w&WILo z3Vn@U*>&cYv{9*gzIss{E3W1PAXC zr5@}I_*1p|qx0`C+BFp7s@eoq`{Y^;>(Z_@p<zBgC5B)9Q+gRwdqUZqt+FbECKg@NM*5v^37Uk8CI z76$hU6&BYle6u^uwFD8ZgQ;Vfj-}(VRpIWP=G^h=24=DEh!ZiBD|LpIfh1ynOYhSJ zn`OL)-!WFurv>0-&iF6Vw?7|L1yb<9%ccdQ5}?O z9}E9Rq4cnyg%9U$;J+{+Ev`SstZlc0n8dhb5{s9Iy0G=_8g;YAa8+Xe1sM(>_*e+* z*00syx&$<;p(v?G!yrHa9$Q6^yBQ|guw#^8QD_W8gXxWd$JySf1ATe!s1cwVwaF6h z&jN)Zq;(o@PuhWs-p1s}W<5c$LlUh+KlWWp8OjcrG9q+d1m5aBDEbZkO5Lf!0Yt-=v3&|w(5 zT6H#v6;ea@nqL#FGJbUz8%*1>qm4I9s#r9Og3Hn2vMOv~&_JQUpNkXrq%A%v@#)?fR*zvxhr zWcu~=lfPN35*)IaQJcPIKseYoav}$A(4Xw5Xorj98b%jJYY1}zVpo-T`KDB~kM(T4 z0ygdAIG^q&GwU4-KjEP{|Dtm{04X_BXy=$8*kLKrLv1>&!_`>rp$%*}&6S$S<^Q?H zSscjix}r-TJfxHf=ha@=t?{PWzvph&m*T0x89ix52RDDfjSh8H0(tfrjkDDGv5QBE zgljOH`Wf;?Iwx3Ej7VmP;auniU8k38^=d=^oVZ&VKVcg}Jk{Evg#&D=pDJ)J=Czu; zw3Wk+lMaqCXC|7@IEwiKQA6@D(JtnuqFYSJkhWV>6to;E>jOhd)Tp?@stL-PzTy>5 zHD%OKzSZe&w*=u5a?tl$EcCHyRnW-a+EqMo56#}+=jH}0p3r)R<#p?oTFxSY3O3IGL)Vev22D$J$DSj<>Ki?ht@6PR?E*LH%b zw#_C6S=-y~`tqQ)@pxC7Ui(I;&f5@1AA;36WfZdj_2Yc;3NpIUP$aHXqx}(EH*0vP zy4vYYJ2F9-X)}SGDT!~0=nPL>q~SgWCSin^glOx7fhCdEqQRPP zAZ`(q{*a>^IQ-jq6h78GH_HVo90*Yf^+aCV!=SVCqRTxD8r-|SOe=`C3@Kl<#Y+=A zI5b9q_A+~Ai??df+J-mku6iBq0QqR+HxSV!?Yb~tTO#-zw^~cx+US)|;Wz&dbYWGA zRA8}{46#$dnB+RTX-#e&h>~d;1mW76O5uSZ*7zl+|CAW1^cbK{Jw9}U} zdUb#w=_`z!1D}}jBlilW1J_qb%V<>1^+8NB8E0)k0+ze8ETnNqIa|b1i3dq2APWRK z78=hW1~>pS(xqc5Pe!Nb;vh1fC-!Pjr;k5`$Eyl*f+PI%(Ul?Y;|{MYRhqslM)>E} z>H>;wUDJ{6gb2UqYPz5huyDbqDrZ*6OM;-Txl-dgLSlL>8|xV{aDeOGh@gV~^tbq~ ztB2mJ*M&{`ezR86ZH5jlERxOPI(2Hs8ntZH?|PNV1f9}OFzrO8i$ax68?5^*@vCy3 zN5(tPwjeM~m$oJqUl{~}An@oW<Z2=%**(jrP;u5shKSJUm*)<{+FFJJZ#bI?XiN2Xpi~<9iaE7OVTVd1MR_I>u?(So)Jc zj-yX-+8YT@+r1&^9v8+2)+HoYyc-&xypn)4+FTSQIE||dLOUU`43{M7vpB^JVg#ho z&Hhh=K-xMdc3WKE6t4$4{U6<6kQ4lAhhA*g+XIH;9t~fs5k_b?bn4CxY9Ql0(2wO+ z6}%RJbg6M#dW)Wk7x{6xJfD%>V4W_%-6wvV=+m6o#ZO^odpWgAJ%uX99Qf(>^#QP5 zq@lJL==xwA?s7_qrS5PLI<#HG2J%)v2}TUx1n?NTR;#-7M;8J8$8wRYH|iRU#_IvL z;YLH4+^ZH-4LREae#6pY*3Nftla1~Nwb1A&BAsQvW+kjdKF&w>!DvhZ+>cS?{M^7`3ihpTT3BL@MMd4J>I8 z2lA4k(eVgB*tbrVtx+~X3Vt)OwCT7am|7~{T}Ev-Zduzj(51H0P62p%znwxSL}gi! z#9W=+NwR*ITlfU88_dfbojfQCSxBCV(iwn z0)8CvXdv5XQ|n6A2`_Y`;=~YT?4b(4JGsiKiZ;AsBchu3_qsZ#6jvJF9&|~Hfb|Od=9cEcm@g)AMD%!+l&nX&h2_AOqu^b zZ2b#-*LC^-k3U{x%uS{Op71;l8Z&iRe$*w#7-VRhV;bcm6U~~XY^zbzEO~>hG4lXs zrDi#mOtUo)kfKu%ro%MUPbc#<9VOAhX{Lk<``xd{Yp;Fg|NHOO%^S-0dR?#Q;kq8z z<2p^TmWERbZ_D;{rn5xww(>0@X6t`#?x$L%q_&mJVaQeDL}imBUpKE@E30)-l@6^@ z9xbN`WTa5jIZoj7om2|jH?Lo$r(5)A>Ng!7SlgxpMg%1fz9&|1Y3hx3>C=#_#^=vd zuwV%D>^qZ!PEU*`*gp1FXVn=Rd| z4&^{1{gy_i7X=-+%bg6%vG~iQE&zye8Gw=sD$^nnWuzbyB z;Tz^J3(H7f8stm~hw&W0t23t+P3Ue3OS%P5xPfj7ojS@;^Vg3@`pv`a@(Wt>VoZjj zE!DVGX}>w-ls!72_N>hgTJbou{q0cw>8)`h1mAK5c^Y2ZrJzx9=SWmx9BWy`onEH=7U!TX?JlTd-UWQ_4G!sY-U>@ndXpqq9|=PE9~#X zv6E~=ayb4zt-S#4mr|&e0X)#}M#Gy$i=rywM00)6S?d8p1DesVJD~_@Oy`o8oIhb2 z6B3;=K{f&`!GJ46@2l5pqmgme>hHDasa(_)u;zo-?GzVObm^W>?Y>4Y_C~he#an_( zB1(HIhoN6zCBj=8U0Dc&&V}=@RT&Ll8g=X1Xl#-X1!u@gCyjl|BXw{K-2mzWyj{!c z!DBmHqtqF6wI=aWz?tjPR2PHfOy#ZTrRMRzPahbbQ>(8w>Weg&AUINxOeju@$3&g- zT)Y?(FlzMWT3u4F=NluR2cD|1OY1sSutt~k1(A3X351028>2@h6&XZ}$cmXYV32Gf z6Hv5akH7TYGltZ_Z2HZIkdFa&ELj74W;$)sGtmBC;2MqP0OW z6)Sew$C>LOzm6px=u}mYzU-8?91BG@V4qHL?e?yMWKwZ5ZA>&0Jk>S4In+4Us#2HX zcNp@e0n@daLHv4s6k5^+@#};LtrfZ2E2*q8uR`aRnRaY(%>sJ)UT^QwWnAsey1^Y$ zDooJP(meIZX4CdLKY8B34_v){hT1K_n|6HOsnJ#Q^sXnugi+4Y`L6N#9|Bsf`Z-~y z^!!82uGU*d1^5x~kx@Sa^}!I#qaGRAF@U-%+rw$ zF}yGW>Ahm4hu8B{XM?0N;0&UEcTv2vQY+e&GhrSkEcLux)OMgPMgH12Bs5*YSnSoQ zpEz;~E=7}sMWYyJvr>YN!#_Ghr}qN+xUf5P*!Cz)NDImiuznuzmgyL^cg#-Y?EYU= z6{z4k?;u&+pCR3gl$$2CGH{b5TZiWg?Y*AO9NuAjft(n%)PZ#zr@KoD`Nr&{Io{VP z|H-_5E@s6*WcKFo7^>hP`Y@+7x!=a((A0tTV&>OGf0fVFX~}$X%wiwU8Pd;xt=Cnq zRm6p4C8gJ;|;D>lC zGF!Gf&5kqbgBIK6CIe&vrFq(eu0{Q_dri>X?CudQK=bPLvWax`?WJC~lO32lm2Lp3 zc$qmPN$JrH3@{qwz8;YjN5MF37&y7e=$&bCt%mL?nS(_b?d%!~1A}fV*V{h)<7O-G z6Pm(!pwpr}@O=Ub)~LuvkD74*ZF3j+GGcHm?E4pA5*dL%);OFkD2!<9P%(qnMB zl~j6OZ@=$hw^RFvPhH6i8vmlkJFkG=CIMFfd%hrqgQpoy(SwB~heDu~GYkXrmFpx6 z{1JEj+w5Djfn4Z19h&BiXQIAZFKjUxU)Q)%VTI%Pu(E%!@?SM#qbX{*1;z;yfOM%f z$~k|J+-|3i0S{eh_G>Zjv#r4(ng)$qHPAuIp>}oOqZC9n1$g1=`k@7zhjHhGO+5sEYROXKT^Ovu?g?*K_HWz z(RGb;cvybY&h3B}n-bj{&?hszv*112j-67Y)A5Xq_bO}kT(+&t8gyR0wvzANtfw3G zqeZ%k6WAI=$5m-wgv~*@rNSH#>#y*ofTMfX6ftU~WI!|{I@p>mZH!4GT(O0LYpQUz zUYf@9GAU$LdJ@ZSua-RopxD_Lrf3j;X z>eRhfAe4LjGwpG)Mc*2TZXu}IR2W8fekFBqvN&&2ty8Krrbc;ALMC{0q3$m6$oLOqq zsJ(@(W})(;yl7u%U6RBy$Oziqq6J(+;RZ&W?6u~gn~2lpk?OW4x<5^!X>-3OgVP8U zmL_MBDF=mRdvR=az?f5Acf7bkr`78iQ)Ek>ibi;XG=3tOv)WZbB#vX&t+`#A?4C#y zH|aNq>oCAo{kotuDJYF2w4gLOf+nkJ(Ty|ifE;M!Q_J*`@|XordmloaP>5jlHYfR% z_=j(GIT)O3Udjh{IgQ8lxpz_*PY)UUWt2n#g!o>HFL($l@%nm|`3Y|<(m^fi=nC9h zS{8&D)@MB#7md+5^B}&3D39mJk7ET$maEbP+-I8PIBa?Zhq*bDJZH8>okCZBl<Kguv3| z3X_u%o7}-k&FX2?Yl}ox4x4qzk0Vmtw?H4}kTYet&0%N&;k&Xd@ z->t-NKW>fKPJg{TMzalh1=b~SfCe%g|q}bOxp1{O97ZFr^EWxiXeQS;8y1IO6zwUC_ zeS!nuOIuawZ-G-S1RC}0L9+0tM}(a>ez#e-H|h-f^tNhgm-gz{-&wtiNGgECnQ}p3 z{==R5IY5NViQ;b5GGT&oI5=OA z!2wv7nH=vj;Ud98PvRflS){VG93mCC{316ilU^mJ?idqOUr-k*h5pM#@{Z34>$At8{Y0e zAMbziEBtrS+$})P6Zcdx?u1K!hS!q2;ij)9j!j1PLI#mYbiW7INvn_ znZpcMI&{9k-a%nn*z(J`Q*_KGpb13Z=HZoty&^>)6TgQUMGj#k4FY;glVhf<+Z~(# z#9Mg9NQ$*py*vbjze%?mCg=1;f+LXs=_|Ul9S@Pt0~vj~J53L!>jHEm;SBOa>a@~9F`zOu zJ}_@0m8x#Kelo;GPUC@~h}x}*U4iAM60=A%9_dEKbu$-t7EeRX5`3xEOZ6Jntp9P> zFD~+AcekkM(gZ4THfe5OjEb1T)i4J&){Tf+y-_vpnBg?Yo1IN}R{dP@we!= zm*T)v70B5G^Hes5FK`7)tTbtL^90O2iVFuBC%uGrC7a{tt0+$cUjeSlbZ%*~0Rdc5 z$Zw%1_xF+Js!l1>4P}wvauJ5FOtpkgKBYV^TB8oWQ78UL`NZ72V)#qmYjN?HlqRe| z&QF_!Dr0@hB2Q@>w*wo;PBrM}(u7-f1nQJ(KBD%%akKr;(L9P?$K0l}h(k^A6sME%HCMF=SC|PdmV|25Kf0dRd*I#6{z-&!2 zw0SIvI%vx>?Pfi2CcomJ=Q-T#7CN+?pu(vMeYy~X+s!{`r;4jgS7VWx(8Q;aY|s_G zH75Nxex3h*t)DZhG})t32mqqs&)gcOPJGBHg7u^|4OC26e3TXI$q;k2O=-dk@b=2m zAQljemo6*QC;9m@jk5kJz%+aECjVurmuP{7=X3Zk-zw8{R#50DHV&1`GV+juL*ow> zMlS_(BL;WGnL4;8M!j&Ba;Q*KJsaw^+~(tRgpYH<)lcaTgE#@uVyb8CQ;{!(+P`i< zPrL62S%p}!3hLe9l$!MlXN(K$3j*_#KcId3gPyH`?fkL|=V}Q+y)FjabGa~Q`Sn6A zlkq;iTMJl5*N6V8Uw6!me8ovTnI9k*5$g%s^xx(I75MKgGFJe)fFK2jq%Jx_lq?Fy z29%OFpeK1R--}LIq{~+YVVfJ!Mf^w7@;M6EcGd;M7Fv!J^m^4a zY3v0t_i$JSW`x~n1T_{KKsFK(|i)~S*?Tnw9!Tz9CE|FY4*B+D~-Ks)>OH0h&N`eIEmew?(= za8`*&a{w9z#-nVCln^1xzB(O>W|94 zin>6s0juicL~U*i1A(t`XIDgBVf^AOl?7K1Y65RBC&{NVa96&GO?U?MEhUO^L4rPh}1QRK>qj-}Jidcl2IQi|{0zIyWk*ke# zsiZ6TVeS!7u%bq{)avDiI5xM^Tk@K?FP}GbS=y+1Jg_xtXJ%5V$9u8({1z&!G1deL zH(4-F`>%_VX;w=7$EnuU75)&e%)~PN9nJ5f&R?x4(`{yd^bC?i;ouNvEM7xiDhAE<}&rNQg>Hr zbd461#bjhP2R6x3*ObyElKw2jAZL=MN!%3AeFppk)kr`4ZJ)M~>1wsP(Z7zVTG|+} zdC=F2+Q!%*@AX(t5oZr1xmw#gHO|qK2@nyG9`5o=UO|a<_^$fEIUVdH95|$b-wXIJ zg?Q}Kvz5`5;5vtBxjTKBBs#2i$B*6BsrC2>v@;_Aq(&?3&$iTuE4KhfqT8PU)Q7jH zhR)Z7Gr&Qb)uSbNG(?RnwZ2iyILUpQgCXE=;aIpn&``7w%66&$x!s3w4uIBK zE)95;HCkZC~V8SXLRjbtv`fj5x zVo*q{mUn6HK!|-5-tiYAC1V;p@{hRnWD#(rC-UB!&@Exzywo#*e5himV3h5EUc$gJ zvjAVUF=jn{pBf67F)SCi)owCqImbGn!L0F6@_Dv{D>|cU>CpjQUKJC-v3JKIHisXI zpg$TfY+{d~0@lXjCj;vKLF|6TKxi}mi9BNm!hGj}7LE&}v%)xmy-5Gi;VU!ItIDxo zXB9Nb_i?HBg})l>Utv@D^D^Jc+5Yt@u=hzW%^;67I`nQm6g6h>=Y|j;>{ZE z$MWkuncwA~{0y&d@&ow;KxG1y5g4Fazs3xm8F}nnx6|!3VMhktyS3FU?vW$>rf=o+ zK&!fG#t1O@aU?e`$4OruoxwU;6JH7f2ZV9Q_v(}QO}2dxZPDId`Ly2Z>3_snoWboX zc{^mjzRV5_UkWeemEl|X@Pj$A({*rp7`Qe%e$?un%pB9??MIDzaFJ*ZS(DR#t-6e` z6cmuzynPXaEpzhoa(d2e)+>E_dO&@=6w7PrmbPs}y}wpmtvrUJ{y17( z=7Icg*GKPB{sYQ?C_XU1TjRT8ib3?hb#8FiZ^A?0{#-ce1)DXOaVmcvDlt3!ACY#x z9>VZ_r#s@^kd4n2&iPtUt2@BPjk>l)70kZ8Mq`IWOkR5x5EmM!!Tt`GUd>u;;>}SA+aEL6e}U=t5!cbM+?aEa+No9!SY6JSQf22 z3%fMx@qix>q?3ioy*FpYA{P`T3yzFjh`a+BR;c_VgQudlm-{}=ifc)k?)H|g_dhuv zb2#VFtqWZ(NI;)%z2KZ1bX>PuVRShIy*l*?6+Whl7d-Rmb-nUNw2S1)-*{gOijo*? zZR+4Jv!XD3@bsuQBCxoI7y(T3!U#@LfjD?g_{fBV_vQJ{`NtWL8P){LIz5)RHCkpsVOx@;J#Op^B!{0o z)GP0QKJkCT9G8Bz8DlIF;nd%GsN5xx18wR`zx}74kelWdAqJd@!#9sRz(g_(3$m_9 z2RQWbZOcG(^=VR?Ul%c^(L1!x%?%)mOWs#SezKT}7+#k5GD&vT>EaQR;GrN%Cj=>5 zOz_BspTUP*uc=;;sFFhrE296TaV~Glt-aboYlUovHF?W*nG6SdH5d$m09X0#JJ@|k zVQA8F?ufUi>u_&QwLZXO`TSK8@9XT=3I5V4%q!olMO*aH>uw0+r!D7cbK)$WT_+lD zEY6}5m-}0QvK1-c0_Bd_-D>JoOHa%Y0bt)kPp8)%NcE#up;(+O{oDC^2?glg8Rh2` z6brtjT~oS%*m?UD2KBcKJ(q#(LI zww?jaJSPtXNN06FinL)K@+Osrubc3(Y&o`=oXND+w$UI+GF88knF$Aou>d+^s+8F2?8j-5vYP6q?w%DM^{4+t_?_r-Pzt7Y~f<=ovRFnu`twgKn=%q$WyeSH)E8^w@_k z;zKMzJ)eUNCM|bajEdnoDFb?abh3YH2TPM_nMkQ#ddOkE!Au@>;CmmG=iCwU3|N@O zD>XI^T_MS89X&#f6D$V&C#!J)VvK0(ee>vKd?o|lQ=gY*KQGHKsufVZaM7u!!*%t(Ya2hn(8l2L!Y@WJ>RKPJRrXa zd&H+r@fJTepbZq6`b7lt9xhE@^}LA*W}%m!9L?@h)r99h=nr3=+TwH;mCYgwiUj+` z=?Oe%XdJ-LopE#!=g?G4>Yk&bONcBx?Gt^4syxIcTJ%_($>KQde&T99oZd~VZq(-c z!`V>Sf-D=E4I130YqC3nw@9Og6P!5%et0!M`AkvL0bZ+-HemAZj$w}u(1vdvj?`oeuJlN_fH$v-`;Z{5e~J!Iah4cm0p zu-K!MHcL)wP1>uResMGnxOQ5M)a1sci+^J#`C=poh}t^97z}5-4LJWN41_;{3YS5g z)+T*6(*t4An56tYbu5d>Ba6?>2$0)_BferXU*TL4@-fzANkQa4?BQ=|NWWz`;ol`` z7?B;=ve(0XzG=iqeZn7=eo>J}lyDnHQ#(ZOiaWT^DK2Yd+pwMA6#JTe4 z{KSx$9JY<=x45}|Us}8{qVf3O82??A{5$)W>-dl|9bc)z6uqa2B0F054I3$c$eaF< zdp@`?(0zOY@RjROntnps$`v)`qct_#k)7#RJTlanYhAV}kfJH7|CKvG|7XNrw0bZ0 z%B}|4P~?`6KU(=yG%ouDLa6EO-ZD!Uj5uTHxAcs$BZKqxV0y=CjL*0DLyrF7haBb) z+39@30e=WN$vLjJJ=Vf>M{bK+9(v|{-lY%$PVUzMK4XK~rUXPW@FUZE@ix7beGYpO z7GZ}@%|3?&;F~<`WB9UMTc$=IEv|Xr2lryT`MTk20s+gRYW(0QY_+$T-t?V}`%J$g zjROBT5dHz)k>YzIk$&i!r2IyvukQ4w^rfGW>8pTSxELd*6^F{gW%ihFS~R37VK^Pv z7sWUs3_U7qZ#YkX%WZ55D%YXm;ag9~$R2hibrKgw#6R7Xul_&Rq|*C>zTKxyH>6+0 zrexJ0*RR$hXWFw7bKKZGaBuJbRrjoTIii;D)8X7V{rlN=40?_d+fkm$Kh-I19I)@6 ztaG!|b&fB{8UTn`kgn$~~LaOR;k;v%$gwB)9L<4oCN;qW$PD&dSaWCct zGRGTzV8j;8%Rb@44<4#Bvh}~p{g81|XqXxIVwL{K$B8XK_!xrafa!vT3p&ouWrwgOhwbIPGMHl9M8LG^q$mIhml zvm-K+d|t^HlyDl)Rk9!gQpp#UT&Uz6B@2~&UCGxX+=JIl&;R7BO3qXAB_$Uq`9_p) zC*M?ZQE=4B#Y(;$RBCdml1r23vPwyZ zlAkI0p^}@G+@a)7CBIN|tCC-u1pAee+mwJF*D1L-Jk8`DB@ZZhC~$T1YbAGu+n(H~ zWUYff8zUQ;0Dba^lKZ3TJb5@mX32)=ubccv$?ugsqh!63UL{W``Jn@)C%?g#Q&J8;g^T?+ZN)k+pG5vau|JH865h%XEbE7M41$iixa*`koba zxN~b%cy^GVqt4Zy7b=g;>V=wpo+c6j|7JunMtw);UZXu%#-`-m8axBG`>1C$s#l|) zi`kT8|El7*lt=%xU0N`xiaZ2~thUIhD2hHU#EC5(Up&7K;O?!m<>{Ek|4y67=g{7L!fhqsjfALSDOeLvEWV0VM?A@E(A9PVV5 z*UNRdl`{_e42bAdIiD5jD|98=G+ztR3@C(!`UcrE;&v4r|J-go?kypljkY-Srr4r z&emSF+Ht-nFVJ2Kb*5F^PwTa3ljeU_#pkKzOOdFv{9+ZmiPl?}YSdzlrs2|3jk-c@ z%c6(g{;ry7TM_l$bI^MvuB_51KK5o6-l2j!Rs2hhzDA_y!r@gv$ z3;fAOUHGVq?~iEX0~;cU@UivLUhj!bI)bJ6vku*&(;?a3jNs8l&ui*S8s(Wpb4Mkk zb}E0nK34 z#x!hf)&I1s&k@GZ-kd9IZ;Cq81}2vwDkGjj0^Tof^w+Za5fz-P&06BwNnA;gK)P|$ z2DF^(Mh{_=&j&N?^2p>CrWBnVS;XJYa;@=p$=~QjS_8}7DN(G|Uy#-Zj5zpS?i+TV z(=TU})X}12ea0Y)f0j<5Fn1F6bDd_s9ORk7so!B4Q;At;w z1~?9Q5GN;UfbsOM1>0)(s-Rql&G49~SWJ;PZ4qT{M#nxV2E@_(J?eYVRwq`?$QMJ#eJqzw|n@@yIIGu@lXUVcUnp+ctLWR zAxCvl^zT2ot zR04Wb{Y0|PQrWz2dvY>urgwFz(0FfZkDhfp$AwwrS@_Q)G%zZcYzI6D7I3=?tTQ1n zX~kSb`Q(u?Eso5M6Vfg*)lu^Qc#S?aPY={(=_T1Aqy}xK(@Sn`(KqpT+G8NmluTm; z#qE^r+cTF9D+AO1T8|d2(V?#4O_Thu-=d2$!6NO=qd9Sw>x+{GX`h5K5oei`b(x^V zI9hWMJFeDWEZ!+Td(57W;JddKcZO?15<7i4mNrI@=qp5-XWtku=U$sMlUxWiKI@}3sC-YRW~#HAnt_%zmp zQXNN`hq3(&7VKR{hd7c|(4z8I6}0OYG^=dWvBQGhE~I*&ljhyLevS5bRnFqgf{8kH zt16Mo3U;b1vZrGX%pP+cmZjAPW8AW4a*Sq9TSGs@F*_>)`oN?!e9Y3d`f0YI8!Xi_ z*F=03cY~QnG`=7eXSkit2*0nRd&p`1!5W>?t4rM~WGpRoEY%7)x*bC^=;?%Th-*on zPSduJi;=$Qy32?WS855Cu3CFDRXVzU0j*b}2-x9Wt5{oxJqw^z_lo=_~= zfV=62@~B8<<@ah-MJ_NQvAO&A%d=nY^!ZP3M z84!RcMT=UrmbJl{46f7Wi!D118*Oxf({~Ql%D!=MeWYE%w-euaq)uz+N7u?YMNT{++0(WSk5jSbqYZ)MHCV9)y4AbcMafXhzS zTsu3Tu?(+P>uK!cnNi90(mc(si%i0M5W^N{PD7NO=SH|WZ{~EI4>MH8GLTzZHUiAH z-Rbc9P$vaNwnyfmE$`G1dLkKkaj%+O?=#oK3#Mj1&K-94yVDi(OvZy4fX)?-Q9A&1JnclO}zel-Vxt3-k{8oR_An~b0~;z~GB1<5bH5rD>* zF&Wjtux+l1{+vhDsWfAvQd66AdIbyY?EpnXqY_^BkcgMv(s+X=w9F1#WS?gyD-J}bWc>Ab?^VW1s2TMy3F(s z_}kxi{SLtL=vH<3aK725;t`U3!MAmBmo5YV->=b~TC_&@_UiOaddYqOI_81_MQ}aRz~T=q68jYc zxXaZt!$pxKid5_0-aFvOb9LH{G@PG&Sb*}`PjBVo4cE<4=<@8MwQJ@7mAi9W$O1T?M<}T^ND79mA>kF zv}bDdwhib39AFzzO#HJX_<33)7GX@gI(?QRYLMYfj#JK^-|~E%*1MHA{p7jSr9Mt6 zq%7H?amE`YGfc@(o+ygkiNU=i9T7w{-RBCM-czM7xYN-Sj`lu5Zbo*f{@oCM`cqAM z!Pb+t;%L#afjjlEvB!jL`}SF_9wH4S(v&Sv^6eySU$bsmXn(kVayU!g` znml4&1$?{K$9ucsA~nD7*6OF33_qcM(EA0hC|IN&Es=6SbB01EKfE{MY!fnA;$1;?jEp%*<@avQZNq|q+0)Szn5Zn z4%|CUd|jwpy;;w5rMAkSk)rz#aBpkZ3`YXAJ&T`@HLvn1L+yd#%eo2HP1vy4I9Oj>269n3J8}Y#Kt8}za(FC)U z=<2jq_mQ4J%VamPS@&dbHJ3t zroLRKN&in?L1(iL$~Y5JGWzJMl-{pZ8n;@vW<3*LqO8Vb>9?#55#|zmI)BZ8#DN*e zA-jo_45Qltr1U!c#bJVX&Wz)lQtOpShLpSNj)#k~o=l;NC1(b%lg6sPg0 zvt7`Wwj;S36<|l~S?{zS00Z|mX^GGOVL4T`>cMt>(Glm8TO2>SJBoJMJMho^E-mZP z@-@+TisH4UFKD+HwcF;P)Jp!UQ3KJvoV$NjaWW}CG8@6hZoq1cp zkG%dEO&I;FNe49!`I!sSlcK0U#q>Xv^&7*ob%wHNzU4TJPUBtKLfT z&=&RPw1?+N?n@7l4=Tx}<{b8N{^VMcsJ(r(MEHWw6|Vgl1Cjy+W3%i#kIPT4C`t;( zB;z>ec23hs0i-&5n?&N5oyl{9WdlVAVV+_VZy&Ja@h08Ytc@0r;^kx8wTbPcPA!#f zg!Lbuyh+M;ieqP{4Zt@?qm^%M)l7r8O7;;6y=e0x96Skhg^}A~WUZK`S#^47GXTK2 zY<|Hp1|(C>BjL_>K*QSZ-)FlIo}4|rl|D`>Y`bKm@;v$dqdh7}eZbApaO0rY!!+cP z|7_$|2z6{)63dHPVCuv=52C%JT#uloIbhzz=W8_H3w27JZkr!^ve^4biPLl@E7|dR z%?ovRi;lP1#ohR@+x5q=b=E5LPtybc?@sO46Ltzb@bqTgXJZ#`DX20vyI!R$$>TMo zL{H{QlO2UI(V%2{IQ4_6c-B`ljV)QR?z)ie%2aJG)VhXnOX*!!XuD8pxbnqTy=@=l zmK3Xy9GGcQ;L5w5y0a(dr2KW0dfkLP?N2iTZ9$urd31S_iiB z!`TvjCY>C=nx!bv}6#OX?bE=%+=3Pp#Bh0OuyuFu-@{)|Cg z?*(y6-)!O$EY~N^t>LFYtWo)!rmhj>0_&n5t~Pwan%}Wdw_7djG>{wB7|Hy9Y>7F` zQ`)t^mx#>Ck=Tir`}v+mL~?aWXEe93=+#s1PqmzM8uv$v#v?m4DHG<8#mVjY$(00i z$0WCxCa+JAJpXB2x7rwj!#TN+9!-n%=QYu2ov==RpKAMsjV?(GpUWaBMgjFQ zI;qx0HG0)<_|#fm=Hwq(UozewZq&QJs;`)4UyqyMa@}wB#-N`cZjOilZ@)g|+RSZ0 zDOX?qiGkg`j_OF4PZANB3%s#8#?wz3(c(oqiliNCD7u54`vMhGRF+kSpOvf9?`!mv zx+pxkra`}H7AtwqBJJOz&1|lVxo&ab=0+c?_Dwotv%4?aDS)5<^7N>LqL_|aj3{N| zu~kuE_%KScR(m*)3-R(B1<=~dA2r~TnK4F*C75_BO{mlWl2LgvpmR_){Fm~m{zIR@ z4JJN~d=9X=)R(lRF=`%v2pP!qlWrAv=>;0gY|^FHoP6W%{VD^{QU`HTAl}O=<58re z@RkYZtK%CH$Jq{dw@AOk_rEEU@Dmnit@0m?LXzFdHKm4*O#6alC8Jy3*HZXik8XJb z&xJ(y{urQnudPFx3H+Xo!YNk2FZpDqMAKY;o;%X6ue50^#7-8k=XMNrC>_oN0coJ( zfX$CZlz~Y7g1wNtfwwmpE1}@H*z29cSLrC0pXZt zJ(9s5l8E4%P`PU}7*^sbw1ZtkDW=pVAqk!ZKIsEn^zJsD1=JbPgBbvO*m`wFx#sb9 z;)9iX0&>1eeQEe?o?e-+`5DVJ4;emKI}ZXR6&d4rCj#84C7FX$Y~)V@0~+k-JsPkW zV;wAuWqDbnvP_JZJ>_S42Ecx5is_(ip$eUUwW3^|Gh$Q$TR*uR_`j^w{K*(nFr`5dK%2lab)(m*Lcm@K1>vba(5gU7F1{Q4Q1^&DruT6`QpllSB?x zpMK~tP`&{eLw{g|mOGR=+qmKj-Yo82C1ir#(h_%3gB3*Wclg-svO;6Oz^Ewvv$(mf z`jUyt(V5P#K<9InV3oVH+{<;h8!P>Llm3H9hG7J!@CT0^(B%HW^RPgv)rJLvs~n)& z4rbBvR8(sc;U1v7+jyqIlx0|-1}!jqK#k;DYS|13q2C`fVE#YtYD|cwhJjD550UYb zUUk8bA2iEd8hmRla~U7y$O+ATVG+YtRe&uMH8 zRQed|(9UsMezM;2vS}G)&9geX&MwBNn#iliM!*Vrf6{$-X@S*(JdZ`KVuhOmk&^(5*sVWGc))>2l_ob&(dkU( z8wOf7)|jEf8gf6$w$f8kT~jy((7H({&;(xe@UD3Y+WISWvHr zsMu=Kqs>8Yf@l8F8o2!&DxEWpG@E6=QCTmis(|LVujXV~g<_hcd5X ziiBAg22;Xq4w5)gtsAQJ83PztvNSQ0N(#chO$7YI_%5EFY(1+*%Qs@;SbX@17&x8pH>qh-M2|jeKBUrI`D?!N&vr z^H_S77V3BPfdRhOq`i@(Mfc>i4;+FASMBhXCtex513q7+VpPyCRpFpaN4Nfox8+vb z>wBUp*@0PvYvEQmLxw->S3d5{c2yW$P4mgQ4x-gD=)RPjs)+ud`LHXsI+07=933nv zQDE`7e+^gPZ3aP)_Lxq zOic5KxyYhlMRzq=S-a95UHXH84YXBRulC!d#%YI}lKJ;`1Cxo+_@K~U z-ATi9Dt`xbXupoC2{rO}pM>M_yn3R%1*CCDRlv?#TJY5mjl8_v%K}dbN_@x-l#2U; zo0^RYZj7Fvjboy=z?pmhgnxInuWY_ZOo7^}tbKlmww==EJ27B}t zxa$d3pye`v@z56S!l0*hZRp;&B7U))R3f)2PV+oTFTBBbs?U0FnFTw!T07H(Rr7Lx zo3Bmj{mPISQNOgT12+D>TzF3)F|uv3r;&)hT5V8sMq=x5Er-+QH|rPFWNp>bZTf5m zdmbRaD}!W9Of=nGuB$D^53PZ>)~LlxbV8kWPqQ2|?MH_EKSoHhF6HFYR-$bxO4lk= zgT;28&O$2doL~0nFqhx1`eSr*2yC`ll>roe!SK1 zXT2{KZ8|qKk?6hD*M4Y7do*{(0lt@xd7IVl`ObxXDz>FwYDaP}A7iGv^#)CFOppscF+0X@)JMmR|9U>=ibjoIB)U0OwCkge2~J~cR;eJBhwx3e zzS^nldQ_j%TrX(yi<-PyH*L|;869^jN_cdPTbd2L$pR>>)s=K5HJ)$v@ZU6DB(FNU zRs5(XX0k&CfSG9x_Y4-?D~4)))rgDG()q5isPORadkA=C@$?}(?l2tb8EoIWuA$^f z(ASr3BS_+%ni_~cZKX&DDgdfHW4#Djl1}g)6F7^#V!PA!s!Bfxz*`K+^HXTK)13N8xezuo*>3Y%FXaK+=a0Wp%tX6?u2TByStbX&can6BLEtdFe(c+4U!x)ujDqOV5QES!sk_Qn5{&Zz=m^FcD(~YEVRZFO zdyE?b4W?;PHX?g@P|JsLD_mL7mmTtpolZtV0+FN27KMg{2_$#DO}7oN(%mX?;?}Be zZN;tV)WybQ@1zT5+HF#H0VfWG*ON-M%3S5a4m1<~#>(%LHsNpTd4REofCoo6YW$+GRx1D%z1nw^X7+hfPib;7 zApgPW-O5)$1(TFU(6#kC-!mw2Y?rmDzE#h;i)_Bz7E!H0?Ie9v z_QuP=j`T;ruYC6wo7kusm>!A;=^j8wzQ*v=OSr$Lrtfta@z9!>r?7~wunzO>;WqnY z={V6IQm)_KG(0m)z=7pdC3x&&;4y{L>>(k#YU*GEeS z7^hR=uiQNtGKaX;UJ9@Q27!3u!QZX?43?-+x87c%=T8wk6^63%SuH<2uq z@>+z1hSAhIXN!TRNbU{$Xi{sjp%hC2VhMVUkKL>b@ZCUnDX(S4^)Mo3P-@S~npC2V zPUTu)F_xkz>|C#_(q#tE->K8F^L3V?UFuFgLwJ#88J1^p^#9oj`$v#*rHb1W5rnK@ zBnX__brU;fOhu*PTQCxUy4CG3gsbz^ikYUO@n+JOJQf48c2w4)jG~NFlx( z{T%23Z%!bcQVN9lr=uG{)5X@_IS8)GXgW8>N%f9HELYYjv^LYP4@3 zy`8sY$@ABn*Wx&_l{1mb$LV<5yf;Sy^&L6w<#aww_OZj~ncd9BUZqt5bi{f)Su9tv>C>OXbY)rSC0?cF13If; z(`>I%o4B8_OmX9k+e2Yis~l|!?PiXYAoYe+LDBBXOGM@j`C!C_Czv8&?^vI!+Vntn z<8aTnIz~o-Y;KD8JQ)4@tx@c-sZZtox{u@#A8f80zC)jrG2pn?;b(bqSeaw0^kI;L z!zOJYQ=@r~k}q&RVc+>t-InSid>OI| zzXU1jmbqx`%QNvDt?C+Wo2QbxNYjN&xo@E=8gzPnWJbM1XMjcO zYf*Dfvs!foj2sje=3|X6>J8}f$uuyxH6G42|4C`G-jIoO@f-rw=~k*^8p1+B5Vhm4 z-5PmT$>y*cT>QLBO*CfPV_rl9PifE<^%~!-A97Uu-jp=Ub!lsljx;lz+oWlbvF=m) z*?wKfYLk#t8@%q)Mty>wcmN^H!AB!6YvUnFg=@;jS86nFIJ1*#b&+uqEAy>J{SJ89 zt`ob|NqOBGeTs-G-Q&x2Tv^ok!0d6yanHeiPr(P$@C~oTJ^UYG^&H9^T@$eRjQP<# zoPmaO49P%B{7MdQqBY#-9P0*L=?>nE5Wou7txt~)#EQN7;iRH8nUf~oPuHFw*FGr2 zGqk7cbS`ZQLUNkl{>krY6y#E?irY07$AnHbU4e>A9#`Je8uf|_*$d79DtEgUq%lO& zfyN|rOOw4n9+^LMwd%N0754=Cf>|no7gdtRJNl}XZ^aAXxnnr3YoR6@deMmEjPJ#@ z9i$2s%7}+>O?ZyY<(-fu8;7b|?(Najj6B$^e=~dmWji`qze~AcpUA;P+@cm^LZH`T ztC92yPpuZPVX$VXLbhl`QxR%swSyMDLrXq50tK5(0 zbjURt)vBs?-Qyv`LG0gR-+qygOP@FKaL%W$qq z2}bw2qM18|$|V$aZmkZ+GJ3hdIk|eRa(YJ(661UI zCP2ce8fddy2V(BP`W@zGvuMIHo1QBisedG;*Q)h{D*bd`BwM!e_H%~1h|Fow+yO_eAVLJ5bOd;X!ZK6%a(*BG|MU_3m95TacWepIlv(99WwC zY`UIJ3wA1_wePnXLh6h2%rMSu(B<`_fanmE9=;oN>owuSp4F~b|DUw6cJr>JuP?I; z&j6i;n`0K(g?RA;n%A$z2w*%wXO+LZkIj2*JvyyY->%k17p9So39T^^e=s#J4O-@@ zmqfsKZwwjUx+eJWox^6%HjX&p`ffegrM*#oW~Bx*Kb_u6-$Q_mLwFTUg}Uf>il+Bs z3T}I8vITv`{k8pNDektnNc8QDG%+k=eg9dnYn${(yW#mx-z9AE-*&y3v36m35y_yJ zQ$YnE`4z8sY0pmG4TO6lXax3!09*->!1F9NaWE_i`$BLTmh2iw1GySeOGP4aSyZMs zx_9D67{z%yu}&narRVcX{Lp%JH${i@F?dWlEq4fhPPMFweaH&_D29SY%K z_GS`Es;IKM&}P%J%{txL&eu4le|(G3Bnb1~N}){te?qsgI+rq++E4G-MGoDjnG%FlH!tg9<4u3Eo1Z4$rH2#kWhj_r=geXI%e+Ef78wrHbMiZSwKoYN;37{F#KR&*c`U3H@Z+e2D#8Q7 zx-Un^q%`I44PT8HXb)(W{z#qPoUd`d7oj(dg&xXjeYfqcju z8%92?8;sL$Aq}#sy{`hx?e+pM2Yd80gP&l|H|y(LL_nNX{d@*6zL!Cuuente#Krs8 znLjpr|5}Y6niuWU0knge4qxCr_Ks$~H7o|;$3zp75H)MJYx#fR=XOo)Hqm5!~0q+)0?*=Vy2*K~c_)ha*vB^Li=<)(}3-RWitSRy>lt1)&L zYK*}K$qei3^`EBTRF}8JVc1EwzsJ`G&dD?t8?>xjXLRWe`n_`toPLQDm+t`3lpQ*> zAo+E1LRR+jG$nhok|~iWIE8Kx<$BmlR9USS3TimdJ|T48#$)<$rm2c-R?cpUK|IV< z%5&LKNvket*DW73zL+_{Dt||3A(ZluLt2n8( ziGk9oB{AD>=G!xdjxJHEoRe_azErjjN2s6mCSq}JXVz7Ns35)^1=6CoeQS1r3RXlV z0*XD>Dm%%ky#p7ub!uPg!Ce@2#wKmG6+X%Pb;p3tHzY_SFDQ1%d=z_SX>tOrg;_z| z^ORFXy_Y~^&eM7l1&4tOt%ppZTy2gTZ16>I+z=CJVWyi<{YRH>wtYFGSAX`Y&Ozq* zRHv1~;|KHu3^C0RjF(%OU7b#{D)fJ&Og-mRm%^yf|;V7*$GH5mY<5DWr6a*P260Ue8y zQ=CY@gj8N+FYgmYXTjeMcM$bUZ~5n zP1&tk-)Yfpc60V&eBp2un61eihh>=z4|szJ2NUA_JjJ0#@+Sh=fG9=+R~RqRALj6k zO#-|<J=;`FqXshBiZcQ$y81RJl+X=yp_LXEN>rf)!VW3>0#&*rt%1$19b z7YA_SlXIfSXS}mE!uL1cuRIq%K!c2R-vAV2iMj6a+cb`j_X9DNhKgFi_8$3>%0<-m zs1Y3l7R(B!i|9&hloJ7hDrMT>LlxFSvtmSW$wtjs4>MD_~9 zD0l1RE}h{hXCCMxt37-ew~LBz7?o{`Anbsy$@0xm0$(7&zq( zL{k(LQ3`ZtXFVT4-QWijDtKu>qzWO+M*V(K)T03va#(YC;C@}kSh4B)ba^lv&`meO zUYZSKM#o`;F$HD zC`tP!|KfO;CKKU;fDL}*qSE9sFv);c_D2VyNoAo2KrB~VV9*+2yW<XJ6C zGFYNw7tWuU-9n~j4Cn?r|4)x_2A_F~!BxwmNI|J2lv$<%dgj*zCRtG*l`lsw(pI8u z#xqwi+!^r@jIf%s%}3X3s+~a~96S3c7t?AyuS~x&4@SbnR_-8*+OuofI2LnH4808* zOlhu5OHEbbvLPX4)VKR%QqtIuYd_n^w@3};%G5&G(kq) z6$w%KJvznLVS)4fOLoN2=L*K2j)}70Ew$SFiO7BG`1lY+S->&F#P~k0a}!;3CK0-O zGH67LgRi!$g?K@mnr<5EYCe|BXwU8_r$eO4JZpONAp@lDE$T);u!AGoB+h zYz~YxI76dsd^?O!25N#JLI9{{@FWn0z}&x#&9;r;XGl0-+Nys!+DlG#q&cn93fe4W zDLo*i?LGR>2%6|vN}-+X1cFV2UoT`x>EbC`KV5$+*QYH>r(3G7J2P@k>9jDXPI>b+ zfG`Fm%YkjK=ou!9V5?Rv9U2h-QoA-dg1I6yxM`--oGxcN_2~3YeVa@t%p9uVWwmbB zyQH3iiZiKx?u;7j3n7)M#s(DoG!QRa?Tv|5ij7PgW8SE%=1ZQw{S46U~zJE%&RTb>nl_Pl&z z@w~1eh|r{S8T!zo_W>mB`i`U9BT;M4wrqy?>qFMz|IJdZCYX-E!~NKR7VL;|w^V)w z7vXfK9-7!Yoe3NQgH&IsW2*H}eA^n$nWx`?S1rb&P@4D}%NWYR1vsTBnK>p}WW4K0`cl-LHOyhvI-kT^ zB9HSznO9|p_BCuep_wLK(x|B|YHHW$VQ4zS(^=oGW4iQGXH2Z%+QKU?-WOaOhSbMzH6oa=w?610c{$Gb> z@L5IG18@FIDVa0037XW|AHTI%FXBXaDPU&4VgL{Cecgb1M<@Gc0E6-eoQQ{}Yk9dI zq<`D8SM+L( z4?jjEh_CmfzX2eDI9J8@waL!x{^c?CZ%%b2*o>~xk8Aa$jsE9|zc=frcCiLXLVOc$`BMi-MDgUhuB zPr~jTHlR8vn&~>EJSuVe{VlIqjPsl{b%LSMKCCA{-=^KWg#cwvTwjp^q)&mt26UYx z*Z-X!>Eg2tJdWfOHtBH=8^Tlu_(93MQmcmg2m})f97EY1kL}W>oe>JDBp}73_?+?x zN-iPIzA;iOfj4+kz$B=oN-M5%KBqeB4TDOuv@^j}4$Zu}NUu0o3bPVwp-V3kP5z7#hxvlLmX&LEr6$kz^m#_l*G7PBE@5;(t+!D}5NWz3_=HJbfqOfYcSj7( z8T(*7e14C>?KuK)*)(|!;#C;Lyyz$SVHAs1%C{*BpxoeSP4K;b1Vdm11<4bws<~D^ z!+i!YI1Xhfw`(HHj@$+u^PreA{omm(LlW;C)aRZqdiNM~=0PJfhV_$umY! zgxDVOtKV_o!d;%q#M|!~?l0Y-_xyI!7*fOnZ%OTT3ZKXlADRuZTDOCpKq&@JdpWh~ zWIN+6^u@LHdDDwLIG|zR^m`xp`^)vYN=>bf8jwfM442`sc~Qu)F3s^+82Nthxiiub zWLg~gCyPCO;QW;_AbTh#?xi>eDl#Rz>~bFGG`2w}@&#nt9OUyHP-TXfc9GY&Lm$rg zH(Gs`eN0y!))1n zs)3ldm6p75sr%IYR_NHDIE|`pgbkWgnslA41-9CsPif?8tvx;Z+ceeaBlA>Lr{(y7 z8OVIaH0i@sS~cpnOM(Mza&hGyS#>|+1;Le;S$Pf=tMn(@)ZDN9P8~(J;a+__Vgil|J3Cix8|JA!WVj$Ye@sGJlFbJ6+d1>J;~jc|&P(PBxi(1IPQEXz%=_uUH;X zoZ%NO`i}jPCG8?-iJa$tL>f+5a=dbTm%c*=h38*T?1>J92OwOVO0v`(U_OZrc8hv^ zt^ilRrU+uX)*5Fnu{=jr>4G}_4z^@r#4nz5>eQ8vmRAdYBYQma?9 zV_fC*t)8YBpq4a-Av?plk#9QPx*-z-=+cVnGpUs}8!771mF{(LPs%{Ppu!g;6=V|e z!EM2itneP;rY$$%grqGtyH!lBBH*f8O;wso!my9=ne#QKR@*YEe%+ue>vbF5O&fJF zC9XLg)2dHeq%FgTIm<;Kv{V~9^?A3ac-d*Cd$3AdbW)$bk%@6z#{R7`n_W)sH};W< z8`DGYP5eUeNF|MKVf}}YzocL1lT+jrYx1!lEswDKRc;jv1JF?`dXAAbG^a@`=t$70 zL!IgUUFXS=v|rM#rFcxfUsHRvnhD@EyGEEkUEZ(1&?n&*=P(ltS`?9b(ApjZMzb99 z2^6M5l~DNz@)^*_`gH*mQ(3S9q(D%KhH3(rj%(20nq!RhhDA}!gTb1JY3S0E#!Fx} zydlyf`JBrKG{0YmV*>bCJ_R%E z2zj9QI43}B%v>cbQC!k;MsQY3`t+*N>_rsguk z!qby=I??ljWa*jluemhv0a3}n?xszJnl@c|r>gi&P5XiJ?o{5dlz(5u=n-kDVE{eA z3z@*UE8>+oBT4F;xY7A8rM#_0E4u<(BWfYV8WjSaR`jX}zK-yUZ7z3VKWZ_Wb(PzE zp)>#1rDN9UY7hT?ABbC;Ov=c{8ya+4vmS3$^&(AWacBSt%<~M^fqqS<&o(26oHEU0 zSD2Z+oQOLEBiGcQ69pvAA)&q%n z%UJItU$LTJAE&w2bX`YkL9MQ(q_a^V_ZOz1A$mLgwK*QR;AtG{dh%#c8z%QFI#ms@R(oUq~G=F(g9(Q zi1RWR_4_D_YF$wyLQYFuFN{z9^ggZJbFLuM5|^x>iTjM|ILujr#i{ zHF1~?N;~{m?ic6(7xY@`iL-=eJJMkw^xm9-xD*`J5~JUeO0jG092I8;l)K3f`E~e) z=eBC9eV>nZh5NRiG*@KvrK71nC#QmIGGAxZ>)WubHrbU#9OxK^3|+yB6Y|D${9>7| zML9b}yL)}yJGyb=3G%3krO5{W74?{pEsRcp-(;as@|)sbPesD2n2nAqp4AoL2hJ1E zdI87Pk&9dFb#PDboh8FcL1OKN7!woW^}r<4;iG)Z|QhGJBB3N?X+&O0qPWvQP!25-nBU zpEdrqcxgpxax9DQ1eo!#m+V4mvKu2}i=+7ltHfK_MBY9ZshYKwfAXA~IHkGgRY;MP zW#Ak6KXl^IFM4Iz-1XjEK73M{ZXu<|v($lwJoZG(yrN7$DARW#HylXfFwEp#&RCvU z797!4{Hu`$hwT)H8$f6nHJpp}&APWyPcDiUDEVEgqlGCL!=aqQfk<;rBTALcx(QwF zDcIE(-TH?!6Zv)flmuI zrOu07QKs$wG5(gL@*7U0e~hKZ0`gzRlJv!)@_w!GpE+#*A6st%-c@t~ZzM$8xuf=j@RIF-Cd#U=W1-VuC7#}6Xke4r*lUt^r-6>hS$d@ zcRN}mDm03mII>q6V`Mpt#g!qVNb&&7`5wipv6OCKtm&5dQN?=G3q~y?y_2nc>U}TR z3jUW5@ZSP-um8*Q?cm$^k2mGZAGX37;S0hyjP-{X`I;nFP5T&?QT{xwSUZi@X;yRv zj0y1F;aB+n^oi)wn~F8a-^>sBa-RFGV(sQ{;BkBd>W*LJ@3U;AFev;le+IjN9m)ua*5mZE~N>`$ZL==ws_n{s`>LJwB!hn4!8 zZ_pup7;mLRCxgkIYa@JDh^5ASBPZorQ=x0XA=TP|kk^DPow-s6B7ycn_}UUML%Fd2 zg-FyqeQe)q|2EMDWfU;U(Qm}#P7*XeO};<3IeJHc!%wHZ8#uo_hNP3h;VKQRv@d|f z5sdeXLQOSk6NPlG?=(5i1aePDzF-8R)8rG$M@Kwkm{?1elLpdZ zO(d2m0bj`DAeQ)Xi)O^w$06jfaKL%f4|{@9_$g};dmt_zaaXLogf+YI`!r;UTkmsW zhYO4Ky<+W9tb@I`t9=nR16q7fkM)VL|9qYuW~=*o^G6ozNdG;jSQqE$aCH#;>=ZT;CSXp76&-8d( zPlSP$oM#i9V=3Y%{Q8bA%6k|d*U^#Nnh!RZs%`e}DepSd&>||S=V-gR`qCMPlPR2Z zE01lyq1V^yx_VL6)44ou_gFWA;DRqR?PuN}LbqRzU$#MKYz%(XGY;ddMow%!N$M6@ zhS{G}S&xAeKf$D4X?U~e^vLqywQZa&aM6>M0Z6W6qL3eY9TA&Vx(6-6;jYxiRxPj( z$o0J0q8N2UeqT0(gSZFbADYzenGEAk!1JETls%KX_zVA0vXK#Kx{JdH+{6UrQjI6n ze@xW6l3mR`MaPt@@1&q@u*6E7u+5cS2wM4UI^!I+8?#u8@je#*S)2jk3!*$ ze@_EG*MNUUAsDo`UA8K3n{MBv(cLC2(anaQ;iFYpsKS#2?XYhTo)eS!xWXjJa=9IJ zLBAoE1Zkv%8uJHYw%q{Fv%_^|Cnq&4_o;}RmHa+-@^~iAF$_*gZhlgbuY$qK$ydoP z8i6)rvfF7sn(u1pSwTT07BuE;o$x)4@L0F*E0lAM22=iagBF8ZejH=?YVV3kPn4d$ z{6w(Um%ga6l)|-X_ZKwwO_jW*yjNlz+u#pX@R0^=6TdwW7wI77?xbXll3kSSr`qk6 zx4%mAle`0BRXLxkWQdaOLmRLIT#en4C?X!@?3@I_66Id4BkI)e23>i(rVyP*C%3Bq z2eEIs0CT=nReth)$0Z;!cAuo&X%X)w`>d3DF5n(7XzU|?t zuiVN@HH8q8(&~mO-L5Tf1`67;QAd9eeGF0>-kkj8XfhF5*MjbpP0E8<>I^-SJ?nFp z?Yq`IKRnQGCr8k_JQs*{XZmQ~Vz~y9)gm!L-0z{7PRQVoSjnThCS8n~46LW*1r<2- zO{H)**YE=myrY%DCz1Muqez5#_x$9DY$gt6V(_(i{c$YG`H?i^xxlM>o`x`RZ`|i( zlUrs@>|N4QgWe+YYzlBU4Sj@R&>XXQEf~1jEOfDC!;sz2i_j@{^QW|A-f` z&2;FFEV7LWhNHEiQ9F4mXm0vYp0z?n1lZk5Vr;9{5ybW*Zsuiq(|^YP*KgL|CXn#5 z$(O-ZyDmRTsl^9n&pL(b#qUNB>Udc{i^+Y*`ZaIzv&QmLD|D7?1E25?lR6}QiTS&9 zgNoe*3eFN2+(eSRAp~*A#YW7kG_(4N16pk}FlmvL6K5Mx~J{e}K zz`Iw|q{~_~q*a$>O?6(Q^B4mB@9+oj<5_7`%@Wncv!?5^8G4@#*ZDn6m=S)^m|9K4 z6Z9jF0P3KBnsjwb^l+Q*O(Un@_-`H7KbggMCP~W3`gfT=bO!v<8R0Pg&ev*)zYs~7 zgjo6uXm9dTg%8B=2!y$t1@JBw zX4;B|`r@U`VBE&_>D0{Z1v_{_igCQEYu)o?^+Zkbl5XkK%MPUh{{KHqqETUL9N9g3 z*=^USmtCYU9m|b<@JhC{sNO4KoFXeBJaQ|0(y3FjmE6%kDP%ET$zYDjEc9Vs$$93+ z17L=7aBc)e6VOg10xw}F^wgw`QtFtrO-xB^zwuW8Ukb13ZREZ@sP3B%>TYQ<{p$I93@2 zN}Ju)gpAb-jhfV(y-in3Q+S|)GgR%t*Z|n73pRFi0KtevNSL*Y0BvZRk zeP8rBi09KskJql_w2L1~^cyPRG|w_S#4+&gv;F_KADUx_aqB!C;nA6deah+NoTf)T zo$7)nt+NA2pnzMsKLZy!BNw&__}Zn$_3EcvD zz9~g?s zM8bLE7d){e*zapdCHU>=p89@K@(%}H7ZfEA5i)gBK2>xe>Xi5R!dErY=9$_xU5U*E z6--@dzf9mmf7WXL)e+003~k#I?bAy{T;4NTZ%@mA=p}a;23Gyl*Kj^ahNar9peHZu z{O_>%JCK3RG5a;oY8woNHVJqHkSuTEc~`^+rryYk9sBDXl-{fN?)cK{3B zuj!-g?x0uud;Lo?zSA!TCmUU8LtG`@?WNf(beHLJ0ID@iXgOOtKn4CtXZi|s=w;{q z3Z>^oGNRl5TJKv7$Tt5=z(#}KzFj9|+Has=1kAR{Z=!^icLjP&YZ00hRwfltDg}50gU#z=-UVQMub9~TkU`W$zYf-x z*P$68hZ5daCQ)JU4B;@oy~E;p7vc|57nU6jqQ#5M%FYG9HN6E#{r|nfi8J*+hjMO| zkW+RMwFW+y+j_6Vd@c5kIN8PQNY|O}SM9 z$zJ3kUyZ6vy1M!&fATAgF@-G9{@`HK0k(H7>=}^69U&UeSO(I(YqZ&dfCYIBqS$xkXMvvN!$6)>V4-}FoE zYcIaUuhx~l7c$l~Q_Hhk^Ko`Mi~775w|=LqHNx)&uM!I1e~$*@+;T0*#Ox?S`hKIh zHJ<;z$S<|pk``V`dO#U~f87jBLLyDsNj(Lj+9yRVEkKKtVl)7A2EUu1{CrR{sn5mP z-7j@%c2d1)=i}Zh!@JDUTRx`9cYjrn-FS6yFc7ve4NB`cQa$#VuRkx+-sk#XZFHKx zMfj4LnEh&lR(GmBTl^Q95*zQEQkAv}v=ZCH4FkufSB99R7ymI-gx=$rE|f5aBkFN{ zQx46};{OsRSgjv%ug-PXXp7i4j}_Sm2&>6JnM zmQMYwkK#fPrE)GdAmh*$x|u_H*-^zfJetFpZRfAs#- zIIg?$zxoO?1MB}^Ku&|+z5OeLmbzDHAB)gduOCeX81DM&?VjLmzbM;w6ja=W&05v1 zk;qJLaz}o0@-7khC5pwL$2z=5bdkU3IVAXXM=y6At=dA>9<7nbN6@bX?gybZ(lHkZ z9lO^LBi34SLlm}jrxp6NN4s5Pm#XScjs3ZXJg6fcQr=o~2GF9<>cCf(_nPv^X0Fr0 z?`XikHGqH>+5c^twMn_3Di`k|FBvd6W{r`1-FCEcj#usj%315!__Hc_Gf2^AD5*VO ziG9QE0W+VF>4l#oI)Sjv4f2*c=vpIT8&;$%|u^_<**+jW5S|vim5?^ z;dZ<*T~#x6B+@k}Vu?-W|6>rlYV>-&UL_#bppQUD%X=DBo?-bffNrriqH{Ymsb7os zHL)Qz;@xkm-&;X|AM?R~i@XN;ASQ0otZo(g{-TslLVgGik5s=R6RgXsDcWah#Gi|%E0@ViUh~%wr`h_`T)oN2=JPbDTBrXg0(vgja1Lw<7O;hvr_USsU`uHh{_ zxe}|9KV8>W=$0(Q+Db#B*HS+QBeQFXpFo*l=e#av5X{Q7kFT_b9TW z?ku-ojeZ38*g@LO6l^x=!xSNBDxLdyr&;TNYtoet?osvMK?)wIY)7ENH6{gC7X%U~ z4^EgV17U+!y#x>v)1jkVm&-=b5(OqZkuzFt{Q!mLc9)8OY(F&d1hMY>Ca((?Dg!;{*>w=bSYPUYETwQ*gcZ1 z=Sq*m{#AB%{pV`KY|(sRKP*~}&SObLN8RH1{04oHt}62*2n;^p4cykOgPZirmMFga z33TYp1drLE86EnySGbHI5*)3~(XOggwMH*dj$u%@`i5vPyr(0j_p(C5;Moz6S)o6=T9aOOPr3|$e%6$r z$W1n;b!cgq_J%j;Reb~UzvUcwv(0jaBY<4xc)M$$P49mmU+?0)w2zv zI5GT9Kue!4g$Ni2*G<+wYr?a%P3zhPh8(~}qmg5mI_Wt>xlpz<#ENfs%ZP1MACUZ_ zrI-)yEU)3W=ChH=>lrgTCz>X4-RQnP$qC#!_1cllNMmTKdwQkFWr;XYbM+qu&>GC)!w*WnJq9O|9mUKsyN6Qfc4&*_| z%cp(dkD4vI5L{QS?U3hMt-T@e>Ih%D9?3C^Kx~-3h!(n{Sr;|M;Ox!}^KkKx=rk>X z{BM^M($)^1Pw_?DLK=>-1?1*1f2L_IGjV9xY~|Gk5$k8<{$RRJjB2kSbS+b^k^~>E zShZsk5_6dBk;s3ghLDJXu7g1&7dcyt(GP49bP->foYL6#AUCWiSb7w-D>>u*Ok`#Wt?;i@#Q;GaU_}Bz%^oa;aQDo7bH2^^zpbBP~ijWwF|W{+YO5HT;h~hp)0e64R9gGbwq3`1GSE| zIhbpJ{a&v@Pwf=#d<`ugd^ED0{af|F+10Ld6BsV_PSDZmEM&RGjN&Y_Fu2(;7+Dy5 z#kk2x>yS_`({EGtJ3X3zV%AS#Npr%-Cz0_|l?p1OX6Niibv0x|^fC=Yvh|>5bujg^ zRZ)?b!5cd0()XzCk{^#(OFFO0t_R$|G|!u0ALn>5+}r?)`&Q}v z8odqHYSb?lhtrCt?E!@DY&`YJo&!Sj(=o78FfniKu#@|wS)1f`Of^|=eo*_mQ zm)Isw!6uH=Y3zVKxZk*}qaQnf?~tObIr^=^1Di0SN(a^G5NDk(TC7k%SR1DLZTAfzI=gC2NcTb{J2^7X87~bHpRl& z76*YxAieay*woWN0P%nr+%;N@%p3o07W&2#;U50GTqC`zt9@9r=-r)ynYqzXkZS#i zHc)M01en*TS1h-M>3JM5m2uq!zB`?D&a)Io!jF>Z2x zG^LB<^lG-GEt8@YcRbT%%GH0m_NBj~OG?+k^SL-gH*dD?s?sqv>M}4cY}D~CmJj5R zt>i6ZhIf6BZt;5RTePrM|1@-Y5xT+#o!6=Umf_*+HFvW(J22HQMx?neH`_8Hj_+pQ z@v&}fn1jjoM&%fV-kk2zY+Yi7!#z5T#z9m8(2umwr9RnC4aWM&W}V)oOImefn?9xQ zgei!_y@mnn^;}-EkE33Mg+ZPOPi=WzSk!6B4?MWx!ie-1xNqKMxLfqqF@=m><@3)# zvnfg*8?U8+XN)G)ud)~w2V0Uj|LGMvW{xHs=Wy%kY4)ZoVz#T%Km&|A3^C&c$+tFZ zX_MMK!(w5Zs@$6zI#a!sA$3)2002cv|8E5a5g$3{j4RikC8RYk`N7BqnYZ;mFqqpNjn2362)x?m@b!YuBO{(xtD#8l4zC4Se$gw@Towd7Urt5WUpGdFK2#YWS)kn9;s$c!!2} zX%P^V%^?m0Qi6PPY{0y&Cj?rU0_&2NfK}?3v8LbVee2?tr#RtD)|hS2Y0*d2;IwK) zTQoeZeI?%R8+lptoIR5SEd-ej@nAD8mRE9rEs$Gh1P3(@Ouc|IF)ZPN>`frOE_CeA z-1ulPn1rl-rbGU@O0W8MtZ30^%$;dh@doWlVm+H;%#AoVH@W4jpeS`QKD6rCW>9q0 z3_YGTvF}atZu)9WiI;_f)t%mpQa9@DG1p;I@IWYyEnklds_KRKKZEx_!fgORoQlo)9K z44{#Qepd3htG}$$uMvRS$g&;Vs3nH1X)mTdIGUg9&&4NPe5pxn@mm?7{M-SQm2z~=`n4OqXAIwic-ocIL>T>8^P6DeNl~0CF3_!9O6>z z*~W)gc{*4c>qxC0Iaa!Gf4@fWxa~<#ADBK*@3u1kv6cA*5(-&+jrSbxcpWa2BOU`7 z^3E`Bdp$X{K+n(-v$x7p0iFdr1^e=%8`Kow02((Q z#D-*Bf!Re}98F-#7w08A>>p(4=ocGwdZ$J*I~!uT73LtWQaYi`$ONYbgqDoc*Rlot z*`DbKtZ zW8|b=$8nblZr}`dXiS&#)@uZ~+GY+182}Gle1CjYdwuS$cwwr3>}ggDXK2LCC{xPM zmH~Wy$P-46HmKgjG*42ZY)f`b7>?y`*gK6vT^HuhXAM~p<0f|%zLK7J+?vmV9Ehhw zY4aj425>?ggD6lZ)dAylIbDu@yVtn0#B82v)%G6z>nAKfkusN z&;*tQywAqTk-@okRa9Ut;h36qM615xmNi)a5g@nDpms0o>ghoTkbX8dc^S(~7J~p? z=BmT@de~ngIRyQ?hO7Di;t8{kW{AL~IWt6uB z1?!jD-NE%Ep0lf$fn(l(y+)Fsb>C@n93Y&nOco=a0Hkrotpzf1{({gG7YeR8*EyFOCHA39dvYV7VUO3OaT*H!(*D9SdoCZQ zj^tcD>$uPn^R%=ky2p&+;=2_FArwL})J0CB*>aaAP=P*PYxo|LDI6A$gY91#HejHU@u%NN~xD0EoqtKkh?vC%!I{UD%j#nFSi!c#Ry$S6FX9Qmt zR0b@>YOkFaAxg4o1(5llZaw_W&!L?- zXGgOUe1|mnxtDR#t8|0(g@q=;NVQo8MJ3koz>=#RFCs`n+WIbHHSM~^gf|*O61!#) z5}>HRn+((%rSENBk+Kkh-0=D5o1IRuXqC}OFCzMV21PouPPu>bTuxFUDGq_*eElkY zYD-7Fhy^_A%-U_nMMu!UzcY?F#YInps=a#Sk~2 zxsgsLtv&Q$RQGO1KAgfj!=CO!O*YuL+zB;mG>-C%*Fxp0(0IQ%N|H*tKcqJFhQM8f zyE_|HpFYc(9ZXo0b@eZp+v3C%F^6Yb)lRh8S9{VYImDPIZ!fKUmhy<*PgRc-g0wj-?x(bPpQl(7)_9qyLbSbR=oAKK44S@4;oLm{rm zM1=Ng3NR@icaDHk8Vu{dY*TQt^F2lADZ7X4)yQ5cwUDrw0=K(aWjuyy`-Z{cciVKP zw`&!dN1L{i_0b$}H!lNQe}eI07+Hr$_C{z~i~}|MO$)#5IC66{Lfe2q)@9~?mWQ;S zS*`bM@z1H%)lP~96VHvN;|6(Q#70)zvp`)?;<00E)HZf|Nuz1SNmjj6!|1x-r5F1r z3vBOhcSHhE#Eq_=5W^o+>^hKzmBoPU^DR-7)w^5=SAF=7$epnmGW!D#Y^dR>Gc3vf83$yHQg0kl(sEO=PW=-0G?5~2_pK!-BhNc z$){@!y>2Svn8r9VLz;LGJlt!0Ae&Fb`{*^2L{-6s8LB1OAp;4r5v zjD9*v0Th%jARt*5#iFPd(q{CRw!>98T|YNcB@7Nk1U3(wqd`vSz=haq0iNaH%o?J; z4cVZ#ohaJ3vyyW3|3WSwkLIW;%QH&C24PQ^5m^|mZ8SMGG(I3vonN-{*m)!qnDqzzGJ zOa{dMmNe>dgS3LLUTy5o-@TG=o6VBbs=+4YtY#${kRD6M&37t~%<^XS^J3~@fFLR( zRZ5@u z79~Z+TIxs?rU%%@0B9U-Wd0w-VF*ZqQb?+nYZ#B_etCE<1dmZ3RQk0V?aNfYto6+$ zjt=EJWkB{nw@VkV*EQs`%0pe`y0gkcoRoPw)l*PQU1j#Q(g>!#H6zf)ZL?$U{NzfF zo)=wMZ}a-65=nDVXxcoDrwrL%WIvZ>ZD*dk~_jcwX&ws5`*nziRj zZN~(z4ZhBV%Ph&SmPRWiuuVyY7R(8X>7JD;1^CtLJ{TRhspMBdubs6jkR*NoxDIS1 z!5<<&qc&+&UNUfSQj(kG4hpb(5H{2WO$}K=Q|{Rsm{v3OmNHSqLC$~jjk2Gk6 zus|t(?tXoM%Ez9+@Ooy0$lbh+l7Bn|38b{8MLYF!klJ+t{MAnVIfE$Id)RcES?e_I z0nGNti5gg{KM*0Y9h^uDLC3OiGZ`fS3Fm1vyz$bsKFhe=M7yWJ?Y29e>3l9sPZ|a% zh-55*(}&up`2!$|Z6WylyaP)8Y>5`P>rNrx9Ll*F({y5I*8+V2;nXVli zFQuhej#wIw!M(SRW+wAKs$ERxP8p*P0~H45$JaP!8Y_ zJzln16Z4WGW`+b?$>y)azFr-vTgG-cpY;lD%~-%3?KoR)RY6QTu2CBuA;IePq7>}T zGAE{gV~u)KtD@}M_CX$~(n@<7Bm(Yo80}`$WA@D?WJm@EkWW&L-)YtN*o$Z++=29N;jR_VcJjdfUHKii#GvyB266m{u@%_>Yq!jpvu7~@fn1#%zIC$_OmmrjKzqRHy44xRftmIoxkfLY z`+Q7bS~>;Q;&wUjzfUio^f67btO9G3ry@wQtW~SBtzZrNTN^icCMHn!kLjaRDNW;? z=Fv?nkG96So(_qNy*4w!6CJC0s#^Up!QO|F_0bUN(gyYKgVw-}%dE;CZPrwa%8o6{ zYYqQ54V7>xpzDQBj;F2OZlN2ASwnzZ{E@xfrkfkII)!an@Doh?Z3D4I=Hr?^$*p?4O*>nw?6yHa0@6DLL$6xolRcSC z#?xW6)m&0EMzyZ2)IaBmI#7%?Jjoo)eNnQw*j*Y3)cD9s&^)CoDAQx`yQW7kS)vM8 z0Ql#soMw{Ny026Q)8nZcjWNXCh5VzJ4S6Y5xufZ*>~sbJnQvmsJA;1tm-YJ0J%5Pd z@Z5um3&E=-sN%TCB)Trlj-Us)Vj3tBY(P+mMZza^`fPvS_npT@ORmLJbs`yCBPAF$ z!`$>^?;1^Nh;R^PK_x3S#eHiRwF}O`hz_-P>Dcx94REnMhM(8DGQqQ8wzmfTnB!DnFF?@kbi2-MIFm(e;5rLt*)#lhG3ce=4825DAa}fX>{Dx|ujn$T9wv~zS4Ch)r zC|QhYJ5-FcsHG1uGWM_rP)Poq*1Vy9Uxr2vKeSX|7!MG=+3aA&n`dPM5a(!l3WtDy z)I`uz^oEGwz1|>F;qY>!f3F*yrd0#mT>IoMZ_Rm227+oZU~CkH%+{mmf0E*W0fs(-P-7jm;$7OTQqT{E_cos zr~p9fax@qmo(8+R4Uq2Nq>^mA)}$dO^WLeaI1bk6kv%z`eNIrn)3tZGjU5KHWE}%ZnB+|yLt!`5v^@!SOF)0yw#(ExP z=V-0_{T^Uly>PvTIs$`Y#CnpW#7J!%pxn{AuQ0~YV8pjh)CjYq&vTxRR21i1l0^=I zozGY4y=pa7YC?@Jt5sgT?sql{z1JJ@z=nRbN|!aqxVl3;ARslJ(_pc7#>AY%Hfx|y zklkQZA{zo%=Z?`jtWbxUnsP-N4stxX))oYV-m?-c$eGdI_e!?NBqCB{fAaA$7VWB9 zO>xldm5mm zD^#ub2b0SFz?K=}lOm6Kh+%me%)AsgpB79G2knK2TcZuIWsG*Vc&J3rl9uZlWXV+0 zL1qSaG;24r6OV7z8*O^uvIK{dR7iIz32UQLCW1$4v$uHCF+ zKb;Z1Z9F;)j&(b;Ok0~Dmh0i^+QaENK-c!h7!e%vVs0~+b&Urr=PZgCZ;?YEL^XS5 zW|2EEvl23Nz>M45bcfdf+}eF(B?XufI5ScWgaaNVLFdQC-BlRQ^!~6 z@m}ybw`XSYZ@A{wy31Xl5A}%8{DzF-oGEQ?(1-BunsqKya#}RjXlK5cG1#6O779cB z%=%zerA(nh-{n_5Y%vY~lgL0kZkn!t%n&%7-RQ@;%!y8JBOR<;YrOYHjfkm|6Zx}Y z!|`c6YPr_fe6W(0tF*OQKQ#pXizjEbdL^@ny0aTdtmDtOT1x{08NtSF+QaKvH9A_I z=RpOxgyi^`=uLEng?1Qz9LXSSnA5#KPgLo^H1$J!VH&s@3cgB;xlfdAibpKb#q`Bi zebJ_)-v|P2f1};1l;tJIH@qkD^JsOK}92 z(JbBhQ1pD>des#rFrVLJb^}cs$LrW}8bsWNw#?;fbbrEab0PzU_U7W(R_kAtVrCX5 zZ-Bkv{*4-ipG91{S-+%Tz592KYuAho0WcQYf7xsdHVxcG*TO7!8dyG7 z(53C%nQ$c8#&MDMBAx~aS!59SraR6tQ*3IDmfMl3ZBWvzzqD!lH#}^jL)Uldu=V=a z_+TOdkY04Vrvg)zO59i}AEuULN((p(qNa9Ceg&V^;)kwpLdKL^=J20CFVY@Qc5PHn8xM@%f|G zHK>4+-i{XihG>$7pUwuIrfSoPrOFAoU)tkX?bZruvD+A5!0vK&PS?%WNTBx=CUqIP z&F>S*p=vUxUhje5GM@H5j#^+QcYPqu_%SI z>jTY_lN~%b*_4|cQhuw59 zWnfrxyYgAdaxFtb%+Ufst^mq5=^pb3ws>&ZiMABni`1a6G0}9OH{sgcWDSjjz|{sh zIo<~rGk>aXoUYxnWz2Mfsm#lG(+CFz1$xd@Iy^0Om*QyJymhMn;(A7a76n8^J~0-#*38fkrp?x4ReH{& z57BQUYt*UB_^s{UX*USJwwtYmJKb!?!W5qZhV=s*7hohi=oRub#PU7i<9V#KPzk4qWD=*pAxtpJi z*K!|61i-DVa-CPAPX)`seB+XTkzt8sc7)OB=)SB{bxcsA%>t1<=k8Ho%hM%gR46bo zw7pzY=AO0@2Xi(NM9Pvl=MvGf-Go8$HY>z}=Oy28a<(gD&Uc&?re~UyT)QxF!{vdK zGFiRW+H8kv6;|qj8vTcJv209eP+l*x%3{&1hj29O`NEo@sxF(%(LQ6h1he-8Nmfqf87z$%?*d4bRxD=MBp_xI;_? zdaEI8U6O&EH$8xU{OFJ-mjYt^|SjN(KR9;Z3L%7`A1) z0K+|9ORUWZd#=fDQ0{C!Y8X7+-qV#1)J%4bL*JxZoRm@DVOU#pqTcpy8x8IC@aB%g2=}KrKsk`O1*>W#4F^h3}!|aCI2ectfJ(5_Mq|ghC48F zWVyaJQ`MdV010z&weG7FZC;p;G}^FmN5jHg)@CI`h@%z~j@t%4(5_Oe-1_zEG=K>h z1Gp+m&T`{K@@TVI5-CWgnLEM;2YWbyTvL=^DAq+jzFdc%29`&gbdbZ|^pc$&A+nq1 z=`uJHo=sfRpf#o?@6o2;y7T#8a0f``JHwcR0{8cCnx{o@KZ^BF2mTLhK)h!Mvz^X+peX`YwOwjiANAnF#>Jl` zBXXzeRC}iBn*Rkyh!}fikj&q&QK_vt^k(vs?Arr|PlYrtZi^Y0;Fe=8PSr(W4N?9B zXX@)j?5*whxi_L%*jW%Gv6_Mo?MN3XnhNG6{m6+>NH{3ywL4VkP*~jb<@mSL6K3UI zGF>33TT|+_wf7|7%|JK~4c(b#$}aSzMKTcpj0ak@4(~X-cZa$QU#Vx+Er2YVg*Xn( zO@>&~(AC(EwA!f3T(xcGIv@jAy~Sv0){Lvyi!Pw|Y-`k~psW<+KpC~Swauah&e_jd zGi)e0=W@qUr)MB%F)aZzobz{^^3`c-rZjx6%cttQ~rwPRmq5lZ#p4dZi(t8OAojQpon_F1K#L z2i#hw2T##%23>>;O58~5-8njDw(hG6j{|9(W;w+5E9?VoQhqhoe84lI0sd$e#Kj+z zK^ojEDApy`Kga-q{WSniYrlU{*f?Fc&CoKMLKxqs+HbuB`$*=Rng|ji|DcG!8R(*X z#<(eBL8b=#8}1(7tQK2}TvKQ+Zqudh(QcX`z>ZzwAabywHWNfLAw!;SiqY$Z5x4oc zRQpos;rhh1IAxeKm<_hzSS~z*tKAm(wHh_mhuM1DkY;FajNPc_UWl@$ADrox9G!7x zmw6fJG(4kc`_$NiCH6)*opdN3r=y75l&Z}L{DSGqn;BD(YBPn5PI#j#ddrt^5H-4= zOi;b9H@NCC=ovW#o%~Pl3D#ipCLnC1ClgNe;CE)H* za*NH{p4uW}T!+OLy~cS>)$^v#_QR3PRz)^{kx}2h)w#U?_2FgC$}RUb0?iZPt$6Dlj)AnECn1G`lLSXj|{9Qu{@$0HgCotG=DJAov;k^I5bq zZd@Pef=RM0hCQjsPexGEK2Cp5GgGCy!g?X>At8|b1o_$oK12Zuu z(~*2Na|as#hM~oUO?t`^zmM7@wswBXobjIYv*~03n9h6~aTFT6aXGtDszavg*QZ3P zhz`kM;FN^VCLp@Ms?rT6v9JkunuoTzL2st$*Fw8&mELRC;wGKis(-cV@^&rQpmL8c zx@NP+8~;xReitR{Fk3LGN_B3T#+hbzw)2(1T|AML9SUa#|CD++oRK`H9Z|cQLT+;F zz15?)^@s#%?sQ1~j|`}C()sVm0|^L&2ik66diHVn4ggc=Q0J$GkwziM%@De%(4+_B zRQE>sQZr0ErCW-g|3WhY{6;@~m2V|%>L|@_k-2{u+L3oT{EhUXb(Xf}w!NudX z*EpTxh`|hkC*`7=5={%!iGek(TquXR?s6&IvTA)=slT}wHyb#lQAazOxW-0@NCGJv zQV-1?TCrYRi;}mBBYn(Q4Le)I0Q{G0=vocf6it0=i;^pgk|jmS0uT#mVWw`hG6yOU zpeP~ZMlgi^N933Xv0hh?*JR_6fIu^K2bgfKs;UJ%yS7FZ_4;{Zz=JOtq05vI3o38A zs4HHU4v-5gb&NUEB$9G?@p6rF9PfCu!AmQHR`gD@=o1U&X-1nCc0_2a5b2$%N+;CD zvnqZUgkb*9)zF^|V5lZT1tH}9ys#gmmaBEJ=S-oHOP6b!ka2uvP{ z!tqhpMhu)mq7d5%)cn0tOXfvM$etwWD4t!aS+}YGLvDE9syt#%@2Ni;`&}j5G#c3( z9OTbq1_gdDK3law3SY73MmlvNA=ePneGe^6*C1C$PPrrk-n(@phje62*u%|m>X1NYrrMwKx z9M+;z6MlLtgJ|E14NZ~EQX?7S18LF$8}UJYGR4Te!aCt9Fubi*nhe4dAMQy;2w zf3lnKX@h=+Hn>weaS9J=&_mkI$;Vm?((l$rlUtP1D9ib9p57HEG9?3Z9NQNqPxy%# z?ldH_pe(|Y3oQUcW<+&lUzds;`qPfPPVtzDX|=kU2wuGoY!o^K#MOInsWiy#B0Na zTr*SS&dm~GS*0_f=Ch1BTOEjCg%eyYj)e54pWqTaMc}tdUs6QVs!uYUxzP!Z65=8F zkF2djyLD;udNmnPl^ESs3{GlulRYi5myg$3_K#8)L9I3FLr#|;@41{mst6)M3U4^I z|5WJ_ukBdJ_Xv1?&oE3y>~1ZKH&QmZ|JbAZ>A*QFi2U4$lj?lo$}J&Ue) zRJ_!CwahoD224If-(&QcU6m8&YVd4*Zpk^NQUggnIQ!GW)ZHao+R$gXc?Ps*KHg(( zM|qi3%ZwQDl@GH+Ue5A^45KQ-J|peaB3voBZD8+j|$Ef(zF zbBqL{@lT=4Fi*Usr*?^i(O9QbS>dz2U@X(U*q{Z*LQgm8T6655Y|}UkST3BUPCf0# z>`umnD`p_X{o0)&#TaOiA&`bHB6B0DG_6weu=0dK9*RU#3WsVP(mAgvfoxOYL6Ex> zxF`)C;LgSyjG`#8WrrKH$%9`X_mw0+)j?^119X3Lg$p_%<^CPttXDmA_WV}OYSWYr zn$@9~x`LSW0S_vUp=rN`q+OIe>+&d=E2sQ=0(cXV*vAdCBr< zv7Te=w#g5g)G8VjjP{N_#UMGl^i0#vPnptqv;9_SQL4d+IyqV zz+w~;PF$PL?$GF*gc+BQO|wcT(BWJ2FwsTp7LvIi&(z(SSpM9wWFizGI{wu~(KNhM zD!57GGE=x$&p!+-aSrR}E&4W~=s68V8nb}Uc^mXUC_SP~NTY2Z&e1-($x#d!@CA9D zXivtGeyA))jgZxv;8@eRnR>B8+gMLVlP2WST-nooh%RkI9rIb)Oa57*jgCbVcLE?C z2lj5&rw;hcB*P8^*2)ChPcJ}XMC0$6hg+)q%mLnL4&k|OLHu53d`sqPbCu4n){WVt zH`xRfoLQbF~MFpntZEyWQECWb&EjrgJHdeC9_w>C?&8|Z>43H*dJF_J>IWuj< zadglcOEaAh0RtE2466$;Qoq?6SY;9&z6@3vK4F6{O1(v!!W0br$1cNJj<}j4T2*bF$5A8 zcSdDpoj2z|2L_*{(PgtaA%+S&x9A1BoMmV9eOUdDy`%;a#InW$oN_Z*9>#i9QSy|% z`-$8mlt!5rdg2?s&PrXaGqNJ9j+q-pLz}3}v*&eBjedpnHflRpWsd1{aH#kD9WA`f zrf^iNE-)b$XP;(780gXtjlhW`5S@CvhA}(m$E?im(LwDiahIxt<}*&;k?yfkxgL3!8>NF-O-PqRqF8? zbvlKC6dhy!Oi>0b^L%!o?zF@^5)g)$r?*7Qh<8&klc^m*@3%IpAE7D}NNxbhI9vwq zZOKh`V6Wgn+9}vJ(IIY+>OeW;oxPj|%?7)7AH5^$?TLe|mN`0faHC#m2$#GUSw(=t zxBkaQ4wIU6moFV05}ZUhe!FeZVJW`yw5%OWX~q#i8Ny~EQiLp#WFbghWm_- zAYzl^s4;VOh$DD+R_mI|m}-5ABQ{s|t;Ej~VQ{KHTc!75w6+R(kmj4ll8i(1Fuc`d zCmey1emV)S6-7y9O875}0wv-_L(NAWkZnZ^gTrR)x+*nR>ltST|A7zUgx8bK(#BRS z*H3*$$T0UEkO|a$vsEXx>0QTo6JIFcW$t<{+^hp~V$9Pqe*F_km5$akg<4|)#?XJ) zwrO#h@@VR1HF%gUa$4StPDYITTLa5wB|KYk|oJ ztc&3DWt}|(&2#V^*K23!_j$>@+~makg!$2%U=ISnF|u=`J;h;_I_`#ub=P79o0V_+ z@gy!IP5dBNTdsB+Cn|0TR`ZMWr7W>0MeD^;ZW=Js3~lhztQu9-3z(OWD?z_{s8BZd zIjvfsj(6nBcj$QA{SHDyMaivjco?wGa4wJ%FwBw(W*7=SB&WmYyL1Hn1lY@Df2QgX z$FK1BNt2PYD0L=lOpRW4T$(DUk_OG9A!mzbK~HLrZc!Z2P(Sg!@fuYgwf2-~l>lIh zRazK|uBbRl;7cmB*-xLui>wIp%+e-3wo(Ta$MCp1diYmr!MvDWdh+`C^&KtVxZbt= zD|=F*i&+nI#42M*9B^fFT0GLMPiQ#l(l8*|qArbHuiJ}~Q69Ela*F6$ylk{`3YCMk zbzkc(+r|6hJC8aeaN|!g(@f)qt^Iy*pO*UUUUc*?snnzMR9Rz!!O9rci54xH6D}&G zh5GU8=MR7Aio%XceFBTVUQ2NQn`3_lwnoT{Rk6eWDoP5+E8m}o>$Rp@Bh7%K7H=d{ z!Ix9u)}g)Di&KN*)!(1|n0w?+yJVh@t=GONu~zNaq2=qv$zJ5o9D!_C2cN2LUW6TZ z^x?!RaW0CIYvB?AveQd1UbR0AmN-!9bT`an>8lulTfl=>x-g9 z9T@=jPX^e}(<^8X&K;Sd0pNPI6(xh5=-d$}m-H^b3-gN2K>&ckFzCGObK!0|d&rcE z^dhf8AQ@&iO#^OM!Sitoc-9OeEk()G2n>Upi=)}*H0&ceuQrtzC1Z<2iux7Dbiakg z@kacGj}iIYkr*S`o>*wP?&L)Ht)isji8#lx1QlyUol9NO<?(P@WCn02>1ev5`Q_p%0L z*razb5Qc>h8lWD6NhLbbseK8Z`SuYG9X?)9*yoxL7R?flq@m%-knVh&?^zi=aISH9 z2QeV(SU*lDmj+?rEdL@&+kD#*mI{z&smId4v|8J;b-rD%Dg*mkFEYRqw#;6`wkBNz zLDZRI0<8?UokjKs9c~o8ol|ve^wwnig!T!ABn~0o?wIgy6SQW$a>i*Aee);@L8H!yG~|SmT7ml3r4GdIy=x~ zj(y?5&V#H$8~aOYec8Es2-sU?p-b~kgdR3K=C+qWA!|^RZ`qYt5GxZzizq~aKXx)3 zZW`xE3=^Is3bswV*ab+L^IS~Cl36GB&5jg*uF0ZFv!*pgZ7cypF402YYf5G)R5_2t zE(@cq0I>uI;+}Io0SU81&bh5TeF%R?X?R39-8+Xeq>}6{P4kM9yJ!M~>PB(--f{J~ zAR~O~xO)7BpHDGk6EpWXh4wGSi(l+JMUe~l==^4_qamJW(42r;D@tDX7cN5B*u$da zKW7ni<2!u zHa?1Peh%}>Bx_7+QF3Z=Bny@^|J~~4P863Xp)Wv*6~~85iiKp7l3*S>Pbsa`#Ep^M z`E^mUtJM)NKG}P6YEiPmPhK8wVpOm~GizKu=iY;Pe`Pa($l|?T^9ALGV#}E0=XS^*edzQ&9?8Zr~ z+!IL0`!AeP1w;<7a5KxJSsyRx^$^dw&Wekb|DafBqfrUeOp79~u2Z50e3GLy2Uchh zbM%at`dNqHiUMhju5?w+T0jX^if=WC((AxKA(Q;>X#Xp}KGd7f&-pe! z#@SW*ERfhG{+?ZYM!)s<6cSE%7Vmn0W*~RUOWM&3xZNv!!6VUka=*Cla$Ph%7)r$a zw^`M1qoB1~PgVBBI;b$B_-+E@G6`y}(ji=mrr;r?|6!DY70CgWZqU_P+NA@{N!S*P zKUG(|-VN}XX8pct^T8Jmrc(zEo!azxhdS9weq~(1estIAoFn9F67kkHIP6rJfeiCq znn!3FJJcXnl2T5yj%?BURO+?GT!Vv+FrRUm z9=4T2KY|R){8e)+w89l|Ie?Iw&|`c9`tT8g1})mfQaYktwQ#i3Ag66m16k&&fyB<5 zsUk!!eJ<=5`l!<^3yCtPYigHHDp$$$P?!*$(5NMVc|J^twOJ!uBAkm-9nhtE5;Wx@ z&jppi$Kc(;NRiX5P#zWs{ygv19Fb~vrl0dod?!4yS(Vn$V0Z$v9=XmD5U8vrKLt+f z3i8;L(}NjO>axe7oRQ@!l}4ni!%~D2<5Kg(v21mP2J*G@bUKxlJc293RVvm&{A8XE zpsJDmpQle~_g5~uh8|566vNaU?rjN5z{qW3r3(4scp;^fh7l^{C*Ou0sk$V|EZUZHQ%0Ksm>+IiXzyn)nF4eDR#DwT9- zsym7-ERQMY3#+3KEgE$PxLBy@#-8$uop7cALX~5l{BHOtCU#=ht}W>#Q?5IQ}*L;qexyJWC5l#-+H+XUbRc{S`je_5S=c zpX@9v*q8kCV~e%S!h^{1c|L#?e`=8s$G6_qVW1udxXLqz+x#rPoQyfB@lJcIjqJDp1z*(ARJ@9Xi)`7T>3! zOON;<*b{gx$>Slz_jq$sQRij%_TDZ$EH(;rGl_SH80Jel%eUjXVtwTO8R`95VM*}I2vxvnYM|mi*>Dk&bK||&wtxTz_U&_p5)grut6N=&;QHc zoK7~U$fyG!p-gW~)t^v<<+^veuA8C1&eSH95-4R(kXT-GAED8X=%&uj|F~`1^Kc8H z!7Nwuu=1Y_NYHm^=?9pl|Y zNT%vu&m_X3_#DG$KkGl0+S~1Mj&(k3B^SY#x4_R@Xx|ND58a$5GnXd=Hequ!)y;m6 zDD)_E{$#{MN&}{@usGbbC&%fdQgs}yp~r-?)_tOKPg2fQ4Ln6xovz{Ks-6*s<8N%s zxq1z6w5q3+=BRo4;L_-0pT}6<`sl>f4JQHD`_|}WTk?SB-K&L9hN3uRm3~Dr6fsh8 z0x<6x<-VlEHQHPL6#fNbJb9yXJ3}CU=vSay>C|nS{Hds>;T#Vbt)wunz>RY>;G!N6 zg&CdXgD8pp>Ak_^L8VNFflLv4m(JU&+PvflD$q&pP=rneI+a`unc6kU**VGGg^}*c zP5*L~et(KbOU%*RM1js$!S_@#Umw;*7=g&@Mb~TbtwHq}ahHZZ5M`vf4{Oj`9rBbO zqc#UhKgH}XgleL!d-O*U10ghvyU|UX#4j}lwhv>@_KzG>?*2hv1n#MzYdeGR; z3`Z}e@pNAq71q;k&|NN?sQ2p}u|k{3Ou2;=nW=84$x2>U!f0|B(a@7N>dFr_;A23Vp(+#d~Jf7(gh!_l4IPO13$v?Tc}oy!0uuFdCi3S zC$mVXEFQjt5vVrmJkCVM485AhPXE`*4YFZ_s2fYy@dOR+WuP z+{S4>8X)iv?55`=c5$HIC7KLex-qsW9X?KCEA5(6kQ;-}Qy$vk&Ny_lIn!g@9+b%U z5u`a03Sb${wGoy%gHGmtbYvI_@N!a=e0mcuZ_=l3@3S+n->%0u=sT2Uk?cTtvdYAw zRA&6L1TKDKK@BF)92NR0dGl3LtG_smzoQ=n)otLtJyXt68mqyvj2MRekTIHw%)rC| zA;*Akcst(Mg6u&@&d@e`YR%DtR4ft5tbm9RceH58zWh+Ta`}M#lUZzsn;$moiN2qR#Ykc|y=2?t4ZZBwqA3x27pB^`_Qv701zy z+1ath7%XSuasBwtKV*CUAHLRn#i8XrvlN%`<*e|lx%mGswN@{hvWSwYOY>ru)%STW zui!^_@FG0jzUuEhjBHJ(KFi*VkwE=ZraXN%cZqlvWrp+iWc7N$G%fO5S#m$;Edddt#K@T=Kk-q@gnI`vlX zVzQOg?i$^DSj-hrCbGiXrP&TS2(sgS4+v zN)=ch+#8CdPIlO#rbPqYV(4-o4fgum8}wXf1O!;j)!Dh6I9@L?tifv_HuB|EP4aGB zGhOo>tnOeLb3?bkPF$s0pLhYU)aW7$Ms6>I#l+g?CSB1I@$f0FI>tLNh1g4{P6ed- zx%X$!T|8b-kLy7QBmM5D;NwgeU9c8rFMft!Jil6BIs(6Ujc$Q^P_KWYKl=zk|7NYV z!p8%}s+Rb<=li*5b!eOMAt);qfWM7bsRRBcWn$*S%M|+2|H?zpw|LeG8Wq3tph|r< zPlwj%BR>&FI>lL$V(<=M%W#3FX#0E%bURl>U=?V7f7Q}lTLkW zfpr(HK8XcwyhT+hpJ#aw$LX;Z8k_1d8v?W_I(dGp;QB^lJ)<1FvO)7Z^^NSl;Qe(& z((rhZlr&PGo4f*l(g|)B{{dx0#XXg7RCV=O0T%C-5AHod+v_*d2>$Lp?zcWm=J7OIokT?c*&zu;jD>#%EPoCvPHfwqBTeus~R4V2D3SHUyw`6z~m$18B^E1@Sxb3*VhG$p# zEw+%3snP=A%~wz41oJk#8@11p;0Avqdy85h%3uz~NIRFdCy#4D_6ofc)%yzA$ICnr z`O54q@LqX^?KcTJE(h_syHf0xpUkH##W#D50u~bor^DCbe|sg$Z-G%5k;G|GqE6Ps~cXKV3X4hds4iCwuWsZ~r6(;Vho6Rwn+j{`-6cn=*;Qp6Q8B2yeNr^1BpePEJ3QD17Hlev7wpBK#toPKlUU3bDA?-<%m?&v+N# zj(NTv-F=T^evm7;-NcYNj&UyK7@ZwQ2``0EE!UmtOFbNQBEY2T-dCej(4%73Hm^3` zmwjC(UG&m?w^M8Tyi|!_YH%i9YjTsNzUIV!x#qyCsJnMqY-^^Y=JPIwYt}&-B$d-8uG{ z>_vJ27_Lh8BCmTyS6P0H+bbBYvAm84%B-FkfwlC?k4eQWz4F<8ncaKqeL4`Zx%GSY zk+k`>%E!_6(J+a1`vzU;mrCcJPxGOSaD=DOZ}gnsXt2A?&FCv>?Rt$Vik*5idyA)h z7JGKn^nE6+W%r`6jGFMdg%#UoTdG1myW zdeb*0^{ImawNyDQzPIa?7assPn|}UnJXSY<>#h9#j8kxj1R{Yc!be{f_$}(PgFvEQ zN20v)xZ}PW-I*+*sgNa5=qsLmmR|6WvI~CBS69B16;wx~>i*9iU#>m8?KIg)nQV)_ zZ0toxiz>JRF9s^8NpC?WLMO8L4|G?jUd|TJ9UtSx|1MknNWAX?tu2o6rgeKpe2m+@ zpjapPPok9k@3aZZ!x|o`+!HkLc#S+&xu+)zgp|&DOso?bxMFVi&cOuU9TN()sDYV&ds5_XpozCSM;?z_iiWR|5tgB zXy97)ds<`HXz0tTeLg4$L<@6267B4g*EM9bO8O;3wy9sYzMYnIK|3E4{I2UJ>VxC8 z;B4h0!$d+pQuwO;gk_%=fn20L=LgYnufA4$T&;#W4P2z59Q`X1Jjvy_`IT+~UkUDo z`&pDscV>;AqWAk)S5MW+<@#u*s!$HTM8xEus}e!c=PG;LL!wm$ymGy2vi@2xr*JO4 zKC5(IvsSH*7zG{b2DU_Ng>pAuORZ$vhMroF-*<_-U$z+j*|jmwgxMFUi~of zG;>}Tv*|gkeqThXSHU*zZO-r+`N@U5B?I;ih7V54PpBF9WjV*Yx;s5ItfL@!lik}& zV+!wWVjkK8kAbMIjM*7fh+sHCBOfhM&R45|PhSzPau@A zSjpy|N-JWLG?Yv7gB%!;)$5sD7Qx<>);M@ZaN#cSO71jxzR2v{HO5GXQ!-Pp#f^G; zi5eRMu?K#}BB!V9Vvsk8@je>eMc61B7NzIQE=}pu@m@w+pq=X(l#f$kG%}7dms-oT zz~7N58Z+)v-ZS&@su{vCk?t%Z9`1#a_snypHZ*|ckmy{bOpfV%qc!TsQ|*`QK7?zQD?C9wJidRdIn!CuJ zcvewWX6~QyV&LSzU?S718m+{?@Lec$$qa#}q28ZJmzzyp-W-d0+m&c6W>-^;Vrsl+ zQ4QFnTE}?zG8$oQ$Ej{RFwt0@h1K}f*1D$?Qx9N>UGHrf<%A$*)MvOI=pi-wD`9Bs z*ozu?HpxMH;igh#Lq<9CBBE^;q0>rL zSEgOeq8wr-e4rq&gl2Ikx^>T!HDWq5VTX!Fjc@o$iPH$*tlOG3zz0NjU{R}HY17`8 z;jykH9ZEk&ZwSwY9vshQG6hP&9k2oU$GY7y8IxM?16k3F-3jN$O8uusZ`ErUqjt4+|fZY*@SFlXz>4I z>uun>EX()*;~oPyMMcC!d}tskQ)^iC)#a2ivCuZh)Ts+oG;3DEW>6!a_<+nY9}?8^ zF$F`@bWIKNX$t6Un1%N6%?H%sC~=uivqV(j|2~g<_gdfo@AdM<3&&@l`?{~|yw3AD zkMrq(T3u16@7q|X2=H~<%eV?)k#7yzSqBohSr~pUQp!sDbL>-B!eHsIW*ds^M8M0P z!zeeXRIU%DKu)&jILcGEm*UN=S^6laDKu$%)XD zC-mF?Nl`(B7k462f!J1BO>oAS3r^n7Qm5*Dp8Dx)!TMEa(GAwrE85jn$l=A^T1FKcSQHdmoScJk=Z+A~fdufa{2`46y#L$l^z;nP{C0oB zJgLPP;*WE>)UEqbp38|IS`@hg|H^bY2%ogWZiGKWHFKFgy*YY3EJCs8p@cO^#-MP_1pXI@7ZQP@93lX6vGicp327!M*!5>pAL(2M8apAdv&Gh}rQu*#Ik=dF}(FV#yx!3r&LFTM>%;N8sMGzr532uRIz zD6`#wVSbZ7G2UL?qN@ktgbAoeo^IJI=+tE{y?A-`lfbw%aJCRbzDZ0gY7Bt%J9On<7i|0mAkX% zLE3leAe;NJ_3Hy-MLH+}zJEt|8Z<+wBjb?ClXX&d2iIrD>x4R;SL0wxb`Afhv46`8ql-G>$ZQh|Pc1Ae$oM|A1W&qUt)&n*?{CeVzfpjRh3BYhf~nKrEH zl{_v#=`2oOJ5VDI)?u!qLSOx`q~AqKx$9rnYDS%&s?pfYq;}h$))C@w&}mKD>is#L z&ToTG=SYjk3y%a>fQbi<|J}S(T2TBjBRIQtRiAdwlmnYXP=@Qq0KpNZdfA@ZVhd_x zm9|ty%Pi=@);fKoMx>{v%Co0lPdR(Ju|Y>Q=?Lpg?h9r;z(%Gs(=Hs_oZ)aT*VFR? zo_RXLPet&AIQy29iG49;gX)sR76jDs`{JZKo2$q&NSxSc1rAU=!h*Vtlk%)s3_6#E z;%+)HfNU#0x$MO3n2L}V7g=+Emr>B+4XJUlaN2u$4N;_CO>tMZ_lG7GE^zydcZ1x> z3)u~C#o?s#PKyM#3EUdj+7sjcYo<4rd%Dy4h871K=hXebD%_El``s%t=Ve^x@+cAy zkXYE-i%63;nfVo32@Q)zMwn3&P-%#rH$KLtm6JeHswY?veo#eZ3XQMT`@{ijv@Vm7 zKhip*QEzyX@8)T z`w)Nh^~q6KSD~6JeTjJ%+#R1tQH?6oeK2W%tn)%J9M!H5;TQ>O0Cm;AHhKkJ@@M`>eJ|~PAzScxyW@r zX!34Kby5IqnVjdVLEqr>oVF2y7}$Md#i@&;8PaArS;oS}TBj@Y1J**xLTTpk?T*WYI zW8^dMjkJ)o<{7?=sx{j}Tkf30W098G`i+(C=sEh8Dfb0#&M-V{vd8kk7&%W&h~=${ zqQbwiHkyd`#j{>XLa8RC36U>RoJ=Ek$f?44P~TYLKCMNH?4lO%8DzCO21b;l?fvw9 z>Z6!?3jOzdU+7L;)Dn*)6^ClmVST!VZpmn*>iF7)Ok?kk(_aFB!To2HVYO;%w6tCu zo#ld)*a<9YkMgmZt|uZ@%|%r+*;QT{%ibGjL7peX-POD-sm@gzS{(_ToW;Bb-N;#6|3D^?9YGm2@4~)q z>vEd?&;&Q3$f)?1qqWR3{7jXOs@27w0CGj9SNjYA_$n;UYFnITS&KY0*n37GsPqRr z_3N&Ho%1&7Q1k|h)Xm3Gp{ae@^^-`*`H`b>^Dm_xE+10`SK#G=8ex6|u z3Q_<+@BBCUlM-%kr*Ywx`lLhMwk&A~veZ21b^~An#GkbW>&)QC)fxPlY2AgL7chQM)mhDnNEiFQWMck2T#xI-1&aFkfyR$YD))KGgy z4;L9g&Q<+2aFp(R76!!Wr?{?GstDDR%(5I^ueom1!4=qMje38p*0t!K)Xw_d5pu5T zMeQoGQXS{&8=S-&{PZOxmh|>}h()gAqM*$#MZVCrBSO}|-z4ptRy4=vDZ<}~|-iBt} z&6>i~Q%A{m{}30JZuicVStL?ze;(N{_>bLML-O|4SOez*DjQv#oSrgQW!jbGLpa7N z-RFJyvc(n1y2lK?RHHu`hg@r&{}$;tbJX3S<*cVUitC5Ah?^T*k>OR&=RYv%?MTy32+S<) z?8W3Y#!x~Ir6EqmaGK&E#ro{3CRcftNeXSO{h5qR5q+YBSh4z#}e?&{JCf(pB z-bssok?su9(u_L($OrUi=egm`uJ)cxa40zz^S<)`)$isbCockuD6n-%mu39gd$P=) zJ}J9XeM}hTDHhlN$-oJtx6^WlLz{FqEu%fuY2bk35}AAduuWsTbRQX^_z{c&HOYXk zAS^opaOhh@z0&prl^d^|_o2=bOB;&U{OEn_G`&Vw8IY`Y$GqY>`gMcmIl2Tx*o~zd zkPXA3qa9{_4f!;muzHieN&^#bKCK#|0k`>OCuWcs0oZDp{Fa{xzeL*Pw=4s22%mLV z&zl>R+Vq8vo4{%O%Z<4JA%O-{)!~TA&pNbsoAv{NkR%Bo_?t-ee9K2L(jH}?^!J@- z=;D2>4Df8KL@`bv;~MLXP0#BKLN5nIVf_s3s8Qo= z9ayjO#sD7Ueh|%3NkL%&&zp+b86T%z-|CEwnm~gT92tMf!xY<=#hA`Km$Yo@4akvK z5gE(VYBaK5r!+)w&xtD?!=*)@SKh$yR*HHnpa^s~N*)rs(f?Y`H*|-3>@3ywARl+t zD1a$)@5eRnGe%MNm}U`wVMF$4*YDdjl$Yv?x}Fgn8DkwhY<)Dk9n0&^gpk-{nM*rI zeGPhfZe;j_PRU`zdY1FMs{*cc4@;)FCmw?}Ii|BA49QIPE&F#>TytoXu`p>|^Fi69 zfR?X8WBQKoZ}#lS6{<+BGj6gk*T)YD1N~5^eh-_+k!{dZujr}u;--HU@W8X^Yz zFD#4>KPi7QfMF1RTIHwl(C)e*&{k&{1~UzYdCaOf&4^<*J!xHitpdYLKP{SRF~-s?w6G|<(O}~^Ak_<-J?chzTHoqmDX#y z>0B->j_3DtS$Td-HfHOlew|Zki`-)-xu;8$)~O0ffGXJ>Z8;erJ{14LG#XHrWt_+W z-@;@UkJ4D3nq~v1d6WAaO(PEvSs6ZOWq5sE^c>FB=$h%57?lMz9w(F1Kw7|(o{((cWX$)dm^1<+kiI7%$k_%?BN)Z;ZzkJr@{=AL?w^7|(s{`G%@L!pAhP7C09Cby5 z#^QxJr}t^}+kkB{>79cycqEecQ^s#%rdb?5K^ zbvKw)4YsImO+0}^w^#n zv6F5}OC)IW=EW>zoMNRm;Tqc)d(fyd&%1F(qn2A#zCuK=Rgb5m^bl7+ourxqM*(1y zD7m`1OAoHozs=kQj6Udg{Sq@P}FvuaQ)$WRh#HPLYmOGzeGvey&a{#*CyMe?$ z;AH@|3+AY^LC-en0q%9Hy3A$_9Rwukj|8)Xx7%x7TDD2w@g@)ip_+pTNF_~CP^9Su zhYH;ftM93-V~86XOG5%D-v3kUwUf2hl?^(KJU};{-r+cA=&nqE;#nY>kF`g3^w()_ z?ES~5I1)SZj`DyjSau2xwnknD!hp}kDZq2Crn06W5bRHm4|~na63pdFKGC2}b9H2s zl3#_5&aDcjH1Aav(2A``>$V4CnMz6|S%qbKxKv}%KSVc-#-FU#Qtv*sj+29-8%$rozm)a*r z6||nFf7ok>gY)ZxrCByB9953v+_}*|=VeqJCFFN$c&GX(bfSWConCxhLxv{wKU|0p z$T$P+HBbQ!?0Y(qYvK#?fxA)dmZ^(pY-H~}Uj;eU8vkr?WeS{UIs_c8{_hUm{*ZQY zGEx&PA5DNg@ABwPde%iB3;(W_Ccyin1D^=r{KUc}f`(<061IhT++M~CH=QGaho%|8 zun9A?QwDy>Fx=i4p_6fzI#N|(3<{d{wu47_9FZ&KTNC+3ZIP&!4syDLJL^f83sbHj z!y7MW5a=MxinFGdRcM@#shcigv^4RCtaX3UcUZ`|*A)mz=^5Hs@W_Gwo5H2hWes~lo z?ky?;AG&E6xcWA$6wq~ZW3=pejk)$VD^-{|b|Z_ZrWDE+yFZiu5;tN`+x>u}26&C3{um67Znhy0j z83S8(tHW4NqRlq+Hw2!>ZeU^ibX}Ghj7p#6;6B>y?`Es|`|o)%oeryU`*v_5e*N9K zfQ@>}Eu^q$#X0Te_23fzDb?ZE{U^$a%wOclGRY69)+V~9TeX7FK5Lae+}Mn6S2%!0 z&;kZR1PCrNDmwzRQV}DZ4u)v;wxHo-F=xaz++rZ~1@BK$VR>!F(X~Hqra!zRA>{!#m5aiUKjw1#;zyU zim*GVYyi4nP_Gj`ijl>9qezL5#jt zEiEoDN^M^T$!OHZ#bGOMgE6qoq{p6hGLYF~uFsuzXAt9DSIkYG?>!*^ zdS0*c`?St5fIG`28o5hg!r7Do`Ix48cyx0zc}Sg%QqI>DnM2`W(< z1{nK;d5r#5ModZS>Lr9}-gB>*-?-N#t$qpMsI%!^E8YW{Yl9 zg<5GA#ON{0Z8Siep)HQoT#ZJLWjwE4j2w?*S2gG;GT)z2Rzo)T3oP`du3ih+rS)Z#zs5Qyu4M zvDNq&nl+_8+SZ|s$Ag7vM^hdBY8H7YZ3RXBRa$Be=hzuKiPgHr?nslv&`_%#2)mste*yxXpSntlN=%vz^kt`F~zoQxPOR1Hrnjd%z7HsH)DWVX~rX$h^5`TX#vXa)rtxB!N{J^C%cjFoeFoyYY*J^HXX z{Q3oc!MtjHz9wXEjE^HB+i*z~o{+)~gG#+&e!KGA*^;=!1@37-4XsGi57%Enp?s3o zP*YVsOXU>&vO@eL@^-$U%WTNLcwFG64{G&EosOa)G1CCNI~>A>`I?#2aZVov|3AJ| zqn#TA!;{8eP>j?9I7|p*0Pn^>G?q^|* z^6lG3_ly?(5G|b&9cf0l_oUEJ2hh8Ob`7wsUQ*Plu{mVdQ%cj@&(}dcg#&AnrzG7X z#vj;?&#oPu?Xm4A|2x^HHlQKT1~7Ice%Vq$7HIwqF&PtqIl3UyMgT_;tuiw zk0cArdKfXF)|jC)KVPRW`iY08FW9Fa0pzx7q+bo;JkDhPW5#dcn^);{s(~Ez83%z6 zhcipBLyb~oZC!L;lRhE})~bhE^s_;sw*vFn%Uv)#woCuSCJs_`JbkEj%a1a4kw;e< zy>K;F^ZFI~qD8LC^YREBpigc^pUl#itog5R)MY;iSBrOVZBl)+uJxd(&!w6_)A<;n zzdI%AfiA(`q^TV_E;Yy6qw=l&p{_-CMR6x-1q>?q#4>-Ev%=Eg1xICoFCoCB*$89tkc~!TE$z>(h~1E!uE5&H4C`2IqW{H z^74UKP8YCJ50Zc7(i`sAM9(?e;HNXx<4v2_@t)27-BQinI|v@IKA=Vd6ECwuGD z3zNNyljqFKao1Afg%gKN<}MSfg8Uh2{f5Ym^ISG^NDwyY%CjB*e!5ldrVh%}9Y_)T zFZ!!^NGqDvsb>czPLClmpjV@O^!pIC$&dzw*hfc))S+zCEHAJ+RM|{4SslVqe0*S*HYdd-Dk!eF-(&r$UQ;VYUxva@{;!34Q>8 zvFB_-m}9J`5-sYAc*o_Wo0e%&srCYiRqNYzx}-+GrI!f3SZ#&Ty4`biP?K77(G!v= zUOpIXr5-l1K>EiStJKyd#xm~fMk~u1UE}}|=D3Hy5Cn17%5<7bQ^L=q!4=ei+@XAQ z$SN)C)1^3f1~z%LhowarZe5K|ogLTzYYmzO;$jq3OT=7q?K*-SDq8vza3SfG2G>3M z7T2moh1HtS5E!G#Wn4wo5yCF7kC$EJ24(GSx^&yoT0F9n4BD_S?aP6nA3}Tj$CDACBajflHjcJ=qAfuA7+SD!;G8DTv%3JccsC8J< zH$1s?m*mpJA}wt))1xq9M??zH8s99U!2jT8!An?{h>v_dQ)1+nKur*fc^+XVEV4K{ zc+59gt$>%fSqoO!10^uwn$7)J>LJS&w~WZhC9pKW372kt7a;MDifl(lXE~+EeG-HT zp%8QnSOS%fc&D-H#Z~%6+AYC^2~7$1&49!EO95-+%p2^RcIRa+Q{l3l_KMptectmo-_0d)-Wq@ zGT;ii8o&~e-W&>q$)pzTncCJ{`h9Z(Mi|xsEWsJwoMDNpQ!KGwOET)_iL7vvEeb7^ z&S6QT17(qRGN0%qr*^C+RB2jm)JgqhmiDNRe5!@M_2)Po08hgF@qTaO{^-Sl_AByA z&TEh4QV2Bsf>Bockt>zbCI%Rw_w7fk#%GZ-Goa=1_Pz^i^n?w~0rh&+k=7**+Ssfy zP*p9uW8jH*Ma!lZKklte$rF8eJgb11*BiAw^l9fEVMVTI_#<`Slj1fdrFwU=p0Z?e zcPKz$9wq74lZ@r=$|REnn*t=$L91IRE2bH2tM;;6S2cj;951_KrGAjgq)q%!y7bd* zSklp!glKY$kGWNgycm$}n3OWIm49fmmKeOfTB{Yz#H>+!wgw>FpBvR>yMwVK?SD

    Tr1&$g^pYZR!g%s~ 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

    IsX}r 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 z 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

    |CJE$3I3{bL{>L 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

  • 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%0AkIP&*_|3ir22>E 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 P+V{Vz*zBYJ`~Uv|9L?Gy diff --git a/.vs/bts/v17/TestStore/0/000.testlog b/.vs/bts/v17/TestStore/0/000.testlog deleted file mode 100644 index 6bbf10ed81b7163bf1f3b40a26c65804007b7561..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20 XcmY#XEb%NUP7PsYU|>)HVh{iTD<1;I diff --git a/.vs/bts/v17/TestStore/0/testlog.manifest b/.vs/bts/v17/TestStore/0/testlog.manifest deleted file mode 100644 index e92ede29d76aefe079835aeae278da5341f6e15c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24 WcmXR;&-W=QP7PsZKmZO+yIuf90t7Vx diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite deleted file mode 100644 index 352f6d0cbd56a9d979cfd332f981d0316d339e5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118784 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 From c128ed046d847992366d3714e47fe4659eaac307 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 15 Feb 2024 01:08:09 +0100 Subject: [PATCH 007/224] fix: Correct the call of the tabletoperators and wait for the asynchronous result of getVoices(). --- static/js/announcements.js | 63 +++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/static/js/announcements.js b/static/js/announcements.js index 04de96f..538b7aa 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -19,9 +19,10 @@ function announcePreparationMatch(matchSetup) { } function createTeamAnnouncement(matchSetup){ - var teams = createSingleTeam(matchSetup.teams[0])+", gegen "+createSingleTeam(matchSetup.teams[1]); + var teams = createSingleTeam(matchSetup.teams[0].players)+", gegen "+createSingleTeam(matchSetup.teams[1].players); return teams; } + function createTabletOperator(matchSetup) { var tabletOperator = "Tabletbedienung: "; if (matchSetup.teams[1].players[0].state) { @@ -35,13 +36,14 @@ function createTabletOperator(matchSetup) { return tabletOperator; } -function createSingleTeam(teamSetup){ - var team = teamSetup.players[0].name; - if (teamSetup.players.length == 2){ - team = team+" und "+teamSetup.players[1].name +function createSingleTeam(playersSetup){ + var team = playersSetup[0].name; + if (playersSetup.length == 2){ + team = team+" und "+playersSetup[1].name } return team; } + function createRoundAnnouncement(matchSetup){ var round = matchSetup.match_name; if (round == "R16") { @@ -115,34 +117,53 @@ function createEventAnnouncement(matchSetup){ } return eventName; } + function createMatchNumberAnnouncement(matchSetup){ var number = matchSetup.match_num; return "Spiel Nummer " + number + "!"; } + function createFieldAnnouncement(matchSetup){ var court = matchSetup.court_id.split("_")[1]; return "Auf Spielfeld " + court + "!"; } + function createPreparationAnnouncement() { return "In Vorbereitung:"; } + function announce(callArray) { - const voices = window.speechSynthesis.getVoices(); - var voice = null; - for (var i = 0; i < voices.length; i++) { - if (voices[i].voiceURI == "Google Deutsch") { - voice = voices[i]; - break; + // Seems like the getVoices() is an asynchronous function where it is not always guaranteed that you get a + // result immediately. The wait for the result must therefore be handled: + // https://stackoverflow.com/questions/21513706/getting-the-list-of-voices-in-speechsynthesis-web-speech-api + const allVoicesObtained = new Promise(function(resolve, reject) { + let voices = window.speechSynthesis.getVoices(); + if (voices.length !== 0) { + resolve(voices); + } else { + window.speechSynthesis.addEventListener("voiceschanged", function() { + voices = window.speechSynthesis.getVoices(); + resolve(voices); + }); } - } - callArray.forEach(function (part) { - var words = new SpeechSynthesisUtterance(part); - words.lang = "de-DE"; - words.rate = 1.05; - words.pitch = 0.9; - words.volume = 2.0; - words.voice = voice; - window.speechSynthesis.speak(words); }); - + + allVoicesObtained.then(voices => { + var voice = null; + for (var i = 0; i < voices.length; i++) { + if (voices[i].voiceURI == "Google Deutsch") { + voice = voices[i]; + break; + } + } + callArray.forEach(function (part) { + var words = new SpeechSynthesisUtterance(part); + words.lang = "de-DE"; + words.rate = 1.05; + words.pitch = 0.9; + words.volume = 2.0; + words.voice = voice; + window.speechSynthesis.speak(words); + }); + }); } \ No newline at end of file From 9cf13fc0dcd65c6893472e2b2467f294b6d58ee9 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 21 Feb 2024 23:59:05 +0100 Subject: [PATCH 008/224] Handle retiered and disqualified inputs from the Tablet and send the correct winner status to btp --- bts/btp_proto.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bts/btp_proto.js b/bts/btp_proto.js index 4a43078..034b2d3 100644 --- a/bts/btp_proto.js +++ b/bts/btp_proto.js @@ -97,13 +97,23 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud }; }); + let scoreStatus = 0; //Won normally + if( (match.presses.length > 0 && match.presses[match.presses.length - 1].type == "retired") || + (match.presses.length > 1 && match.presses[match.presses.length - 2].type == "retired")) { + scoreStatus = 2; //retired + } + if( (match.presses.length > 0 && match.presses[match.presses.length - 1].type == "disqualified") || + (match.presses.length > 1 && match.presses[match.presses.length - 2].type == "disqualified")) { + scoreStatus = 3; //disqualified + } + const m = { ID: btp_m_id.id, DrawID: btp_m_id.draw, PlanningID: btp_m_id.planning, Sets: sets, Winner: winner, - ScoreStatus: 0, // Won normally (TODO: correctly handle resignations etc.) + ScoreStatus: scoreStatus, Duration: duration_mins, Status: 0, // BTP also sends a boolean ScoreSheetPrinted here From 0bb98065703ffbd05757c749e2776899debce5dd Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sun, 25 Feb 2024 02:40:23 +0100 Subject: [PATCH 009/224] Show better Playerstatus on Admin Screen --- bts/btp_sync.js | 204 ++++++++++++++++++++++++++++++--- bts/http_api.js | 140 ++++++++++++++++++++++ static/css/cmatch.css | 96 +++++++++++++++- static/css/court.css | 23 +++- static/icons/court.svg | 121 +++++++++++++++++++ static/icons/court_history.svg | 121 +++++++++++++++++++ static/icons/no_umpire.svg | 128 +++++++++++++++++++++ static/icons/tablet.svg | 178 ++++++++++++++++++++++++++++ static/icons/umpire.svg | 128 +++++++++++++++++++++ static/js/cmatch.js | 89 ++++++++++---- 10 files changed, 1189 insertions(+), 39 deletions(-) create mode 100644 static/icons/court.svg create mode 100644 static/icons/court_history.svg create mode 100644 static/icons/no_umpire.svg create mode 100644 static/icons/tablet.svg create mode 100644 static/icons/umpire.svg diff --git a/bts/btp_sync.js b/bts/btp_sync.js index e8e260f..736df3c 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -156,10 +156,30 @@ function _craft_team(par) { pres.firstname = ''; } + + if(p.ID && p.ID[0]) { + pres.btp_id = p.ID[0]; + } + if (p.Country && p.Country[0]) { pres.nationality = p.Country[0]; } + if(p.LastTimeOnCourt && p.LastTimeOnCourt[0]) { + let date = new Date(p.LastTimeOnCourt[0].year, + p.LastTimeOnCourt[0].month - 1, + p.LastTimeOnCourt[0].day, + p.LastTimeOnCourt[0].hour, + p.LastTimeOnCourt[0].minute, + p.LastTimeOnCourt[0].second, + p.LastTimeOnCourt[0].ms); + pres.last_time_on_court_ts = date.getTime(); + } + + if(p.CheckedIn && p.CheckedIn.length > 0) { + pres.checked_in = p.CheckedIn[0]; + } + if (p.State) { switch (p.State[0]) { case 'NIS': { @@ -272,6 +292,38 @@ function integrate_matches(app, tkey, btp_state, court_map, callback) { match.setup.tabletoperators_btp_ids = cur_match.setup.tabletoperators_btp_ids; } + if (cur_match.setup.teams[0].players[0].now_playing_on_court) { + match.setup.teams[0].players[0].now_playing_on_court = cur_match.setup.teams[0].players[0].now_playing_on_court; + } + + if (cur_match.setup.teams[0].players.length > 1 && cur_match.setup.teams[0].players[1].now_playing_on_court) { + match.setup.teams[0].players[1].now_playing_on_court = cur_match.setup.teams[0].players[1].now_playing_on_court; + } + + if (cur_match.setup.teams[1].players[0].now_playing_on_court) { + match.setup.teams[1].players[0].now_playing_on_court = cur_match.setup.teams[1].players[0].now_playing_on_court; + } + + if (cur_match.setup.teams[1].players.length > 1 && cur_match.setup.teams[1].players[1].now_playing_on_court) { + match.setup.teams[1].players[1].now_playing_on_court = cur_match.setup.teams[1].players[1].now_playing_on_court; + } + + if (cur_match.setup.teams[0].players[0].now_tablet_on_court) { + match.setup.teams[0].players[0].now_tablet_on_court = cur_match.setup.teams[0].players[0].now_tablet_on_court; + } + + if (cur_match.setup.teams[0].players.length > 1 && cur_match.setup.teams[0].players[1].now_tablet_on_court) { + match.setup.teams[0].players[1].now_tablet_on_court = cur_match.setup.teams[0].players[1].now_tablet_on_court; + } + + if (cur_match.setup.teams[1].players[0].now_tablet_on_court) { + match.setup.teams[1].players[0].now_tablet_on_court = cur_match.setup.teams[1].players[0].now_tablet_on_court; + } + + if (cur_match.setup.teams[1].players.length > 1 && cur_match.setup.teams[1].players[1].now_tablet_on_court) { + match.setup.teams[1].players[1].now_tablet_on_court = cur_match.setup.teams[1].players[1].now_tablet_on_court; + } + if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { // No update required cb(); @@ -456,9 +508,8 @@ async function integrate_now_on_court(app, tkey, callback) { if(!setup.called_timestamp) { setup.called_timestamp = called_timestamp; try{ - const [tabletoperators, tabletoperators_btp_ids] = await get_last_looser_on_court(app, tkey, court_id, setup.umpire_name); + const tabletoperators = await get_last_looser_on_court(app, tkey, court_id, setup.umpire_name); setup.tabletoperators = tabletoperators; - setup.tabletoperators_btp_ids = tabletoperators_btp_ids; } catch (err) { callback(err) } @@ -480,6 +531,15 @@ async function integrate_now_on_court(app, tkey, callback) { admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); admin.notify_change(app, tkey, 'match_called_on_court', match); + + async.waterfall([ wcb => set_player_on_court(app, tkey, match.setup, wcb), + wcb => set_player_on_tablet(app, tkey, match.setup, wcb) + ], function(err) { + if (err) { + callback(err); + } + console.log("No error!") + }); }); }); }); @@ -497,7 +557,6 @@ function get_last_looser_on_court(app, tkey, court_id, umpire_name) { 'end_ts':{$exists: true}}; let tabletoperators = undefined; - let tabletoperators_btp_ids = undefined; app.db.matches.find(match_querry).sort({'end_ts': -1}).limit(1).exec((err, last_match_on_court) => { if (err) { @@ -508,30 +567,143 @@ function get_last_looser_on_court(app, tkey, court_id, umpire_name) { const team = last_match_on_court[0].setup.teams[last_match_on_court[0].btp_winner % 2]; if(team && typeof team.players !== 'undefined') { tabletoperators = []; - tabletoperators_btp_ids = []; team.players.forEach((player) => { tabletoperators.push(player); }); - - tabletoperators_btp_ids = []; - if (last_match_on_court[0].btp_player_ids.length == 2) { - tabletoperators_btp_ids.push( - (last_match_on_court[0].btp_winner == 1 ? last_match_on_court[0].btp_player_ids[1] : last_match_on_court[0].btp_player_ids[0])); - } else if (last_match_on_court[0].btp_player_ids.length == 4){ - tabletoperators_btp_ids.push( - (last_match_on_court[0].btp_winner == 1 ? last_match_on_court[0].btp_player_ids[2] : last_match_on_court[0].btp_player_ids[0])); - tabletoperators_btp_ids.push( - (last_match_on_court[0].btp_winner == 1 ? last_match_on_court[0].btp_player_ids[3] : last_match_on_court[0].btp_player_ids[1])); - } } } - resolve([tabletoperators, tabletoperators_btp_ids]); + resolve(tabletoperators); }); }); } +function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { + + if(!match_on_court_setup.tabletoperators || match_on_court_setup.tabletoperators.length == 0) { + return; + } + + const admin = require('./admin'); // avoid dependency cycle + app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { + if (err) { + callback(err); + } + + async.each(matches, async (match, cb) => { + if(match.setup.now_on_court == false) { + return; + } + + const match_id = match._id; + let tablet_operatorns_btp_ids = [ match_on_court_setup.tabletoperators[0].btp_id]; + + if(match_on_court_setup.tabletoperators.length > 1) { + tablet_operatorns_btp_ids.push(match_on_court_setup.tabletoperators[1].btp_id); + } + + let change = false; + + if (tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + match.setup.teams[0].players[0].now_tablet_on_court = match_on_court_setup.court_id; + change = true; + } + + if (match.setup.teams[0].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + match.setup.teams[0].players[1].now_tablet_on_court = match_on_court_setup.court_id; + change = true; + } + + if (tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + match.setup.teams[1].players[0].now_tablet_on_court = match_on_court_setup.court_id; + change = true; + } + + if (match.setup.teams[1].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + match.setup.teams[1].players[1].now_tablet_on_court = match_on_court_setup.court_id; + change = true; + } + + if (change) { + const setup = match.setup; + const match_q = {_id: match_id}; + app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + if (err) return callback(err); + admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + }); + } + }); + + callback(null); + }); +} + + +function set_player_on_court (app, tkey, match_on_court_setup, callback) { + console.log("Suche Player"); + + const admin = require('./admin'); // avoid dependency cycle + app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { + if (err) { + callback(err); + } + + async.each(matches, async (match, cb) => { + if(match.setup.now_on_court == false) { + return; + } + + const match_id = match._id; + let on_court_btp_ids = [match_on_court_setup.teams[0].players[0].btp_id, + match_on_court_setup.teams[1].players[0].btp_id]; + + if(match_on_court_setup.teams[0].players.length > 1) { + on_court_btp_ids.push(match_on_court_setup.teams[0].players[1].btp_id); + } + + if(match_on_court_setup.teams[1].players.length > 1) { + on_court_btp_ids.push(match_on_court_setup.teams[1].players[1].btp_id); + } + + let change = false; + + if (on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + match.setup.teams[0].players[0].now_playing_on_court = match_on_court_setup.court_id; + change = true; + } + + if (match.setup.teams[0].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + match.setup.teams[0].players[1].now_playing_on_court = match_on_court_setup.court_id; + change = true; + } + + if (on_court_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + match.setup.teams[1].players[0].now_playing_on_court = match_on_court_setup.court_id; + change = true; + } + + if (match.setup.teams[1].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + match.setup.teams[1].players[1].now_playing_on_court = match_on_court_setup.court_id; + change = true; + } + if (change) { + const setup = match.setup; + const match_q = {_id: match_id}; + app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + if (err) return callback(err); + + admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + }); + } + }); + + console.log("vor dem return"); + callback(null); + }); +} + + function fetch(app, tkey, response, callback) { let btp_state; try { diff --git a/bts/http_api.js b/bts/http_api.js index 77b1523..590c82b 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -265,6 +265,22 @@ function score_handler(req, res) { update.btp_winner = (update.team1_won === true) ? 1 : 2; update.btp_needsync = true; } + + if(update.team1_won != undefined && update.team1_won != null) { + async.waterfall([ + cb => remove_player_on_court(req.app, tournament_key, match_id, cb), + cb => remove_tablet_on_court(req.app, tournament_key, match_id, cb) + ], function(err) { + if (err) { + res.json({ + status: 'error', + message: err.message, + }); + return; + } + }); + } + if (req.body.shuttle_count) { update.shuttle_count = req.body.shuttle_count; } @@ -336,6 +352,130 @@ function score_handler(req, res) { }); } +function remove_player_on_court (app, tkey, cur_match_id, callback) { + const admin = require('./admin'); // avoid dependency cycle + app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { + if (err) return callback(err); + + app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { + if (err) { + return callback(err); + } + + async.each(matches, async (match, cb) => { + if(match.setup.now_on_court == false) { + return; + } + + const match_id = match._id; + let remove_btp_ids = [ cur_match.setup.teams[0].players[0].btp_id, + cur_match.setup.teams[1].players[0].btp_id]; + + if(cur_match.setup.teams[0].players.length > 1) { + remove_btp_ids.push(cur_match.setup.teams[0].players[1].btp_id); + } + + if(cur_match.setup.teams[1].players.length > 1) { + remove_btp_ids.push(cur_match.setup.teams[1].players[1].btp_id); + } + + let change = false; + + if (remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + match.setup.teams[0].players[0].now_playing_on_court = false; + change = true; + } + + if (match.setup.teams[0].players.length > 1 && remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + match.setup.teams[0].players[1].now_playing_on_court = false; + change = true; + } + + if (remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + match.setup.teams[1].players[0].now_playing_on_court = false; + change = true; + } + + if (match.setup.teams[1].players.length > 1 && remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + match.setup.teams[1].players[1].now_playing_on_court = false; + change = true; + } + + if (change) { + const setup = match.setup; + const match_q = {_id: match_id}; + app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + if (err) return cb(err); + + admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + }); + } + }); + }); + }); +} + + +function remove_tablet_on_court (app, tkey, cur_match_id, callback) { + const admin = require('./admin'); // avoid dependency cycle + app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { + if (err) return callback(err); + + app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { + if (err) { + return callback(err); + } + + async.each(matches, async (match, cb) => { + if(match.setup.now_on_court == false) { + return; + } + + const match_id = match._id; + let remove_btp_ids = [ cur_match.setup.tabletoperators[0].btp_id]; + + if(cur_match.setup.tabletoperators.length > 1) { + remove_btp_ids.push(cur_match.setup.tabletoperators[1].btp_id); + } + + + let change = false; + + if (remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + match.setup.teams[0].players[0].now_tablet_on_court = false; + change = true; + } + + if (match.setup.teams[0].players.length > 1 && remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + match.setup.teams[0].players[1].now_tablet_on_court = false; + change = true; + } + + if (remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + match.setup.teams[1].players[0].now_tablet_on_court = false; + change = true; + } + + if (match.setup.teams[1].players.length > 1 && remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + match.setup.teams[1].players[1].now_tablet_on_court = false; + change = true; + } + + if (change) { + const setup = match.setup; + const match_q = {_id: match_id}; + app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + if (err) return cb(err); + + admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + }); + } + }); + }); + }); +} + + function logo_handler(req, res) { const {tournament_key, logo_id} = req.params; assert(tournament_key); diff --git a/static/css/cmatch.css b/static/css/cmatch.css index b3b2a5e..2875f14 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -5,6 +5,7 @@ .match_num { text-align: right; + padding: 0 0.3em 0 0.3em; } .match_edit_button, @@ -53,7 +54,7 @@ text-align: left; } .match_table > tbody > tr:hover > td { - background: #ddd; + background-color: #ddd; } .match_cancel_link { @@ -136,6 +137,99 @@ font-weight: bold; } +.checked_in { + background-color: #adffcb; +} + +.not_checked_in { + background-color: #ffabab; +} + +.now_playing { + background-color: #fbe44d; +} + +.now_on_court { + background-color: inherit; +} + +.tablet, +.tablet_inline, +.umpire, +.no_umpire, +.court, +.court_history { + margin: 0 0.2em -0.2em 0.2em; + display: inline-block; + width: 1.73em; + min-width: 1.73em; + height: 1.2em; + min-height: 1.2em; + background-size: cover; + opacity: 1; + color: #fff; + text-align: center; + -webkit-text-stroke-width: 1px; + -webkit-text-stroke-color: black; + text-shadow: + 3px 3px 3px #000, + -1px -1px 3px #000, + 1px -1px 3px #000, + -1px 1px 3px #000, + 1px 1px 3px #000; +} + +.tablet, +.tablet_inline { + background-image: url(../icons/tablet.svg); +} + +.tablet_inline { + height: 1em; + min-height: 1em; + width: 1.4em; + min-width: 1.4em; + font-weight: 900; + font-size: 1.1em; + margin: -0.1em 0.1em 0.1em 0.1em; + line-height: 100%; +} + +.umpire { + background-image: url(../icons/umpire.svg); +} + +.no_umpire { + background-image: url(../icons/no_umpire.svg); +} + +.court{ + background-image: url(../icons/court.svg); + height: 1em; + min-height: 1em; + width: 1.5em; + min-width: 1.5em; + font-weight: 900; + font-size: 1.1em; + margin: -0.1em 0.1em 0.1em 0.1em; + line-height: 100%; +} + + +.court_history { + background-image: url(../icons/court_history.svg); + height: 1em; + min-height: 1em; + width: 1.5em; + min-width: 1.5em; + font-weight: 900; + font-size: 1.3em; + margin: -0.1em 0.3em 0.1em 0.3em; + line-height: 100%; +} + + + @media print { .toprow, diff --git a/static/css/court.css b/static/css/court.css index de62139..76c060b 100644 --- a/static/css/court.css +++ b/static/css/court.css @@ -1,3 +1,24 @@ .court_num { - vertical-align: top; + display: inline-block; + background-size: cover; + opacity: 1; + color: #fff; + text-align: center; + background-image: url(../icons/court.svg); + height: 1.0em; + min-height: 1.0em; + width: 1.50em; + min-width: 1.50em; + font-weight: 900; + font-size: 1.8em; + -webkit-text-stroke-width: 1px; + -webkit-text-stroke-color: black; + text-shadow: + 3px 3px 3px #000, + -1px -1px 3px #000, + 1px -1px 3px #000, + -1px 1px 3px #000, + 1px 1px 3px #000; + margin: -0.1em 0.3em 0.1em 0.3em; + line-height: 100%; } diff --git a/static/icons/court.svg b/static/icons/court.svg new file mode 100644 index 0000000..9926afc --- /dev/null +++ b/static/icons/court.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/static/icons/court_history.svg b/static/icons/court_history.svg new file mode 100644 index 0000000..0258d87 --- /dev/null +++ b/static/icons/court_history.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/static/icons/no_umpire.svg b/static/icons/no_umpire.svg new file mode 100644 index 0000000..f326720 --- /dev/null +++ b/static/icons/no_umpire.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/static/icons/tablet.svg b/static/icons/tablet.svg new file mode 100644 index 0000000..9901ccd --- /dev/null +++ b/static/icons/tablet.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + +1 + + +1 + + diff --git a/static/icons/umpire.svg b/static/icons/umpire.svg new file mode 100644 index 0000000..1e1c29a --- /dev/null +++ b/static/icons/umpire.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 14b943a..33d8fa1 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -41,7 +41,7 @@ function render_match_table_header(table, include_courts) { uiu.el(title_tr, 'th', {}, ''); } -function render_match_row(tr, match, court, style) { +function render_match_row(tr, match, court, style, show_player_status) { if (!court && match.setup.court_id) { court = curt.courts_by_id[match.setup.court_id]; } @@ -71,7 +71,7 @@ function render_match_row(tr, match, court, style) { } if (style === 'default') { - uiu.el(tr, 'td', {}, court ? court.num : ''); + uiu.el(tr, 'td', court ? 'court_history' : '', court ? court.num : ''); } if (style === 'default' || style === 'plain') { @@ -96,27 +96,28 @@ function render_match_row(tr, match, court, style) { 'class': ((match.team1_won === true) ? 'match_team_won' : ''), style: 'text-align: right;', }); - render_players_el(players0, setup, 0); + render_players_el(players0, setup, 0, show_player_status); uiu.el(tr, 'td', 'match_vs', 'v'); const players1 = uiu.el(tr, 'td', ((match.team1_won === false) ? 'match_team_won ' : '') + 'match_team2'); - render_players_el(players1, setup, 1); + render_players_el(players1, setup, 1, show_player_status); if (style === 'default' || style === 'plain') { const to_td = uiu.el(tr, 'td'); if (setup.umpire_name) { + uiu.el(to_td, 'div', 'umpire', ''); uiu.el(to_td, 'span', {}, setup.umpire_name); if (setup.service_judge_name) { uiu.el(to_td, 'span', {}, ' \u200B+ '); uiu.el(to_td, 'span', {}, setup.service_judge_name); } } else if (setup.tabletoperators && setup.tabletoperators.length > 0){ - uiu.el(to_td, 'spann', 'match_no_umpire', '('); - uiu.el(to_td, 'spann', 'match_no_umpire', setup.tabletoperators[0].name ); + uiu.el(to_td, 'div', 'tablet', ''); + uiu.el(to_td, 'span', 'match_no_umpire', setup.tabletoperators[0].name ); if (setup.tabletoperators.length > 1) { - uiu.el(to_td, 'spann', 'match_no_umpire', ' \u200B/ '); - uiu.el(to_td, 'spann', 'match_no_umpire', setup.tabletoperators[1].name ); + uiu.el(to_td, 'span', 'match_no_umpire', ' \u200B/ '); + uiu.el(to_td, 'span', 'match_no_umpire', setup.tabletoperators[1].name ); } - uiu.el(to_td, 'spann', 'match_no_umpire', ')'); } else { + uiu.el(to_td, 'div', 'no_umpire', ''); uiu.el(to_td, 'span', 'match_no_umpire', ci18n('No umpire')); } } @@ -154,20 +155,40 @@ function update_match_score(m) { }); } -function render_players_el(parentNode, setup, team_id) { +function render_players_el(parentNode, setup, team_id, show_player_status) { const team = setup.teams[team_id]; if (setup.incomplete) { uiu.el(parentNode, 'span', {}, ci18n('match:incomplete')); } const nat0 = team.players[0] && team.players[0].nationality; - if (!curt.is_nation_competition || !nat0) { - uiu.el(parentNode, 'span', {}, team.players.map(p => p.name.replace(' ', '\xa0')).join(' / ')); - return; + if (curt.is_nation_competition && nat0) { + cflags.render_flag_el(parentNode, nat0); + } + + let player0_status = ""; + if (!show_player_status || setup.now_on_court) { + player0_status = "now_on_court"; + } else if (team.players[0].now_playing_on_court || team.players[0].now_tablet_on_court) { + player0_status = "now_playing"; + } else if (team.players[0].checked_in) { + player0_status = "checked_in"; + } else { + player0_status = "not_checked_in"; } - cflags.render_flag_el(parentNode, nat0); - uiu.el(parentNode, 'span', {}, team.players[0].name); + let player_element = uiu.el(parentNode, 'span', player0_status, team.players[0].name.replace(' ', '\xa0')); + if (team.players[0].now_playing_on_court && player0_status != "now_on_court") { + let parts = team.players[0].now_playing_on_court.split("_"); + let court_number = parts[parts.length - 1]; + uiu.el(player_element, 'div', 'court', court_number); + } + + if(team.players[0].now_tablet_on_court) { + let parts = team.players[0].now_tablet_on_court.split("_"); + let court_number = parts[parts.length - 1]; + uiu.el(player_element, 'div', 'tablet_inline', court_number); + } if (team.players.length > 1) { uiu.el(parentNode, 'span', {}, ' / '); @@ -176,12 +197,33 @@ function render_players_el(parentNode, setup, team_id) { const p1_el = uiu.el(parentNode, 'span', { 'style': 'white-space: pre', }); - if (nat1 && (nat1 !== nat0)) { + if (curt.is_nation_competition && nat1 && (nat1 !== nat0)) { cflags.render_flag_el(p1_el, nat1); } - const partner_name = team.players[1].name.replace(' ', '\xa0'); - uiu.el(p1_el, 'span', {}, partner_name); + let player1_status = ""; + if (!show_player_status || setup.now_on_court) { + player1_status = "now_on_court"; + } else if (team.players[1].now_playing_on_court || team.players[1].now_tablet_on_court) { + player1_status = "now_playing"; + } else if (team.players[1].checked_in) { + player1_status = "checked_in"; + } else { + player1_status = "not_checked_in"; + } + + let player_element = uiu.el(p1_el, 'span', player1_status, team.players[1].name.replace(' ', '\xa0')); + if (team.players[1].now_playing_on_court && player1_status != "now_on_court") { + let parts = team.players[1].now_playing_on_court.split("_"); + let court_number = parts[parts.length - 1]; + uiu.el(player_element, 'div', 'court', court_number); + } + + if(team.players[1].now_tablet_on_court) { + let parts = team.players[1].now_tablet_on_court.split("_"); + let court_number = parts[parts.length - 1]; + uiu.el(player_element, 'div', 'tablet_inline', court_number); + } } } @@ -475,14 +517,19 @@ crouting.register(/t\/([a-z0-9]+)\/m\/([-a-zA-Z0-9_ ]+)\/scoresheet$/, function( ui_scoresheet(match_id); })); -function render_match_table(container, matches, include_courts) { +function render_match_table(container, matches, include_courts, show_player_status) { + if(!show_player_status) + { + show_player_status = false; + } + const table = uiu.el(container, 'table', 'match_table'); render_match_table_header(table, include_courts); const tbody = uiu.el(table, 'tbody'); for (const m of matches) { const tr = uiu.el(tbody, 'tr'); - render_match_row(tr, m, null, include_courts ? 'default' : 'plain'); + render_match_row(tr, m, null, include_courts ? 'default' : 'plain', show_player_status); } } @@ -491,7 +538,7 @@ function render_unassigned(container) { uiu.el(container, 'h3', {}, ci18n('Unassigned Matches')); const unassigned_matches = curt.matches.filter(m => calc_section(m) === 'unassigned'); - render_match_table(container, unassigned_matches, curt.only_now_on_court); + render_match_table(container, unassigned_matches, curt.only_now_on_court, true); } function render_upcoming_matches(container) { From 96fd554ee9155728603beb8251d1a85abf682155 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sun, 25 Feb 2024 02:40:52 +0100 Subject: [PATCH 010/224] add some Icons for upcomming features --- static/icons/preperation.svg | 219 +++++++++++++++++++++++++++++++++++ static/icons/second_call.svg | 116 +++++++++++++++++++ static/icons/start.svg | 193 ++++++++++++++++++++++++++++++ 3 files changed, 528 insertions(+) create mode 100644 static/icons/preperation.svg create mode 100644 static/icons/second_call.svg create mode 100644 static/icons/start.svg diff --git a/static/icons/preperation.svg b/static/icons/preperation.svg new file mode 100644 index 0000000..abae4ad --- /dev/null +++ b/static/icons/preperation.svg @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/second_call.svg b/static/icons/second_call.svg new file mode 100644 index 0000000..b026d6f --- /dev/null +++ b/static/icons/second_call.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + 2 + + diff --git a/static/icons/start.svg b/static/icons/start.svg new file mode 100644 index 0000000..5ee9a6e --- /dev/null +++ b/static/icons/start.svg @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + From fed4b3ee8dc9ca6ca8e9f769ead23c4d731d6473 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 25 Feb 2024 18:11:04 +0100 Subject: [PATCH 011/224] Add further Calls to be executed using ui --- .vs/VSWorkspaceState.json | 11 ++ ...048b60c6-6f51-4ac8-9fd0-d3a6f9f42591.vsidx | Bin 0 -> 77480 bytes ...59f74877-0cf5-4297-96d2-7ecab9c90a0c.vsidx | Bin 0 -> 13554 bytes .vs/bts/v17/.wsuo | Bin 0 -> 41472 bytes .vs/bts/v17/TestStore/0/000.testlog | Bin 0 -> 20 bytes .vs/bts/v17/TestStore/0/testlog.manifest | Bin 0 -> 24 bytes .vs/slnx.sqlite | Bin 0 -> 118784 bytes static/css/cmatch.css | 44 ++++++-- static/js/announcements.js | 100 ++++++++++++------ static/js/ci18n_de.js | 5 + static/js/ci18n_en.js | 5 + static/js/cmatch.js | 83 ++++++++++++--- 12 files changed, 193 insertions(+), 55 deletions(-) create mode 100644 .vs/VSWorkspaceState.json create mode 100644 .vs/bts/FileContentIndex/048b60c6-6f51-4ac8-9fd0-d3a6f9f42591.vsidx create mode 100644 .vs/bts/FileContentIndex/59f74877-0cf5-4297-96d2-7ecab9c90a0c.vsidx create mode 100644 .vs/bts/v17/.wsuo create mode 100644 .vs/bts/v17/TestStore/0/000.testlog create mode 100644 .vs/bts/v17/TestStore/0/testlog.manifest create mode 100644 .vs/slnx.sqlite diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000..222cc5e --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,11 @@ +{ + "ExpandedNodes": [ + "", + "\\static", + "\\static\\css", + "\\static\\icons", + "\\static\\js" + ], + "SelectedNode": "\\static\\css\\cmatch.css", + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/bts/FileContentIndex/048b60c6-6f51-4ac8-9fd0-d3a6f9f42591.vsidx b/.vs/bts/FileContentIndex/048b60c6-6f51-4ac8-9fd0-d3a6f9f42591.vsidx new file mode 100644 index 0000000000000000000000000000000000000000..550558c29e98813f6a9f458a313e8630edd0bb1b GIT binary patch literal 77480 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 Cdc~js literal 0 HcmV?d00001 diff --git a/.vs/bts/FileContentIndex/59f74877-0cf5-4297-96d2-7ecab9c90a0c.vsidx b/.vs/bts/FileContentIndex/59f74877-0cf5-4297-96d2-7ecab9c90a0c.vsidx new file mode 100644 index 0000000000000000000000000000000000000000..3d79cbffdd58625767c230884f2d1983b38d2c2c GIT binary patch literal 13554 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 literal 0 HcmV?d00001 diff --git a/.vs/bts/v17/.wsuo b/.vs/bts/v17/.wsuo new file mode 100644 index 0000000000000000000000000000000000000000..c508485281b3cb15a63eeb8255e0fedca471705f GIT binary patch literal 41472 zcmeHQ3y>S-eP7ww*ggX`uTn$JVVi`(y^;E zK0lfVNjmA!l+p?5B+ZnxkW4zABqY;el2YnAA#Fnk(`km8X4+}!3j#ESFw-z)!u9vt z-A`KWNp~yh?sNuyns2|=?zi9nd;Xu_L>D}qo(n`y7m*rks zCrK;tpU>W%o}T6_-s86IE}|MZAQkW{N}W=#lt-vaQ*27d?ppK$QQRBn_^Q*}ad73> zPh1-i@8cVd$o+s=kd8^a0X>7eUg_iv$npGZW_qg;5(n_r6$n27zX$$9@T=e%_gaM4!7~o?!jR9`3M0l%tzZ2o@@Llk3cn|zmcrUyUp7j}k55o7si?o{wng88p zT9;YU>@@H9A!HwF2F6nOb}`5Qw>GY6kAL);r8NFU9~5|i{a&~=$m^wM9~byQykl5x z{NG^4uQmRyID~23g!sfIyr=sO^L{Cff08OAchl3I=KXtR{Ii{wp-dzPm%x*b5Z|-i z*}la8><7f_#LdKo{D$pF{6`$mzH=2kpIbd?g7{DTe~;xqd4*1Rwm;)+Mo9W_GdyVw z+h2Tpo8?|S+iH78{4d^xaUU`N#}ICVzXN_d{0{g#;qQXK8$J%-4c`NwfbWGT4cQ65 z3!XH9>E3IGdlBx3XZ!b?&kmTOfG@(scz)b`cLd=G{89Li!prdY!Hc*I)A+BzXUykg z2($3V%>OxrHw5RsxNZxno_7f-!-(ksOfhEW3vP;DQuMFLNJQV4N~VTenX>GBNm zn^XAYi8SN!(C3#oe{J8^#cvaKa~GoGn0!07TbObKWl>y{a=Sm*mP#Hvj&tG$xF~V zg+B=B?4j(R6DDkazpVlIY%c+y{Vj#@#?qZY+G(@w_VM6B+ZRysG-`$Qz`jA(Ncn#q z-?7iq)dOGFTcaC!WjnFQO5kCZxyWa+PMy}jwg0th{Qo!dOV=L%we>%O{x0PBcY=S~ z^#8p}{}aG>N#L_l6YEnCO08#J$AoFn-;+8moX{~ZIRgXl8>3eOJvR71%5P5bPZ#0} z{AY(xu7j|NeF<-lIme zipN&`YlpuVfx=lv9i|7$#nt#E%-}0-D&x6MchF%i8cMMf62Vs-F@TFw!aALyZ$2^ zuIPVD`djheX-GQ2_H%)bv!-u8s3~f{TF7K`%FV-CVNy*g!>Te-(1wdSWlSAT7PVom zC>OJ-;bYoxYFsX+#yrQgGpD_#L+-p@+eN%Pd&cLD#=5*``sAFZP?lytAS-OL!?>Hc zJ6p)BN)8}SDBB1wS)3Trrt&F*Idj?HA{L9E{e~-n z3Up9~`p0ZIk;o;yxnW>RD=Oojo9Lp=Gi4{V?{ZcQ^@MC*;A0yeR8T;P%j-) zR1Gm!^%TY@WK~huF=kI8t)LoDWYdbeiVp^|r<6J?3BJe&!wIyX2K+)Ea(bE=wz2%P)!B8YryqV)jQ>r4HMry6)4iL&{rLHw zKPdj_{M6*;>#qOM(~-A7;`;2+H_>y=FO2V|r|(lgz2W*Jv+Q0gdnRQ^T zKfbNUJ=A3R?exc>IV`PV35}z-Oz4s^?UTk+YyLq0D_Ye0uGT&26?M&YRvKrQKl}B1 z&7*&&fsrWnl;uw2Yn}h78$N9e#M^YU!5;!-6=fa-oNUa;Tf8vyVUHsq?GQV>$n#GG<1Ri(BQ%VgZCao9dBs z#Ljg%1CNyPM;)K)QJXD)z3`S=d?)f>aF$F%|D@aLX|G{h%o4TqDuufrn*W@@50t)~ zdDDpgc@)Wd;xg(~otB>(Tp|A)*0EX+ z*o8WsQBV^j4{mT8E(h^m)c+75 z2;Cy3y_2W`VtwiY=aM}MW|LX})F(SFKV>gL|Af4)qV>s#m$mL8_E$P5@Ml6bzU7tpZ#}{;c&G3^h+BlGv%{ZD-(q}Kz6$*V zVbhi0)V_1$mFWL=_=oh-PnoCP{-XcQ(f+QBx&7JSCiF3<0TUXwKc}>ZJxtKwPTV`~ zf142Z?Ku@XSj+-_2LtX5haE+F$Q?q7IzY zX}kT)wN!on!2$4Id7Tp~f>IgU4XwnRSZ5(@$M~Z>%`we3D5qTejzfPt878>u@C}LE z2_H`W_bQ~b1>Pxq!sUDcx+CDT(YOk{?7NX8aKsVysuZ_q*tk18S3GIVs=NR-`qo9EX>@tr% zgw~zQk^iSIX(hbV`lme2I-u(T&rbrjZgh=V%rwpFFPq6b_1CS@AE)#dABJRSwVN1EANKssczA1P?3>?z z{?yjdmD+>*ABpU~+0;EOy;sF z7_9~i$Cdn!q&JWWWg;1$FCFsA0eLNH$i$qWJiGHnMa{@5MVk+(D_GL)b3^SZvn*#%Vr}!^n3+x%2qQNVl9ij);9(C70g39bdA} zwB7lB8C8-~a`reuBs5USP0~ns2-ZG)p_#Cle)WW}gyj(Mdh;i;Y9T+ai_hhxA9Tci(`6bdym&d+*7jlGn1yY%W`z^7Nim zQhL>hVzJAtVi$#5O&!RJchGs8PnVtI(i60-j*LjF2*m^*+p8v5J^FKHbxK;B%jsrIJkQ}@8hHKCNcyG=B@QT?VBBX_} zG{c}h|D(D2A9nj`z4Je&r#Uz4?3q$4wda2bMC2%JR(@*Bf32ARHJki*==;|PUVHoC z8}H=5`iZAsUbX+KFFpRXS0=xlKKQ*S{`@Qdxa_~!Eyyq$u37o7J^!m2{CDxs|EjKk zy}0zh)P1&6|4aM37>^vCb2+XB-fTvi2mNmeHuEqSY`!0X7y93Mu>a_DUpkSdQ}~+@ zw{8DJCCrl(ea~tASAdPptx~aQvYVb_DfBkdYAR6H^TKHym#yh= z>#KKzn3L|t>RhhY@!~EH3l7)py7l?!aa?=!^}KzsWcA@bAf1(5t~7M+#;hmKI^qgP z!|uer0tkY82Sj|)Oel=~#TwUn-CyYo!XwqdS~z&Z$zk{^X$bj;I#U?1mk5DDp$`0!+Pf>;u3>M zU;px^s|wtmN?uX3DNiD+uZWS=sr&p#j#OxfyE@0UR6)&UlVweR(6M z1Qje3I@%=?ueuCpeqkMD4b51}Q(C!$(Xr}Uv#9&jbT%*NL=)*Vx%4_`O<%$)uMv|~ z+%b|J%@~E!}CU^AbeWVbn{*~(emOyL#NLy%gbyyP%{l3<>gt{^u@hp zc~&qSOt@n{pU>SBv@Fl+C@tx7nb=$Jew>)c8~V$Q6*C@C2Msi#$|FuAbqT$`b;XlG zZ}?>PJH}wT&D4K=tB?T9aJQX%36Oj7r%nIgGxXm|^R2wVZ1kUe!x4Q~BF?j@xJ&%7m{f#ed{GS-VHD;nz zuHMV*bAR`OYczJIsK5S#I*-~o|LVaUrMdZ6mb5zm%Xv8DfwqKVvJz2JF}D&81>Hfv zoOH{{OvoMehy1cXnu(+$zLnC~?fmO3OM`8=yE^~6w|D36fjQRsR!%6v65Mzw==J%6 zes^CmmT(6HiEeky+l>{up-6W;5RCPPLOo}HtMRPPATw2Gwm8km*BZ}A<8@ZCHrl>u zxdv7IbmJMVRb!20!|~c_EI*&OK8B3WkR=>e+d~UdZH?xpB_ov0tQIiQB0pMfk$jE* zwg4lvv5d5AgpSGiQKgO=Tc8nIZ#HIVF&2~x%(VqWjo$T%c^sAvW&uo%OE5^;yIV3c zFLK(sQcAR|&D`XKdtNx?eb$seaqViHiKy&6*Hq<#7CQYdS@B2YaMJ5m0>Ka%(`eKk z3#T*gkRoRSv7n*^!Z8mtub4(~hJ3o!T_y<TZAn+C)ne_Q zn#XPd+^K?dxVbl^u{T!r{bm;@r?vK3ZDpRgn}ywa%^jY&(iRir?sTVRcbxZ z(DrGbRcGNX8I@4Wu$qwUoz~bAQFpY3dS@NH(*E1BURJNEXMuWIz1e8nek;#AXxn}p zu>E$3`4`f4=Q`WAU)!N64hePJ_6yb3?Iqjq4d?D^+kTa8wGFFI*_pQO*D`@+mT3oV z+i&?mrgE>d*m0e9xVG)r)Bqb3quRD#am<&cSJf+%wQav3j&0kolf&YcnC*AgwKq2V zvE80L5Ay|{F0tk%Ym~m6UaS>1QB^xw$Qs~zH%rGhG26aK_VWW5$DUK&c4FPNx7yi; zt;}1sJ&9`9-4e$E6=!{xD6_+}K5_7)FIeTwN9)!S@Av)xTGvx)eQeur8$Wx~u-`U| zzw0@#v~ss{;yrAi4F8#icaNxV_k!7N>&85!KU}1|RHF-X^=M<#_9HFHVJ*r!zvtT4 zg2M9+YSW;_K6iI!KVC|CtB-!yS9iu~t_giMh>TNz#knS}JO9}@g}a_zpDGunIX?wD z+TQ=yJdfIX{&REZXV_`bOP{>%@~6&R`l|cMl=|;KUG?nm{{E)!TV8zq)Tybz_l>9C zc#x8eb+td)Cqwr8x4(Gh*+2fhfBMPCRjNhI%#rWeavLtUQ5LlR9x0 z5O-|xp=7)o=~q9Go@a@)hNCe0sa2Lzm5BnjC(J2h>M+$u7Nu&*_U}-1^LZh>c@5bu zGaHP}C_ncP}e_!nQ|NV}?>%o^Z`QEnt j##5e$aRMCc@nV$UC|MbcHo1?Wic$zFQz@a=lFa@e#NPFL literal 0 HcmV?d00001 diff --git a/.vs/bts/v17/TestStore/0/000.testlog b/.vs/bts/v17/TestStore/0/000.testlog new file mode 100644 index 0000000000000000000000000000000000000000..6bbf10ed81b7163bf1f3b40a26c65804007b7561 GIT binary patch literal 20 XcmY#XEb%NUP7PsYU|>)HVh{iTD<1;I literal 0 HcmV?d00001 diff --git a/.vs/bts/v17/TestStore/0/testlog.manifest b/.vs/bts/v17/TestStore/0/testlog.manifest new file mode 100644 index 0000000000000000000000000000000000000000..e92ede29d76aefe079835aeae278da5341f6e15c GIT binary patch literal 24 WcmXR;&-W=QP7PsZKmZO+yIuf90t7Vx literal 0 HcmV?d00001 diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..3fbce384fdd2dadf6659ce179730d39afc742fa8 GIT binary patch literal 118784 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ɓ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 * { + display: block; +} +.match_second_call_button > button { + margin: 0 0 0 2vmin; + font-size: 5vmin; + padding: 0.1em 0.1em; + min-width: 5em; +} + .match_umpire_style_umpires { font-weight: bold; } @@ -229,8 +261,6 @@ } - - @media print { .toprow, .main, diff --git a/static/js/announcements.js b/static/js/announcements.js index 538b7aa..c184a86 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -1,5 +1,5 @@ - -function announceNewMatch(matchSetup){ + +function announceNewMatch(matchSetup) { var field = createFieldAnnouncement(matchSetup); var matchNumber = createMatchNumberAnnouncement(matchSetup); var eventName = createEventAnnouncement(matchSetup); @@ -17,10 +17,35 @@ function announcePreparationMatch(matchSetup) { var teams = createTeamAnnouncement(matchSetup); announce([preparation, matchNumber, eventName, round, teams]); } +function announceSecondCallTeamOne(matchSetup) { + announceSecondCall(matchSetup, matchSetup.teams[0]); +} + +function announceSecondCallTeamTwo(matchSetup) { + announceSecondCall(matchSetup, matchSetup.teams[1]); +} + +function announceSecondCallTabletoperator(matchSetup) { + const call = createFieldAnnouncement(matchSetup) + createSecondCallAnnouncement() + createTabletOperator(matchSetup); + announce([call]); +} + +function announceSecondCall(matchSetup, team) { + var secondCall = createSecondCallAnnouncement() + createSingleTeam(team.players); + var field = createFieldAnnouncement(matchSetup); + announce([secondCall, field]); +} +function announceBeginnToPlay(matchSetup, team) { + announce([createFieldAnnouncement(matchSetup) + "Bitte mit dem Spielen beginnen!"]); +} -function createTeamAnnouncement(matchSetup){ - var teams = createSingleTeam(matchSetup.teams[0].players)+", gegen "+createSingleTeam(matchSetup.teams[1].players); - return teams; +function createSecondCallAnnouncement() { + return "Zweiter Aufruf fuer:"; +} + +function createTeamAnnouncement(matchSetup) { + var teams = createSingleTeam(matchSetup.teams[0].players) + ", gegen " + createSingleTeam(matchSetup.teams[1].players); + return teams; } function createTabletOperator(matchSetup) { @@ -32,19 +57,19 @@ function createTabletOperator(matchSetup) { } else { tabletOperator = tabletOperator + "Verlierer des vorhergehenden Spiels"; } - + return tabletOperator; } -function createSingleTeam(playersSetup){ +function createSingleTeam(playersSetup) { var team = playersSetup[0].name; - if (playersSetup.length == 2){ - team = team+" und "+playersSetup[1].name + if (playersSetup.length == 2) { + team = team + " und " + playersSetup[1].name } return team; } -function createRoundAnnouncement(matchSetup){ +function createRoundAnnouncement(matchSetup) { var round = matchSetup.match_name; if (round == "R16") { round = "Achtelfinale"; @@ -69,26 +94,26 @@ function createRoundAnnouncement(matchSetup){ } return round; } -function createEventAnnouncement(matchSetup){ +function createEventAnnouncement(matchSetup) { var eventParts = matchSetup.event_name.split(" "); var eventName = ""; - if (eventParts[0] == 'JE'){ + if (eventParts[0] == 'JE') { eventName = "Jungeneinzel" - }else if (eventParts[0] == 'JD') { + } else if (eventParts[0] == 'JD') { eventName = "Jungendoppel" - }else if (eventParts[0] == 'ME') { - eventName = "Mdcheneinzel" - }else if (eventParts[0] == 'MD') { - eventName = "Mdchendoppel" + } else if (eventParts[0] == 'ME') { + eventName = "Maedcheneinzel" + } else if (eventParts[0] == 'MD') { + eventName = "Maedchendoppel" } else if (eventParts[0] == 'GD' || eventParts[0] == 'MX') { eventName = "Gemischtesdoppel" - }else if (eventParts[0] == 'HE'){ + } else if (eventParts[0] == 'HE') { eventName = "Herreneinzel" - }else if (eventParts[0] == 'HD') { + } else if (eventParts[0] == 'HD') { eventName = "Herrendoppel" - }else if (eventParts[0] == 'DE') { + } else if (eventParts[0] == 'DE') { eventName = "Dameneinzel" - }else if (eventParts[0] == 'DD') { + } else if (eventParts[0] == 'DD') { eventName = "Damendoppel" } if (eventName == "") { @@ -97,9 +122,9 @@ function createEventAnnouncement(matchSetup){ } else if (eventParts[1] == 'JD') { eventName = "Jungendoppel" } else if (eventParts[1] == 'ME') { - eventName = "Mdcheneinzel" + eventName = "Maedcheneinzel" } else if (eventParts[1] == 'MD') { - eventName = "Mdchendoppel" + eventName = "Maedchendoppel" } else if (eventParts[1] == 'GD' || eventParts[1] == 'MX') { eventName = "Gemischtesdoppel" } else if (eventParts[1] == 'HE') { @@ -111,21 +136,30 @@ function createEventAnnouncement(matchSetup){ } else if (eventParts[1] == 'DD') { eventName = "Damendoppel" } - eventName = eventName + " " + eventParts[0]; + if (eventParts[0]) { + eventName = eventName + " " + eventParts[0]; + } } else { - eventName = eventName + " " + eventParts[1]; + if (eventParts[1]) { + eventName = eventName + " " + eventParts[1]; + } } return eventName; } -function createMatchNumberAnnouncement(matchSetup){ +function createMatchNumberAnnouncement(matchSetup) { var number = matchSetup.match_num; return "Spiel Nummer " + number + "!"; } -function createFieldAnnouncement(matchSetup){ - var court = matchSetup.court_id.split("_")[1]; - return "Auf Spielfeld " + court + "!"; +function createFieldAnnouncement(matchSetup) { + if (matchSetup.court_id) { + var court = matchSetup.court_id.split("_")[1]; + return "Auf Spielfeld " + court + "!"; + } else { + return ""; + } + } function createPreparationAnnouncement() { @@ -136,18 +170,18 @@ function announce(callArray) { // Seems like the getVoices() is an asynchronous function where it is not always guaranteed that you get a // result immediately. The wait for the result must therefore be handled: // https://stackoverflow.com/questions/21513706/getting-the-list-of-voices-in-speechsynthesis-web-speech-api - const allVoicesObtained = new Promise(function(resolve, reject) { + const allVoicesObtained = new Promise(function (resolve, reject) { let voices = window.speechSynthesis.getVoices(); if (voices.length !== 0) { resolve(voices); } else { - window.speechSynthesis.addEventListener("voiceschanged", function() { + window.speechSynthesis.addEventListener("voiceschanged", function () { voices = window.speechSynthesis.getVoices(); resolve(voices); }); } }); - + allVoicesObtained.then(voices => { var voice = null; for (var i = 0; i < voices.length; i++) { @@ -165,5 +199,5 @@ function announce(callArray) { words.voice = voice; window.speechSynthesis.speak(words); }); - }); + }); } \ No newline at end of file diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index bbc9ddc..31e20f8 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -79,6 +79,11 @@ var ci18n_de = { 'to_stats:total': 'Total', 'match:rawinfo': 'Technische Informationen', +'match:preparationcall': 'Aufruf: Spiel in Vorbereitung', +'match:begintoplay': 'Aufruf: Mit dem Spielen beginnen', +'match:secondcallteamone': 'Aufruf: Zweiter Aufruf Team 1', +'match:secondcallteamtwo': 'Aufruf: Zweiter Aufruf Team 2', +'match:secondcaltabletoperator': 'Aufruf: Zweiter Aufruf Tabletbediener', 'match:scoresheet': 'Schiedsrichterzettel', 'match:edit': 'Bearbeiten', 'match:incomplete': '[Unvollständig!] ', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index c3a69df..2b64eef 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -79,6 +79,11 @@ var ci18n_en = { 'to_stats:total': 'Total', 'match:rawinfo': 'Technical information', +'match:preparationcall': 'Announcement: Match in preparation', +'match:begintoplay': 'Announcement: Beginn to play', +'match:secondcallteamone': 'Announcement: Second call Team 1', +'match:secondcallteamtwo': 'Announcement: Second call Team 2', +'match:secondcaltabletoperator': 'Announcement: Second call Tabletoperator', 'match:scoresheet': 'Scoresheet', 'match:edit': 'Edit', 'match:override_colors': 'Override colors', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 33d8fa1..bbe5bce 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -45,24 +45,20 @@ function render_match_row(tr, match, court, style, show_player_status) { if (!court && match.setup.court_id) { court = curt.courts_by_id[match.setup.court_id]; } - + const activeMatch = court && !match.btp_winner; const setup = match.setup; if (style === 'default' || style === 'plain') { const actions_td = uiu.el(tr, 'td'); - const edit_btn = uiu.el(actions_td, 'div', { - 'class': 'vlink match_edit_button', - 'data-match__id': match._id, - 'title': ci18n('match:edit'), - }); - edit_btn.addEventListener('click', on_edit_button_click); - - const scoresheet_btn = uiu.el(actions_td, 'div', { - 'class': 'vlink match_scoresheet_button', - 'title': ci18n('match:scoresheet'), - 'data-match__id': match._id, - }); - scoresheet_btn.addEventListener('click', on_scoresheet_button_click); + create_match_button(actions_td, 'vlink match_edit_button', 'match:edit', on_edit_button_click, match._id); + create_match_button(actions_td, 'vlink match_scoresheet_button', 'match:scoresheet', on_scoresheet_button_click, match._id); + if (!court) { + create_match_button(actions_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); + } else { + if (activeMatch) { + create_match_button(actions_td, 'vlink match_begin_to_play_button', 'match:begintoplay', on_begin_to_play_button_click, match._id); + } + } uiu.el(actions_td, 'a', { 'class': 'match_rawinfo', 'title': ci18n('match:rawinfo'), @@ -96,10 +92,16 @@ function render_match_row(tr, match, court, style, show_player_status) { 'class': ((match.team1_won === true) ? 'match_team_won' : ''), style: 'text-align: right;', }); + if (activeMatch) { + create_match_button(players0, 'vlink match_second_call_button', 'match:secondcallteamone', on_second_call_team_one_button_click, match._id); + } render_players_el(players0, setup, 0, show_player_status); uiu.el(tr, 'td', 'match_vs', 'v'); const players1 = uiu.el(tr, 'td', ((match.team1_won === false) ? 'match_team_won ' : '') + 'match_team2'); render_players_el(players1, setup, 1, show_player_status); + if (activeMatch) { + create_match_button(players1, 'vlink match_second_call_button', 'match:secondcallteamtwo', on_second_call_team_two_button_click, match._id); + } if (style === 'default' || style === 'plain') { const to_td = uiu.el(tr, 'td'); if (setup.umpire_name) { @@ -120,6 +122,9 @@ function render_match_row(tr, match, court, style, show_player_status) { uiu.el(to_td, 'div', 'no_umpire', ''); uiu.el(to_td, 'span', 'match_no_umpire', ci18n('No umpire')); } + if (activeMatch) { + create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); + } } if (style === 'default' || style === 'plain'/* || style === 'public' WIP */) { @@ -144,7 +149,14 @@ function render_match_row(tr, match, court, style, show_player_status) { }, match.shuttle_count || ''); } } - +function create_match_button(targetEl, cssClass, title, listener, matchId,) { + const btn = uiu.el(targetEl, 'div', { + 'class': cssClass, + 'title': ci18n(title), + 'data-match__id': matchId, + }); + btn.addEventListener('click', listener); +} function update_match_score(m) { uiu.qsEach('.match_score[data-match_id=' + JSON.stringify(m._id) + ']', function(score_el) { uiu.text(score_el, calc_score_str(m)); @@ -275,7 +287,48 @@ function on_scoresheet_button_click(e) { const match_id = btn.getAttribute('data-match__id'); ui_scoresheet(match_id); } +function on_announce_preparation_matchbutton_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + announcePreparationMatch(match.setup); + } +} +function on_second_call_team_one_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + announceSecondCallTeamOne(match.setup); + } +} +function on_second_call_team_two_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + announceSecondCallTeamTwo(match.setup); + } +} +function on_second_call_tabletoperator_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + announceSecondCallTabletoperator(match.setup); + } +} +function on_begin_to_play_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + announceBeginnToPlay(match.setup); + } +} +function fetchMatchFromEvent(e) { + const btn = e.target; + const match_id = btn.getAttribute('data-match__id'); + const match = utils.find(curt.matches, m => m._id === match_id); + if (!match) { + cerror.silent('Match ' + match_id + ' konnte nicht gefunden werden'); + return null; + } else { + return match; + } +} function _nation_team_name(nat0, nat1) { if (nat1 && nat0 && (nat0 != nat1)) { return countries.lookup(nat0) + ' / ' + countries.lookup(nat1); From b2e0906fafb7316e8c7da09aa82c0f3625f5b3c1 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 25 Feb 2024 18:16:35 +0100 Subject: [PATCH 012/224] Revert "Add further Calls to be executed using ui" This reverts commit fed4b3ee8dc9ca6ca8e9f769ead23c4d731d6473. --- .vs/VSWorkspaceState.json | 11 -- ...048b60c6-6f51-4ac8-9fd0-d3a6f9f42591.vsidx | Bin 77480 -> 0 bytes ...59f74877-0cf5-4297-96d2-7ecab9c90a0c.vsidx | Bin 13554 -> 0 bytes .vs/bts/v17/.wsuo | Bin 41472 -> 0 bytes .vs/bts/v17/TestStore/0/000.testlog | Bin 20 -> 0 bytes .vs/bts/v17/TestStore/0/testlog.manifest | Bin 24 -> 0 bytes .vs/slnx.sqlite | Bin 118784 -> 0 bytes static/css/cmatch.css | 44 ++------ static/js/announcements.js | 100 ++++++------------ static/js/ci18n_de.js | 5 - static/js/ci18n_en.js | 5 - static/js/cmatch.js | 83 +++------------ 12 files changed, 55 insertions(+), 193 deletions(-) delete mode 100644 .vs/VSWorkspaceState.json delete mode 100644 .vs/bts/FileContentIndex/048b60c6-6f51-4ac8-9fd0-d3a6f9f42591.vsidx delete mode 100644 .vs/bts/FileContentIndex/59f74877-0cf5-4297-96d2-7ecab9c90a0c.vsidx delete mode 100644 .vs/bts/v17/.wsuo delete mode 100644 .vs/bts/v17/TestStore/0/000.testlog delete mode 100644 .vs/bts/v17/TestStore/0/testlog.manifest delete mode 100644 .vs/slnx.sqlite diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json deleted file mode 100644 index 222cc5e..0000000 --- a/.vs/VSWorkspaceState.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "ExpandedNodes": [ - "", - "\\static", - "\\static\\css", - "\\static\\icons", - "\\static\\js" - ], - "SelectedNode": "\\static\\css\\cmatch.css", - "PreviewInSolutionExplorer": false -} \ No newline at end of file diff --git a/.vs/bts/FileContentIndex/048b60c6-6f51-4ac8-9fd0-d3a6f9f42591.vsidx b/.vs/bts/FileContentIndex/048b60c6-6f51-4ac8-9fd0-d3a6f9f42591.vsidx deleted file mode 100644 index 550558c29e98813f6a9f458a313e8630edd0bb1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77480 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 Cdc~js diff --git a/.vs/bts/FileContentIndex/59f74877-0cf5-4297-96d2-7ecab9c90a0c.vsidx b/.vs/bts/FileContentIndex/59f74877-0cf5-4297-96d2-7ecab9c90a0c.vsidx deleted file mode 100644 index 3d79cbffdd58625767c230884f2d1983b38d2c2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13554 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 diff --git a/.vs/bts/v17/.wsuo b/.vs/bts/v17/.wsuo deleted file mode 100644 index c508485281b3cb15a63eeb8255e0fedca471705f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41472 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 diff --git a/.vs/bts/v17/TestStore/0/000.testlog b/.vs/bts/v17/TestStore/0/000.testlog deleted file mode 100644 index 6bbf10ed81b7163bf1f3b40a26c65804007b7561..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20 XcmY#XEb%NUP7PsYU|>)HVh{iTD<1;I diff --git a/.vs/bts/v17/TestStore/0/testlog.manifest b/.vs/bts/v17/TestStore/0/testlog.manifest deleted file mode 100644 index e92ede29d76aefe079835aeae278da5341f6e15c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24 WcmXR;&-W=QP7PsZKmZO+yIuf90t7Vx diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite deleted file mode 100644 index 3fbce384fdd2dadf6659ce179730d39afc742fa8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118784 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ɓ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 * { - display: block; -} -.match_second_call_button > button { - margin: 0 0 0 2vmin; - font-size: 5vmin; - padding: 0.1em 0.1em; - min-width: 5em; -} - .match_umpire_style_umpires { font-weight: bold; } @@ -261,6 +229,8 @@ match_second_call_button, } + + @media print { .toprow, .main, diff --git a/static/js/announcements.js b/static/js/announcements.js index c184a86..538b7aa 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -1,5 +1,5 @@ - -function announceNewMatch(matchSetup) { + +function announceNewMatch(matchSetup){ var field = createFieldAnnouncement(matchSetup); var matchNumber = createMatchNumberAnnouncement(matchSetup); var eventName = createEventAnnouncement(matchSetup); @@ -17,35 +17,10 @@ function announcePreparationMatch(matchSetup) { var teams = createTeamAnnouncement(matchSetup); announce([preparation, matchNumber, eventName, round, teams]); } -function announceSecondCallTeamOne(matchSetup) { - announceSecondCall(matchSetup, matchSetup.teams[0]); -} - -function announceSecondCallTeamTwo(matchSetup) { - announceSecondCall(matchSetup, matchSetup.teams[1]); -} - -function announceSecondCallTabletoperator(matchSetup) { - const call = createFieldAnnouncement(matchSetup) + createSecondCallAnnouncement() + createTabletOperator(matchSetup); - announce([call]); -} - -function announceSecondCall(matchSetup, team) { - var secondCall = createSecondCallAnnouncement() + createSingleTeam(team.players); - var field = createFieldAnnouncement(matchSetup); - announce([secondCall, field]); -} -function announceBeginnToPlay(matchSetup, team) { - announce([createFieldAnnouncement(matchSetup) + "Bitte mit dem Spielen beginnen!"]); -} -function createSecondCallAnnouncement() { - return "Zweiter Aufruf fuer:"; -} - -function createTeamAnnouncement(matchSetup) { - var teams = createSingleTeam(matchSetup.teams[0].players) + ", gegen " + createSingleTeam(matchSetup.teams[1].players); - return teams; +function createTeamAnnouncement(matchSetup){ + var teams = createSingleTeam(matchSetup.teams[0].players)+", gegen "+createSingleTeam(matchSetup.teams[1].players); + return teams; } function createTabletOperator(matchSetup) { @@ -57,19 +32,19 @@ function createTabletOperator(matchSetup) { } else { tabletOperator = tabletOperator + "Verlierer des vorhergehenden Spiels"; } - + return tabletOperator; } -function createSingleTeam(playersSetup) { +function createSingleTeam(playersSetup){ var team = playersSetup[0].name; - if (playersSetup.length == 2) { - team = team + " und " + playersSetup[1].name + if (playersSetup.length == 2){ + team = team+" und "+playersSetup[1].name } return team; } -function createRoundAnnouncement(matchSetup) { +function createRoundAnnouncement(matchSetup){ var round = matchSetup.match_name; if (round == "R16") { round = "Achtelfinale"; @@ -94,26 +69,26 @@ function createRoundAnnouncement(matchSetup) { } return round; } -function createEventAnnouncement(matchSetup) { +function createEventAnnouncement(matchSetup){ var eventParts = matchSetup.event_name.split(" "); var eventName = ""; - if (eventParts[0] == 'JE') { + if (eventParts[0] == 'JE'){ eventName = "Jungeneinzel" - } else if (eventParts[0] == 'JD') { + }else if (eventParts[0] == 'JD') { eventName = "Jungendoppel" - } else if (eventParts[0] == 'ME') { - eventName = "Maedcheneinzel" - } else if (eventParts[0] == 'MD') { - eventName = "Maedchendoppel" + }else if (eventParts[0] == 'ME') { + eventName = "Mdcheneinzel" + }else if (eventParts[0] == 'MD') { + eventName = "Mdchendoppel" } else if (eventParts[0] == 'GD' || eventParts[0] == 'MX') { eventName = "Gemischtesdoppel" - } else if (eventParts[0] == 'HE') { + }else if (eventParts[0] == 'HE'){ eventName = "Herreneinzel" - } else if (eventParts[0] == 'HD') { + }else if (eventParts[0] == 'HD') { eventName = "Herrendoppel" - } else if (eventParts[0] == 'DE') { + }else if (eventParts[0] == 'DE') { eventName = "Dameneinzel" - } else if (eventParts[0] == 'DD') { + }else if (eventParts[0] == 'DD') { eventName = "Damendoppel" } if (eventName == "") { @@ -122,9 +97,9 @@ function createEventAnnouncement(matchSetup) { } else if (eventParts[1] == 'JD') { eventName = "Jungendoppel" } else if (eventParts[1] == 'ME') { - eventName = "Maedcheneinzel" + eventName = "Mdcheneinzel" } else if (eventParts[1] == 'MD') { - eventName = "Maedchendoppel" + eventName = "Mdchendoppel" } else if (eventParts[1] == 'GD' || eventParts[1] == 'MX') { eventName = "Gemischtesdoppel" } else if (eventParts[1] == 'HE') { @@ -136,30 +111,21 @@ function createEventAnnouncement(matchSetup) { } else if (eventParts[1] == 'DD') { eventName = "Damendoppel" } - if (eventParts[0]) { - eventName = eventName + " " + eventParts[0]; - } + eventName = eventName + " " + eventParts[0]; } else { - if (eventParts[1]) { - eventName = eventName + " " + eventParts[1]; - } + eventName = eventName + " " + eventParts[1]; } return eventName; } -function createMatchNumberAnnouncement(matchSetup) { +function createMatchNumberAnnouncement(matchSetup){ var number = matchSetup.match_num; return "Spiel Nummer " + number + "!"; } -function createFieldAnnouncement(matchSetup) { - if (matchSetup.court_id) { - var court = matchSetup.court_id.split("_")[1]; - return "Auf Spielfeld " + court + "!"; - } else { - return ""; - } - +function createFieldAnnouncement(matchSetup){ + var court = matchSetup.court_id.split("_")[1]; + return "Auf Spielfeld " + court + "!"; } function createPreparationAnnouncement() { @@ -170,18 +136,18 @@ function announce(callArray) { // Seems like the getVoices() is an asynchronous function where it is not always guaranteed that you get a // result immediately. The wait for the result must therefore be handled: // https://stackoverflow.com/questions/21513706/getting-the-list-of-voices-in-speechsynthesis-web-speech-api - const allVoicesObtained = new Promise(function (resolve, reject) { + const allVoicesObtained = new Promise(function(resolve, reject) { let voices = window.speechSynthesis.getVoices(); if (voices.length !== 0) { resolve(voices); } else { - window.speechSynthesis.addEventListener("voiceschanged", function () { + window.speechSynthesis.addEventListener("voiceschanged", function() { voices = window.speechSynthesis.getVoices(); resolve(voices); }); } }); - + allVoicesObtained.then(voices => { var voice = null; for (var i = 0; i < voices.length; i++) { @@ -199,5 +165,5 @@ function announce(callArray) { words.voice = voice; window.speechSynthesis.speak(words); }); - }); + }); } \ No newline at end of file diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 31e20f8..bbc9ddc 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -79,11 +79,6 @@ var ci18n_de = { 'to_stats:total': 'Total', 'match:rawinfo': 'Technische Informationen', -'match:preparationcall': 'Aufruf: Spiel in Vorbereitung', -'match:begintoplay': 'Aufruf: Mit dem Spielen beginnen', -'match:secondcallteamone': 'Aufruf: Zweiter Aufruf Team 1', -'match:secondcallteamtwo': 'Aufruf: Zweiter Aufruf Team 2', -'match:secondcaltabletoperator': 'Aufruf: Zweiter Aufruf Tabletbediener', 'match:scoresheet': 'Schiedsrichterzettel', 'match:edit': 'Bearbeiten', 'match:incomplete': '[Unvollständig!] ', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 2b64eef..c3a69df 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -79,11 +79,6 @@ var ci18n_en = { 'to_stats:total': 'Total', 'match:rawinfo': 'Technical information', -'match:preparationcall': 'Announcement: Match in preparation', -'match:begintoplay': 'Announcement: Beginn to play', -'match:secondcallteamone': 'Announcement: Second call Team 1', -'match:secondcallteamtwo': 'Announcement: Second call Team 2', -'match:secondcaltabletoperator': 'Announcement: Second call Tabletoperator', 'match:scoresheet': 'Scoresheet', 'match:edit': 'Edit', 'match:override_colors': 'Override colors', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index bbe5bce..33d8fa1 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -45,20 +45,24 @@ function render_match_row(tr, match, court, style, show_player_status) { if (!court && match.setup.court_id) { court = curt.courts_by_id[match.setup.court_id]; } - const activeMatch = court && !match.btp_winner; + const setup = match.setup; if (style === 'default' || style === 'plain') { const actions_td = uiu.el(tr, 'td'); - create_match_button(actions_td, 'vlink match_edit_button', 'match:edit', on_edit_button_click, match._id); - create_match_button(actions_td, 'vlink match_scoresheet_button', 'match:scoresheet', on_scoresheet_button_click, match._id); - if (!court) { - create_match_button(actions_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); + const edit_btn = uiu.el(actions_td, 'div', { + 'class': 'vlink match_edit_button', + 'data-match__id': match._id, + 'title': ci18n('match:edit'), + }); + edit_btn.addEventListener('click', on_edit_button_click); + + const scoresheet_btn = uiu.el(actions_td, 'div', { + 'class': 'vlink match_scoresheet_button', + 'title': ci18n('match:scoresheet'), + 'data-match__id': match._id, + }); + scoresheet_btn.addEventListener('click', on_scoresheet_button_click); - } else { - if (activeMatch) { - create_match_button(actions_td, 'vlink match_begin_to_play_button', 'match:begintoplay', on_begin_to_play_button_click, match._id); - } - } uiu.el(actions_td, 'a', { 'class': 'match_rawinfo', 'title': ci18n('match:rawinfo'), @@ -92,16 +96,10 @@ function render_match_row(tr, match, court, style, show_player_status) { 'class': ((match.team1_won === true) ? 'match_team_won' : ''), style: 'text-align: right;', }); - if (activeMatch) { - create_match_button(players0, 'vlink match_second_call_button', 'match:secondcallteamone', on_second_call_team_one_button_click, match._id); - } render_players_el(players0, setup, 0, show_player_status); uiu.el(tr, 'td', 'match_vs', 'v'); const players1 = uiu.el(tr, 'td', ((match.team1_won === false) ? 'match_team_won ' : '') + 'match_team2'); render_players_el(players1, setup, 1, show_player_status); - if (activeMatch) { - create_match_button(players1, 'vlink match_second_call_button', 'match:secondcallteamtwo', on_second_call_team_two_button_click, match._id); - } if (style === 'default' || style === 'plain') { const to_td = uiu.el(tr, 'td'); if (setup.umpire_name) { @@ -122,9 +120,6 @@ function render_match_row(tr, match, court, style, show_player_status) { uiu.el(to_td, 'div', 'no_umpire', ''); uiu.el(to_td, 'span', 'match_no_umpire', ci18n('No umpire')); } - if (activeMatch) { - create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); - } } if (style === 'default' || style === 'plain'/* || style === 'public' WIP */) { @@ -149,14 +144,7 @@ function render_match_row(tr, match, court, style, show_player_status) { }, match.shuttle_count || ''); } } -function create_match_button(targetEl, cssClass, title, listener, matchId,) { - const btn = uiu.el(targetEl, 'div', { - 'class': cssClass, - 'title': ci18n(title), - 'data-match__id': matchId, - }); - btn.addEventListener('click', listener); -} + function update_match_score(m) { uiu.qsEach('.match_score[data-match_id=' + JSON.stringify(m._id) + ']', function(score_el) { uiu.text(score_el, calc_score_str(m)); @@ -287,48 +275,7 @@ function on_scoresheet_button_click(e) { const match_id = btn.getAttribute('data-match__id'); ui_scoresheet(match_id); } -function on_announce_preparation_matchbutton_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - announcePreparationMatch(match.setup); - } -} -function on_second_call_team_one_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - announceSecondCallTeamOne(match.setup); - } -} -function on_second_call_team_two_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - announceSecondCallTeamTwo(match.setup); - } -} -function on_second_call_tabletoperator_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - announceSecondCallTabletoperator(match.setup); - } -} -function on_begin_to_play_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - announceBeginnToPlay(match.setup); - } -} -function fetchMatchFromEvent(e) { - const btn = e.target; - const match_id = btn.getAttribute('data-match__id'); - const match = utils.find(curt.matches, m => m._id === match_id); - if (!match) { - cerror.silent('Match ' + match_id + ' konnte nicht gefunden werden'); - return null; - } else { - return match; - } -} function _nation_team_name(nat0, nat1) { if (nat1 && nat0 && (nat0 != nat1)) { return countries.lookup(nat0) + ' / ' + countries.lookup(nat1); From e9cd93472f3ec7fc3234f829e4fb4fadda8be959 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 25 Feb 2024 18:18:10 +0100 Subject: [PATCH 013/224] Add further Calls to be executed using ui --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bf94a3e..99c66ee 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ config.json ticker_config.json mail.* +/.vs From 4726e9cd9954fe47192c64df55b4c15dc0adae67 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 25 Feb 2024 18:18:32 +0100 Subject: [PATCH 014/224] Add further Calls to be executed using ui --- static/css/cmatch.css | 44 +++++++++++++--- static/js/announcements.js | 100 +++++++++++++++++++++++++------------ static/js/ci18n_de.js | 5 ++ static/js/ci18n_en.js | 5 ++ static/js/cmatch.js | 83 ++++++++++++++++++++++++------ 5 files changed, 182 insertions(+), 55 deletions(-) diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 2875f14..e4e179e 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -10,14 +10,16 @@ .match_edit_button, .match_scoresheet_button, +.match_preparation_call_button, +.match_begin_to_play_button, +.match_second_call_button, .match_rawinfo { display: inline-block; - width: 1em; min-width: 1em; - height: 1em; min-height: 1em; background-size: 100%; - opacity: 0.5; + background-repeat: no-repeat; + background-position: center center; } .match_edit_button { background-image: url(../icons/edit.svg); @@ -26,6 +28,21 @@ margin: 0 0 0 0.3em; background-image: url(../icons/scoresheet.svg); } + +.match_preparation_call_button { + margin: 0 0 0 0.3em; + background-image: url(../icons/preperation.svg); +} + +.match_begin_to_play_button { + margin: 0 0 0 0.3em; + background-image: url(../icons/start.svg); +} + +.match_second_call_button { + margin: 0 0 0 0.3em; + background-image: url(../icons/second_call.svg); +} .match_rawinfo { margin: 0 0.3em 0 0.3em; background-image: url(../icons/rawinfo.svg); @@ -43,8 +60,11 @@ } .match_edit_button:hover, +.match_second_call_button:hover, +.match_preparation_call_button:hover, +.match_begin_to_play_button:hover, .match_scoresheet_button:hover { - opacity: 1; + opacity: 0.5; } .match_table { @@ -111,7 +131,9 @@ max-height: 100vh; } - +match_preparation_call_button, +match_begin_to_play_button, +match_second_call_button, .match_scoresheet_buttons { position: fixed; left: 0; @@ -133,6 +155,16 @@ min-width: 5em; } +.match_second_call_button > * { + display: block; +} +.match_second_call_button > button { + margin: 0 0 0 2vmin; + font-size: 5vmin; + padding: 0.1em 0.1em; + min-width: 5em; +} + .match_umpire_style_umpires { font-weight: bold; } @@ -229,8 +261,6 @@ } - - @media print { .toprow, .main, diff --git a/static/js/announcements.js b/static/js/announcements.js index 538b7aa..c184a86 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -1,5 +1,5 @@ - -function announceNewMatch(matchSetup){ + +function announceNewMatch(matchSetup) { var field = createFieldAnnouncement(matchSetup); var matchNumber = createMatchNumberAnnouncement(matchSetup); var eventName = createEventAnnouncement(matchSetup); @@ -17,10 +17,35 @@ function announcePreparationMatch(matchSetup) { var teams = createTeamAnnouncement(matchSetup); announce([preparation, matchNumber, eventName, round, teams]); } +function announceSecondCallTeamOne(matchSetup) { + announceSecondCall(matchSetup, matchSetup.teams[0]); +} + +function announceSecondCallTeamTwo(matchSetup) { + announceSecondCall(matchSetup, matchSetup.teams[1]); +} + +function announceSecondCallTabletoperator(matchSetup) { + const call = createFieldAnnouncement(matchSetup) + createSecondCallAnnouncement() + createTabletOperator(matchSetup); + announce([call]); +} + +function announceSecondCall(matchSetup, team) { + var secondCall = createSecondCallAnnouncement() + createSingleTeam(team.players); + var field = createFieldAnnouncement(matchSetup); + announce([secondCall, field]); +} +function announceBeginnToPlay(matchSetup, team) { + announce([createFieldAnnouncement(matchSetup) + "Bitte mit dem Spielen beginnen!"]); +} -function createTeamAnnouncement(matchSetup){ - var teams = createSingleTeam(matchSetup.teams[0].players)+", gegen "+createSingleTeam(matchSetup.teams[1].players); - return teams; +function createSecondCallAnnouncement() { + return "Zweiter Aufruf fuer:"; +} + +function createTeamAnnouncement(matchSetup) { + var teams = createSingleTeam(matchSetup.teams[0].players) + ", gegen " + createSingleTeam(matchSetup.teams[1].players); + return teams; } function createTabletOperator(matchSetup) { @@ -32,19 +57,19 @@ function createTabletOperator(matchSetup) { } else { tabletOperator = tabletOperator + "Verlierer des vorhergehenden Spiels"; } - + return tabletOperator; } -function createSingleTeam(playersSetup){ +function createSingleTeam(playersSetup) { var team = playersSetup[0].name; - if (playersSetup.length == 2){ - team = team+" und "+playersSetup[1].name + if (playersSetup.length == 2) { + team = team + " und " + playersSetup[1].name } return team; } -function createRoundAnnouncement(matchSetup){ +function createRoundAnnouncement(matchSetup) { var round = matchSetup.match_name; if (round == "R16") { round = "Achtelfinale"; @@ -69,26 +94,26 @@ function createRoundAnnouncement(matchSetup){ } return round; } -function createEventAnnouncement(matchSetup){ +function createEventAnnouncement(matchSetup) { var eventParts = matchSetup.event_name.split(" "); var eventName = ""; - if (eventParts[0] == 'JE'){ + if (eventParts[0] == 'JE') { eventName = "Jungeneinzel" - }else if (eventParts[0] == 'JD') { + } else if (eventParts[0] == 'JD') { eventName = "Jungendoppel" - }else if (eventParts[0] == 'ME') { - eventName = "Mdcheneinzel" - }else if (eventParts[0] == 'MD') { - eventName = "Mdchendoppel" + } else if (eventParts[0] == 'ME') { + eventName = "Maedcheneinzel" + } else if (eventParts[0] == 'MD') { + eventName = "Maedchendoppel" } else if (eventParts[0] == 'GD' || eventParts[0] == 'MX') { eventName = "Gemischtesdoppel" - }else if (eventParts[0] == 'HE'){ + } else if (eventParts[0] == 'HE') { eventName = "Herreneinzel" - }else if (eventParts[0] == 'HD') { + } else if (eventParts[0] == 'HD') { eventName = "Herrendoppel" - }else if (eventParts[0] == 'DE') { + } else if (eventParts[0] == 'DE') { eventName = "Dameneinzel" - }else if (eventParts[0] == 'DD') { + } else if (eventParts[0] == 'DD') { eventName = "Damendoppel" } if (eventName == "") { @@ -97,9 +122,9 @@ function createEventAnnouncement(matchSetup){ } else if (eventParts[1] == 'JD') { eventName = "Jungendoppel" } else if (eventParts[1] == 'ME') { - eventName = "Mdcheneinzel" + eventName = "Maedcheneinzel" } else if (eventParts[1] == 'MD') { - eventName = "Mdchendoppel" + eventName = "Maedchendoppel" } else if (eventParts[1] == 'GD' || eventParts[1] == 'MX') { eventName = "Gemischtesdoppel" } else if (eventParts[1] == 'HE') { @@ -111,21 +136,30 @@ function createEventAnnouncement(matchSetup){ } else if (eventParts[1] == 'DD') { eventName = "Damendoppel" } - eventName = eventName + " " + eventParts[0]; + if (eventParts[0]) { + eventName = eventName + " " + eventParts[0]; + } } else { - eventName = eventName + " " + eventParts[1]; + if (eventParts[1]) { + eventName = eventName + " " + eventParts[1]; + } } return eventName; } -function createMatchNumberAnnouncement(matchSetup){ +function createMatchNumberAnnouncement(matchSetup) { var number = matchSetup.match_num; return "Spiel Nummer " + number + "!"; } -function createFieldAnnouncement(matchSetup){ - var court = matchSetup.court_id.split("_")[1]; - return "Auf Spielfeld " + court + "!"; +function createFieldAnnouncement(matchSetup) { + if (matchSetup.court_id) { + var court = matchSetup.court_id.split("_")[1]; + return "Auf Spielfeld " + court + "!"; + } else { + return ""; + } + } function createPreparationAnnouncement() { @@ -136,18 +170,18 @@ function announce(callArray) { // Seems like the getVoices() is an asynchronous function where it is not always guaranteed that you get a // result immediately. The wait for the result must therefore be handled: // https://stackoverflow.com/questions/21513706/getting-the-list-of-voices-in-speechsynthesis-web-speech-api - const allVoicesObtained = new Promise(function(resolve, reject) { + const allVoicesObtained = new Promise(function (resolve, reject) { let voices = window.speechSynthesis.getVoices(); if (voices.length !== 0) { resolve(voices); } else { - window.speechSynthesis.addEventListener("voiceschanged", function() { + window.speechSynthesis.addEventListener("voiceschanged", function () { voices = window.speechSynthesis.getVoices(); resolve(voices); }); } }); - + allVoicesObtained.then(voices => { var voice = null; for (var i = 0; i < voices.length; i++) { @@ -165,5 +199,5 @@ function announce(callArray) { words.voice = voice; window.speechSynthesis.speak(words); }); - }); + }); } \ No newline at end of file diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index bbc9ddc..31e20f8 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -79,6 +79,11 @@ var ci18n_de = { 'to_stats:total': 'Total', 'match:rawinfo': 'Technische Informationen', +'match:preparationcall': 'Aufruf: Spiel in Vorbereitung', +'match:begintoplay': 'Aufruf: Mit dem Spielen beginnen', +'match:secondcallteamone': 'Aufruf: Zweiter Aufruf Team 1', +'match:secondcallteamtwo': 'Aufruf: Zweiter Aufruf Team 2', +'match:secondcaltabletoperator': 'Aufruf: Zweiter Aufruf Tabletbediener', 'match:scoresheet': 'Schiedsrichterzettel', 'match:edit': 'Bearbeiten', 'match:incomplete': '[Unvollständig!] ', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index c3a69df..2b64eef 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -79,6 +79,11 @@ var ci18n_en = { 'to_stats:total': 'Total', 'match:rawinfo': 'Technical information', +'match:preparationcall': 'Announcement: Match in preparation', +'match:begintoplay': 'Announcement: Beginn to play', +'match:secondcallteamone': 'Announcement: Second call Team 1', +'match:secondcallteamtwo': 'Announcement: Second call Team 2', +'match:secondcaltabletoperator': 'Announcement: Second call Tabletoperator', 'match:scoresheet': 'Scoresheet', 'match:edit': 'Edit', 'match:override_colors': 'Override colors', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 33d8fa1..bbe5bce 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -45,24 +45,20 @@ function render_match_row(tr, match, court, style, show_player_status) { if (!court && match.setup.court_id) { court = curt.courts_by_id[match.setup.court_id]; } - + const activeMatch = court && !match.btp_winner; const setup = match.setup; if (style === 'default' || style === 'plain') { const actions_td = uiu.el(tr, 'td'); - const edit_btn = uiu.el(actions_td, 'div', { - 'class': 'vlink match_edit_button', - 'data-match__id': match._id, - 'title': ci18n('match:edit'), - }); - edit_btn.addEventListener('click', on_edit_button_click); - - const scoresheet_btn = uiu.el(actions_td, 'div', { - 'class': 'vlink match_scoresheet_button', - 'title': ci18n('match:scoresheet'), - 'data-match__id': match._id, - }); - scoresheet_btn.addEventListener('click', on_scoresheet_button_click); + create_match_button(actions_td, 'vlink match_edit_button', 'match:edit', on_edit_button_click, match._id); + create_match_button(actions_td, 'vlink match_scoresheet_button', 'match:scoresheet', on_scoresheet_button_click, match._id); + if (!court) { + create_match_button(actions_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); + } else { + if (activeMatch) { + create_match_button(actions_td, 'vlink match_begin_to_play_button', 'match:begintoplay', on_begin_to_play_button_click, match._id); + } + } uiu.el(actions_td, 'a', { 'class': 'match_rawinfo', 'title': ci18n('match:rawinfo'), @@ -96,10 +92,16 @@ function render_match_row(tr, match, court, style, show_player_status) { 'class': ((match.team1_won === true) ? 'match_team_won' : ''), style: 'text-align: right;', }); + if (activeMatch) { + create_match_button(players0, 'vlink match_second_call_button', 'match:secondcallteamone', on_second_call_team_one_button_click, match._id); + } render_players_el(players0, setup, 0, show_player_status); uiu.el(tr, 'td', 'match_vs', 'v'); const players1 = uiu.el(tr, 'td', ((match.team1_won === false) ? 'match_team_won ' : '') + 'match_team2'); render_players_el(players1, setup, 1, show_player_status); + if (activeMatch) { + create_match_button(players1, 'vlink match_second_call_button', 'match:secondcallteamtwo', on_second_call_team_two_button_click, match._id); + } if (style === 'default' || style === 'plain') { const to_td = uiu.el(tr, 'td'); if (setup.umpire_name) { @@ -120,6 +122,9 @@ function render_match_row(tr, match, court, style, show_player_status) { uiu.el(to_td, 'div', 'no_umpire', ''); uiu.el(to_td, 'span', 'match_no_umpire', ci18n('No umpire')); } + if (activeMatch) { + create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); + } } if (style === 'default' || style === 'plain'/* || style === 'public' WIP */) { @@ -144,7 +149,14 @@ function render_match_row(tr, match, court, style, show_player_status) { }, match.shuttle_count || ''); } } - +function create_match_button(targetEl, cssClass, title, listener, matchId,) { + const btn = uiu.el(targetEl, 'div', { + 'class': cssClass, + 'title': ci18n(title), + 'data-match__id': matchId, + }); + btn.addEventListener('click', listener); +} function update_match_score(m) { uiu.qsEach('.match_score[data-match_id=' + JSON.stringify(m._id) + ']', function(score_el) { uiu.text(score_el, calc_score_str(m)); @@ -275,7 +287,48 @@ function on_scoresheet_button_click(e) { const match_id = btn.getAttribute('data-match__id'); ui_scoresheet(match_id); } +function on_announce_preparation_matchbutton_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + announcePreparationMatch(match.setup); + } +} +function on_second_call_team_one_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + announceSecondCallTeamOne(match.setup); + } +} +function on_second_call_team_two_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + announceSecondCallTeamTwo(match.setup); + } +} +function on_second_call_tabletoperator_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + announceSecondCallTabletoperator(match.setup); + } +} +function on_begin_to_play_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + announceBeginnToPlay(match.setup); + } +} +function fetchMatchFromEvent(e) { + const btn = e.target; + const match_id = btn.getAttribute('data-match__id'); + const match = utils.find(curt.matches, m => m._id === match_id); + if (!match) { + cerror.silent('Match ' + match_id + ' konnte nicht gefunden werden'); + return null; + } else { + return match; + } +} function _nation_team_name(nat0, nat1) { if (nat1 && nat0 && (nat0 != nat1)) { return countries.lookup(nat0) + ' / ' + countries.lookup(nat1); From 0d633d88e9af0b48dd5d7afba6ff1723305e3e95 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sun, 25 Feb 2024 20:28:46 +0100 Subject: [PATCH 015/224] make icons for preperation, second call and start match call a bit bigger --- static/css/cmatch.css | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/static/css/cmatch.css b/static/css/cmatch.css index e4e179e..0c2cf3a 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -30,17 +30,27 @@ } .match_preparation_call_button { - margin: 0 0 0 0.3em; + height: 1.2em; + min-height: 1em; + width: 1.4em; + margin: 0 0 -0.1em 0.3em; background-image: url(../icons/preperation.svg); } .match_begin_to_play_button { - margin: 0 0 0 0.3em; + height: 1.2em; + min-height: 1em; + width: 1.4em; + margin: 0 0 -0.1em 0.3em; background-image: url(../icons/start.svg); } .match_second_call_button { - margin: 0 0 0 0.3em; + height: 1.2em; + min-height: 1em; + background-size: cover; + width: 1.9em; + margin: 0 0.2em -0.2em 0.2em; background-image: url(../icons/second_call.svg); } .match_rawinfo { From 05b436d4d35d7d701c7ce7198fc447e00da6ab09 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sun, 25 Feb 2024 20:40:01 +0100 Subject: [PATCH 016/224] add Support for checking in players per match --- bts/btp_parse.js | 3 +++ bts/btp_sync.js | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/bts/btp_parse.js b/bts/btp_parse.js index 60cbd85..156e91e 100644 --- a/bts/btp_parse.js +++ b/bts/btp_parse.js @@ -173,6 +173,7 @@ function get_btp_state(response) { const all_btp_draws = btp_t.Draws ? btp_t.Draws[0].Draw : []; const all_btp_officials = btp_t.Officials ? btp_t.Officials[0].Official : []; const all_btp_courts = btp_t.Courts ? btp_t.Courts[0].Court : []; + const all_btp_settings = btp_t.Settings ? btp_t.Settings[0].Setting : []; const on_court_match_ids = new Set(); for (const c of all_btp_courts) { @@ -204,6 +205,7 @@ function get_btp_state(response) { const players = utils.make_index(all_btp_players, p => p.ID[0]); const officials = utils.make_index(all_btp_officials, o => o.ID[0]); const courts = utils.make_index(all_btp_courts, c => c.ID[0]); + const btp_settings = utils.make_index(all_btp_settings, s =>s.ID[0]); for (const bm of matches) { _calc_match_players(matches_by_pid, entries, players, bm, is_league); @@ -220,6 +222,7 @@ function get_btp_state(response) { teams, // Testing only _matches_by_pid: matches_by_pid, + btp_settings }; } diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 736df3c..18c8d55 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -69,6 +69,17 @@ function craft_match(app, tkey, btp_id, court_map, event, draw, officials, bm, m if (tournament.warmup_start) { setup.warmup_start = tournament.warmup_start; } + if (tournament.btp_settings.check_in_per_match && teams.length > 1) { + teams[0].players[0].checked_in = (bm.Status & 0b0001) > 0; + if(teams[0].players.length > 1) { + teams[0].players[1].checked_in = (bm.Status & 0b0010) > 0; + } + teams[1].players[0].checked_in = (bm.Status & 0b0100) > 0; + if(teams[1].players.length > 1) { + teams[1].players[1].checked_in = (bm.Status & 0b1000) > 0; + } + } + }); if (scheduled_time_str) { @@ -419,6 +430,21 @@ function integrate_courts(app, tournament_key, btp_state, callback) { }); } +function integrate_btp_settings(app, tkey, btp_state, callback) { + let btp_settings = {}; + + btp_settings.check_in_per_match = btp_state.btp_settings.get(1003).Value[0] ? false : true; + btp_settings.pause_duration_ms = btp_state.btp_settings.get(1303).Value[0] * 60 * 1000; + + app.db.tournaments.update({key: tkey}, {$set: {btp_settings}}, {}, (err) => { + if (err) { + return callback(err); + } + + return callback(null); + }); +} + function integrate_umpires(app, tournament_key, btp_state, callback) { const admin = require('./admin'); // avoid dependency cycle const stournament = require('./stournament'); // avoid dependency cycle @@ -713,6 +739,7 @@ function fetch(app, tkey, response, callback) { } async.waterfall([ + cb => integrate_btp_settings(app, tkey, btp_state, cb), cb => integrate_umpires(app, tkey, btp_state, cb), cb => integrate_courts(app, tkey, btp_state, cb), (court_map, cb) => integrate_matches(app, tkey, btp_state, court_map, cb), From 96d105bb432bf2289d549beb8ae37102640876a2 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 27 Feb 2024 02:31:33 +0100 Subject: [PATCH 017/224] improve checkIn of the players and tabletoperators status after end of the match --- bts/btp_conn.js | 28 +++++++++++++++++ bts/btp_manager.js | 17 ++++++++++ bts/btp_proto.js | 55 ++++++++++++++++++++++++++++++++ bts/btp_sync.js | 76 +++++++++++++++++++++++++++------------------ bts/http_api.js | 36 +++++++++++++++------ static/js/cmatch.js | 4 +-- 6 files changed, 174 insertions(+), 42 deletions(-) diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 7175a03..de3b142 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -292,6 +292,34 @@ class BTPConn { }); } + + + update_players(players) { + if (this.readonly) { + return; + } + + if (!players || players.length < 1) { + return; + } + + if (! this.key_unicode) { + serror.silent('Trying to send match data, but never logged in. Must retry later'); + return; + } + + const req = btp_proto.update_players_request( + this.key_unicode, this.password, players); + this.send(req, response => { + const results = response.Action[0].Result; + const rescode = results ? results[0] : 'no-result'; + if (rescode === 1) { + + } else { + serror.silent('Update Player failed with error code ' + rescode); + } + }); + } } module.exports = { diff --git a/bts/btp_manager.js b/bts/btp_manager.js index 7103a40..10dad01 100644 --- a/bts/btp_manager.js +++ b/bts/btp_manager.js @@ -58,6 +58,22 @@ function update_score(app, match) { conn.update_score(match); } +function update_players(app, tkey, players) { + assert(tkey); + + if (!players || players.length < 1) { + return; + } + + const conn = conns_by_tkey.get(tkey); + if (!conn) { + // Do not output an error; this happens if BTP support gets disabled + return; + } + + conn.update_players(players); +} + function init(app, cb) { app.db.tournaments.find({}, (err, tournaments) => { if (err) return cb(err); @@ -84,4 +100,5 @@ module.exports = { init, reconfigure, update_score, + update_players, }; \ No newline at end of file diff --git a/bts/btp_proto.js b/bts/btp_proto.js index 034b2d3..a88ba5f 100644 --- a/bts/btp_proto.js +++ b/bts/btp_proto.js @@ -143,11 +143,65 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud const pupdate = { ID: pid, LastTimeOnCourt: end_date, + CheckedIn: true, }; players.push({Player: pupdate}); } + + if(match.setup.tabletoperators && match.setup.tabletoperators.length > 0) { + for (const operator of match.setup.tabletoperators) { + const pupdate = { + ID: operator.btp_id, + CheckedIn: true, + }; + + players.push({Player: pupdate}); + } + } } + return res; +} +function update_players_request(key_unicode, password, players) { + assert(key_unicode); + const res = { + Header: { + Version: { + Hi: 1, + Lo: 1, + }, + }, + Action: { + ID: 'SENDUPDATE', + Unicode: key_unicode, + }, + Client: { + IP: 'bts', + }, + Update: { + Tournament: { + }, + }, + }; + if (password) { + res.Action.Password = password; + } + + const btp_players = []; + res.Update.Tournament.Players = btp_players; + + players.forEach((player) => { + if (player.btp_id && player.last_time_on_court_ts && (player.last_time_on_court_ts + 300000 > Date.now())) { + const end_date = new Date(player.last_time_on_court_ts); + + const pupdate = { + ID: player.btp_id, + LastTimeOnCourt: end_date, + CheckedIn: player.checked_in, + }; + btp_players.push({Player: pupdate}); + } + }); return res; } @@ -332,6 +386,7 @@ module.exports = { get_info_request, login_request, update_request, + update_players_request, // Tests only _req2xml: req2xml, }; \ No newline at end of file diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 18c8d55..e404b39 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -298,11 +298,6 @@ function integrate_matches(app, tkey, btp_state, court_map, callback) { match.setup.tabletoperators = cur_match.setup.tabletoperators; } - if (cur_match.setup.tabletoperators_btp_ids) { - // tabletoperators_btp_ids is not from btp so we have to coppy it to the match generated by btp. - match.setup.tabletoperators_btp_ids = cur_match.setup.tabletoperators_btp_ids; - } - if (cur_match.setup.teams[0].players[0].now_playing_on_court) { match.setup.teams[0].players[0].now_playing_on_court = cur_match.setup.teams[0].players[0].now_playing_on_court; } @@ -509,6 +504,7 @@ function calculate_match_ids_on_court(btp_state) { async function integrate_now_on_court(app, tkey, callback) { const admin = require('./admin'); // avoid dependency cycle const stournament = require('./stournament'); // avoid dependency cycle + const btp_manager = require('./btp_manager'); // TODO after switching to async, this should happen during court&match construction app.db.tournaments.findOne({key: tkey}, async (err, tournament) => { @@ -523,12 +519,15 @@ async function integrate_now_on_court(app, tkey, callback) { app.db.matches.find({'setup.now_on_court': true}, async (err, now_on_court_matches) => { if (err) return callback(err); - async.each(now_on_court_matches, async (match, cb) => { + await Promise.all(now_on_court_matches.map(async (match) => { + const court_id = match.setup.court_id; const match_id = match._id; const called_timestamp = Date.now(); - if (!court_id || !match_id) return cb(); // TODO in async we would assert both to be true + if (!court_id || !match_id) { + return; // TODO in async we would assert both to be true + } const setup = match.setup; if(!setup.called_timestamp) { @@ -540,37 +539,53 @@ async function integrate_now_on_court(app, tkey, callback) { callback(err) } + if (setup.tabletoperators) { + for (let operator of setup.tabletoperators) { + operator.checked_in = false; + operator.last_time_on_court_ts = called_timestamp; + } + } + + btp_manager.update_players(app, tkey, setup.tabletoperators); + const match_q = {_id: match_id}; app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { - if (err) return cb(err); + if (err) { + console.error(err); + return; + } const court_q = {_id: court_id}; app.db.courts.find(court_q, (err, courts) => { - if (err) return cb(err); - if (courts.length !== 1) return cb(); - const [court] = courts; + if (err) { + console.error(err); + return; + } + if (courts.length !== 1) return; app.db.courts.update(court_q, {$set: {match_id}}, {}, (err) => { - if (err) { - return cb(err); - } + if (err) { + console.error(err); + return; + } - admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); - admin.notify_change(app, tkey, 'match_called_on_court', match); + admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + admin.notify_change(app, tkey, 'match_called_on_court', match); - async.waterfall([ wcb => set_player_on_court(app, tkey, match.setup, wcb), - wcb => set_player_on_tablet(app, tkey, match.setup, wcb) - ], function(err) { - if (err) { - callback(err); - } - console.log("No error!") - }); + async.waterfall([ wcb => set_player_on_court(app, tkey, match.setup, wcb), + wcb => set_player_on_tablet(app, tkey, match.setup, wcb)], + (err) => { + if (err) { + console.error(err); + return; + } + }); }); }); }); - } - }, callback); + } + })); + callback(null); }); }); // TODO clear courts (better in async) @@ -623,7 +638,7 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { } const match_id = match._id; - let tablet_operatorns_btp_ids = [ match_on_court_setup.tabletoperators[0].btp_id]; + let tablet_operatorns_btp_ids = [match_on_court_setup.tabletoperators[0].btp_id]; if(match_on_court_setup.tabletoperators.length > 1) { tablet_operatorns_btp_ids.push(match_on_court_setup.tabletoperators[1].btp_id); @@ -633,21 +648,25 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { if (tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { match.setup.teams[0].players[0].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[0].checked_in = false; change = true; } if (match.setup.teams[0].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { match.setup.teams[0].players[1].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[1].checked_in = false; change = true; } if (tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { match.setup.teams[1].players[0].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[0].checked_in = false; change = true; } if (match.setup.teams[1].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { match.setup.teams[1].players[1].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[1].checked_in = false; change = true; } @@ -667,8 +686,6 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { function set_player_on_court (app, tkey, match_on_court_setup, callback) { - console.log("Suche Player"); - const admin = require('./admin'); // avoid dependency cycle app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { if (err) { @@ -724,7 +741,6 @@ function set_player_on_court (app, tkey, match_on_court_setup, callback) { } }); - console.log("vor dem return"); callback(null); }); } diff --git a/bts/http_api.js b/bts/http_api.js index 590c82b..676a74b 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -362,9 +362,9 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { return callback(err); } - async.each(matches, async (match, cb) => { - if(match.setup.now_on_court == false) { - return; + async.each(matches, (match, cb) => { + if(match.setup.now_on_court == true) { + return cb(null); } const match_id = match._id; @@ -408,29 +408,38 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { if (err) return cb(err); admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + return cb(null); }); - } - }); + } else { + return cb(null); + } + }, callback); }); }); } -function remove_tablet_on_court (app, tkey, cur_match_id, callback) { +function remove_tablet_on_court (app, tkey, cur_match_id, callback) { const admin = require('./admin'); // avoid dependency cycle app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { if (err) return callback(err); app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { if (err) { + console.error(err); return callback(err); } - async.each(matches, async (match, cb) => { - if(match.setup.now_on_court == false) { - return; + async.each(matches, (match, cb) => { + + if(match.setup.now_on_court == true) { + return cb(null); } + if(!cur_match.setup.tabletoperators || cur_match.setup.tabletoperators == 0) { + return cb(null); + } + const match_id = match._id; let remove_btp_ids = [ cur_match.setup.tabletoperators[0].btp_id]; @@ -443,21 +452,25 @@ function remove_tablet_on_court (app, tkey, cur_match_id, callback) { if (remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { match.setup.teams[0].players[0].now_tablet_on_court = false; + match.setup.teams[0].players[0].checked_in = true; change = true; } if (match.setup.teams[0].players.length > 1 && remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { match.setup.teams[0].players[1].now_tablet_on_court = false; + match.setup.teams[0].players[1].checked_in = true; change = true; } if (remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { match.setup.teams[1].players[0].now_tablet_on_court = false; + match.setup.teams[1].players[0].checked_in = true; change = true; } if (match.setup.teams[1].players.length > 1 && remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { match.setup.teams[1].players[1].now_tablet_on_court = false; + match.setup.teams[1].players[1].checked_in = true; change = true; } @@ -468,9 +481,12 @@ function remove_tablet_on_court (app, tkey, cur_match_id, callback) { if (err) return cb(err); admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + return cb(null); }); + } else { + return cb(null); } - }); + }, callback); }); }); } diff --git a/static/js/cmatch.js b/static/js/cmatch.js index bbe5bce..c5cf9fe 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -181,7 +181,7 @@ function render_players_el(parentNode, setup, team_id, show_player_status) { let player0_status = ""; if (!show_player_status || setup.now_on_court) { player0_status = "now_on_court"; - } else if (team.players[0].now_playing_on_court || team.players[0].now_tablet_on_court) { + } else if (team.players[0].now_playing_on_court) { player0_status = "now_playing"; } else if (team.players[0].checked_in) { player0_status = "checked_in"; @@ -216,7 +216,7 @@ function render_players_el(parentNode, setup, team_id, show_player_status) { let player1_status = ""; if (!show_player_status || setup.now_on_court) { player1_status = "now_on_court"; - } else if (team.players[1].now_playing_on_court || team.players[1].now_tablet_on_court) { + } else if (team.players[1].now_playing_on_court) { player1_status = "now_playing"; } else if (team.players[1].checked_in) { player1_status = "checked_in"; From 0c0f20b8d896734d159371665c769769d2c9e598 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 7 Mar 2024 23:13:05 +0100 Subject: [PATCH 018/224] add timer for warmup and pause in admin display. also did not so often rerender the admin screen. --- bts/admin.js | 2 +- bts/btp_sync.js | 90 +++++++++---- bts/http_api.js | 73 ++++++---- static/css/cmatch.css | 25 +++- static/js/cmatch.js | 284 ++++++++++++++++++++++++++++++--------- static/js/ctournament.js | 20 ++- 6 files changed, 371 insertions(+), 123 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index a734ac5..90c4701 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -236,7 +236,7 @@ function handle_match_edit(app, ws, msg) { return; } - notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, setup}); + notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, setup, from: "admin.js:239"}); if (msg.btp_update) { btp_manager.update_score(app, changed_match); } diff --git a/bts/btp_sync.js b/bts/btp_sync.js index e404b39..966a0df 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -281,7 +281,8 @@ function integrate_matches(app, tkey, btp_state, court_map, callback) { return; } - const match = craft_match(app, tkey, btp_id, court_map, event, draw, officials, bm, match_ids_on_court); + let match = craft_match(app, tkey, btp_id, court_map, event, draw, officials, bm, match_ids_on_court); + if (!match) { cb(); return; @@ -298,36 +299,45 @@ function integrate_matches(app, tkey, btp_state, court_map, callback) { match.setup.tabletoperators = cur_match.setup.tabletoperators; } - if (cur_match.setup.teams[0].players[0].now_playing_on_court) { - match.setup.teams[0].players[0].now_playing_on_court = cur_match.setup.teams[0].players[0].now_playing_on_court; - } + for(let team_index = 0; team_index < cur_match.setup.teams.length; team_index++) { + for (let player_index = 0; player_index < cur_match.setup.teams[team_index].players.length; player_index++){ + + if (cur_match.setup.teams[team_index].players[player_index].now_playing_on_court != undefined) { + match.setup.teams[team_index].players[player_index].now_playing_on_court = cur_match.setup.teams[team_index].players[player_index].now_playing_on_court; + } - if (cur_match.setup.teams[0].players.length > 1 && cur_match.setup.teams[0].players[1].now_playing_on_court) { - match.setup.teams[0].players[1].now_playing_on_court = cur_match.setup.teams[0].players[1].now_playing_on_court; - } + if (cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court != undefined) { + match.setup.teams[team_index].players[player_index].now_tablet_on_court = cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court; + } - if (cur_match.setup.teams[1].players[0].now_playing_on_court) { - match.setup.teams[1].players[0].now_playing_on_court = cur_match.setup.teams[1].players[0].now_playing_on_court; - } + if(cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts || match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { + if(!cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { + cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; + } - if (cur_match.setup.teams[1].players.length > 1 && cur_match.setup.teams[1].players[1].now_playing_on_court) { - match.setup.teams[1].players[1].now_playing_on_court = cur_match.setup.teams[1].players[1].now_playing_on_court; - } + if(!match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { + match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; + } + + let max_ts = Math.max( cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts, + match.setup.teams[team_index].players[player_index].last_time_on_court_ts); - if (cur_match.setup.teams[0].players[0].now_tablet_on_court) { - match.setup.teams[0].players[0].now_tablet_on_court = cur_match.setup.teams[0].players[0].now_tablet_on_court; + cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; + match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; + } + } } - if (cur_match.setup.teams[0].players.length > 1 && cur_match.setup.teams[0].players[1].now_tablet_on_court) { - match.setup.teams[0].players[1].now_tablet_on_court = cur_match.setup.teams[0].players[1].now_tablet_on_court; + if (cur_match.setup.warmup) { + match.setup.warmup = cur_match.setup.warmup; } - if (cur_match.setup.teams[1].players[0].now_tablet_on_court) { - match.setup.teams[1].players[0].now_tablet_on_court = cur_match.setup.teams[1].players[0].now_tablet_on_court; + if (cur_match.setup.warmup_ready) { + match.setup.warmup_ready = cur_match.setup.warmup_ready; } - if (cur_match.setup.teams[1].players.length > 1 && cur_match.setup.teams[1].players[1].now_tablet_on_court) { - match.setup.teams[1].players[1].now_tablet_on_court = cur_match.setup.teams[1].players[1].now_tablet_on_court; + if(cur_match.setup.warmup_start) { + match.setup.warmup_start = cur_match.setup.warmup_start; } if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { @@ -336,10 +346,33 @@ function integrate_matches(app, tkey, btp_state, court_map, callback) { return; } + // equals checked_in changed and check if it was the only change + let only_change_check_in = false; + + for(let team_index = 0; team_index < cur_match.setup.teams.length; team_index++) { + for (let player_index = 0; player_index < cur_match.setup.teams[team_index].players.length; player_index++){ + cur_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; + } + } + + if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { + only_change_check_in = true; + } + app.db.matches.update({_id: cur_match._id}, {$set: match}, {}, (err) => { if (err) return cb(err); - admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + if(!only_change_check_in) { + admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup, + from: "btp_sync.js:423"}); + } else { + admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup, + from: "btp_sync.js:429"}); + } cb(); }); return; @@ -569,7 +602,10 @@ async function integrate_now_on_court(app, tkey, callback) { return; } - admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup, + from: "btp_sync.js:664"}); admin.notify_change(app, tkey, 'match_called_on_court', match); async.waterfall([ wcb => set_player_on_court(app, tkey, match.setup, wcb), @@ -675,7 +711,9 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { const match_q = {_id: match_id}; app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { if (err) return callback(err); - admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup}); }); } }); @@ -736,7 +774,9 @@ function set_player_on_court (app, tkey, match_on_court_setup, callback) { app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { if (err) return callback(err); - admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup}); }); } }); diff --git a/bts/http_api.js b/bts/http_api.js index 676a74b..f78873a 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -331,6 +331,17 @@ function score_handler(req, res) { ticker_manager.update_score(req.app, match); } + cb(null, match); + }, + (match, cb) => { + admin.notify_change(req.app, tournament_key, 'score', { + match_id, + network_score: update.network_score, + team1_won: update.team1_won, + shuttle_count: update.shuttle_count, + presses: match.presses, + }); + cb(); }, ], function(err) { @@ -342,12 +353,6 @@ function score_handler(req, res) { return; } - admin.notify_change(req.app, tournament_key, 'score', { - match_id, - network_score: update.network_score, - team1_won: update.team1_won, - shuttle_count: update.shuttle_count, - }); res.json({status: 'ok'}); }); } @@ -381,24 +386,30 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { let change = false; - if (remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { - match.setup.teams[0].players[0].now_playing_on_court = false; - change = true; + if (remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && + match.setup.teams[0].players[0].now_playing_on_court) { + match.setup.teams[0].players[0].now_playing_on_court = false; + change = true; } - if (match.setup.teams[0].players.length > 1 && remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { - match.setup.teams[0].players[1].now_playing_on_court = false; - change = true; + if (match.setup.teams[0].players.length > 1 && + remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id) && + match.setup.teams[0].players[1].now_playing_on_court) { + match.setup.teams[0].players[1].now_playing_on_court = false; + change = true; } - if (remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { - match.setup.teams[1].players[0].now_playing_on_court = false; - change = true; + if (remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && + match.setup.teams[1].players[0].now_playing_on_court) { + match.setup.teams[1].players[0].now_playing_on_court = false; + change = true; } - if (match.setup.teams[1].players.length > 1 && remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { - match.setup.teams[1].players[1].now_playing_on_court = false; - change = true; + if (match.setup.teams[1].players.length > 1 && + remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id) && + match.setup.teams[1].players[1].now_playing_on_court) { + match.setup.teams[1].players[1].now_playing_on_court = false; + change = true; } if (change) { @@ -407,7 +418,9 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { if (err) return cb(err); - admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup}); return cb(null); }); } else { @@ -450,25 +463,35 @@ function remove_tablet_on_court (app, tkey, cur_match_id, callback) { let change = false; - if (remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + if (remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && + ( match.setup.teams[0].players[0].now_tablet_on_court == true || + match.setup.teams[0].players[0].checked_in == false)) { match.setup.teams[0].players[0].now_tablet_on_court = false; match.setup.teams[0].players[0].checked_in = true; change = true; } - if (match.setup.teams[0].players.length > 1 && remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + if (match.setup.teams[0].players.length > 1 && + remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id) && + ( match.setup.teams[0].players[1].now_tablet_on_court == true || + match.setup.teams[0].players[1].checked_in == false)) { match.setup.teams[0].players[1].now_tablet_on_court = false; match.setup.teams[0].players[1].checked_in = true; change = true; } - if (remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + if (remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && + ( match.setup.teams[1].players[0].now_tablet_on_court == true || + match.setup.teams[1].players[0].checked_in == false)) { match.setup.teams[1].players[0].now_tablet_on_court = false; match.setup.teams[1].players[0].checked_in = true; change = true; } - if (match.setup.teams[1].players.length > 1 && remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + if (match.setup.teams[1].players.length > 1 && + remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id) && + ( match.setup.teams[1].players[1].now_tablet_on_court == true || + match.setup.teams[1].players[1].checked_in == false)) { match.setup.teams[1].players[1].now_tablet_on_court = false; match.setup.teams[1].players[1].checked_in = true; change = true; @@ -480,7 +503,9 @@ function remove_tablet_on_court (app, tkey, cur_match_id, callback) { app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { if (err) return cb(err); - admin.notify_change(app, match.tournament_key, 'match_edit', {match__id: match._id, setup: match.setup}); + admin.notify_change(app, match.tournament_key, 'update_player_status', { match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup}); return cb(null); }); } else { diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 0c2cf3a..81060bf 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -38,9 +38,9 @@ } .match_begin_to_play_button { - height: 1.2em; + height: 1.5em; min-height: 1em; - width: 1.4em; + width: 2.3em; margin: 0 0 -0.1em 0.3em; background-image: url(../icons/start.svg); } @@ -96,7 +96,7 @@ color: #666; } .match_team2 { - padding-right: 1em; + padding-right: 0.3em; } .match_vs { color: #999; @@ -105,13 +105,30 @@ .match_team_won { font-weight: bold; } +.match_score{ + font-weight: bold; + padding-left: 0.2em; +} .match_score_current { font-weight: bold; + font-size: 1.5em; + padding-left: 0.3em; } + +.match_timer > div { + font-size: 1.2em; + font-weight: 900; + background-color: #111111; + min-width: 2.3em; + text-align: right; + padding: 0.1em 0.3em 0.1em 0.3em; + border-radius: 7px; +} + .match_shuttle_count { white-space: pre; text-align: right; - padding-left: 1em; + padding-left: 0.4em; } .match_shuttle_count span::after { content: ''; diff --git a/static/js/cmatch.js b/static/js/cmatch.js index c5cf9fe..d87ab9b 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -45,20 +45,19 @@ function render_match_row(tr, match, court, style, show_player_status) { if (!court && match.setup.court_id) { court = curt.courts_by_id[match.setup.court_id]; } - const activeMatch = court && !match.btp_winner; + + const waitForMatchStart = match.setup.called_timestamp && + ( match.network_score == undefined || + ( match.network_score[0] && + (match.network_score[0][0] + match.network_score[0][1] < 1) + ) + ); + const activeMatch = court && match.btp_winner != undefined; const setup = match.setup; if (style === 'default' || style === 'plain') { const actions_td = uiu.el(tr, 'td'); create_match_button(actions_td, 'vlink match_edit_button', 'match:edit', on_edit_button_click, match._id); create_match_button(actions_td, 'vlink match_scoresheet_button', 'match:scoresheet', on_scoresheet_button_click, match._id); - if (!court) { - create_match_button(actions_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); - - } else { - if (activeMatch) { - create_match_button(actions_td, 'vlink match_begin_to_play_button', 'match:begintoplay', on_begin_to_play_button_click, match._id); - } - } uiu.el(actions_td, 'a', { 'class': 'match_rawinfo', 'title': ci18n('match:rawinfo'), @@ -92,16 +91,13 @@ function render_match_row(tr, match, court, style, show_player_status) { 'class': ((match.team1_won === true) ? 'match_team_won' : ''), style: 'text-align: right;', }); - if (activeMatch) { - create_match_button(players0, 'vlink match_second_call_button', 'match:secondcallteamone', on_second_call_team_one_button_click, match._id); - } - render_players_el(players0, setup, 0, show_player_status); + + create_match_button(players0, 'vlink match_second_call_button', 'match:secondcallteamone', on_second_call_team_one_button_click, match._id); + render_players_el(players0, setup, 0, match._id ,show_player_status); uiu.el(tr, 'td', 'match_vs', 'v'); const players1 = uiu.el(tr, 'td', ((match.team1_won === false) ? 'match_team_won ' : '') + 'match_team2'); - render_players_el(players1, setup, 1, show_player_status); - if (activeMatch) { - create_match_button(players1, 'vlink match_second_call_button', 'match:secondcallteamtwo', on_second_call_team_two_button_click, match._id); - } + render_players_el(players1, setup, 1, match._id, show_player_status); + create_match_button(players1, 'vlink match_second_call_button', 'match:secondcallteamtwo', on_second_call_team_two_button_click, match._id); if (style === 'default' || style === 'plain') { const to_td = uiu.el(tr, 'td'); if (setup.umpire_name) { @@ -122,9 +118,9 @@ function render_match_row(tr, match, court, style, show_player_status) { uiu.el(to_td, 'div', 'no_umpire', ''); uiu.el(to_td, 'span', 'match_no_umpire', ci18n('No umpire')); } - if (activeMatch) { - create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); - } + + create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); + } if (style === 'default' || style === 'plain'/* || style === 'public' WIP */) { @@ -134,11 +130,12 @@ function render_match_row(tr, match, court, style, show_player_status) { uiu.el(score_td, 'span', {}, ready_text); } uiu.el(score_td, 'span', { - 'class': ('match_score' + ((court && (court.match_id === match._id)) ? ' match_score_current' : '')), + 'class': ('match_score' + ((match.setup.now_on_court === true) ? ' match_score_current' : '')), 'data-match_id': match._id, }, calc_score_str(match)); } - if (style === 'default' || style === 'plain') { + + if ((style === 'default' || style === 'plain') && match.setup.now_on_court != undefined) { const shuttle_td = uiu.el(tr, 'td', 'match_shuttle_count'); uiu.el(shuttle_td, 'span', { 'class': ( @@ -148,12 +145,50 @@ function render_match_row(tr, match, court, style, show_player_status) { 'data-match_id': match._id, }, match.shuttle_count || ''); } + + if (style === 'default' || style === 'plain') { + const timer_td = uiu.el(tr, 'td', {'class': 'match_timer', 'data-match_id': match._id}); + + var timer_state = _extract_match_timer_state(match); + var timer = create_timer(timer_state, timer_td, "#cccccc", "#ff0000"); + if (timer) { + active_timers.matches[match._id] = timer; + } + } + + if (style === 'default' || style === 'plain') { + const call_td = uiu.el(tr, 'td', 'call_td'); + + if (!court) { + create_match_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); + + } else { + create_match_button(call_td, 'vlink match_begin_to_play_button', 'match:begintoplay', on_begin_to_play_button_click, match._id); + } + } + + if(!waitForMatchStart) { + uiu.qsEach('.match_second_call_button[data-match_id=' + JSON.stringify(match._id) + ']', (button_el) => { + if(match.setup.now_on_court) { + button_el.style.visibility = 'hidden'; + } else { + uiu.hide(button_el); + } + }); + uiu.qsEach('.match_begin_to_play_button[data-match_id=' + JSON.stringify(match._id) + ']', (button_el) => { + if(match.setup.now_on_court) { + button_el.style.visibility = 'hidden'; + } else { + uiu.hide(button_el); + } + }); + } } function create_match_button(targetEl, cssClass, title, listener, matchId,) { const btn = uiu.el(targetEl, 'div', { 'class': cssClass, 'title': ci18n(title), - 'data-match__id': matchId, + 'data-match_id': matchId, }); btn.addEventListener('click', listener); } @@ -161,13 +196,45 @@ function update_match_score(m) { uiu.qsEach('.match_score[data-match_id=' + JSON.stringify(m._id) + ']', function(score_el) { uiu.text(score_el, calc_score_str(m)); }); + + uiu.qsEach('.match_timer[data-match_id=' + JSON.stringify(m._id) + ']', (timer_td) => { + while (timer_td.firstChild) { + timer_td.removeChild(timer_td.lastChild); + } + + var timer_state = _extract_match_timer_state(m); + var timer = create_timer(timer_state, timer_td, "#cccccc", "#ff0000"); + if (timer) { + active_timers.matches[m._id] = timer; + } + }); + + if( m.network_score && m.network_score.length > 0 && + m.network_score[0].length > 1 && + (m.network_score[0][0] > 0 || m.network_score[0][1] > 0) ) { + uiu.qsEach('.match_second_call_button[data-match_id=' + JSON.stringify(m._id) + ']', (button_el) => { + button_el.style.visibility = 'hidden'; + }); + uiu.qsEach('.match_begin_to_play_button[data-match_id=' + JSON.stringify(m._id) + ']', (button_el) => { + button_el.style.visibility = 'hidden'; + }); + } else { + uiu.qsEach('.match_second_call_button[data-match_id=' + JSON.stringify(m._id) + ']', (button_el) => { + button_el.style.visibility = 'hidden'; + }); + uiu.qsEach('.match_begin_to_play_button[data-match_id=' + JSON.stringify(m._id) + ']', (button_el) => { + button_el.style.visibility = 'hidden'; + }); + } + + uiu.qsEach('.match_shuttle_count_display[data-match_id=' + JSON.stringify(m._id) + ']', function(el) { uiu.text(el, m.shuttle_count || ''); uiu.setClass(el, 'match_shuttle_count_display_active', !!m.shuttle_count); }); } -function render_players_el(parentNode, setup, team_id, show_player_status) { +function render_players_el(parentNode, setup, team_id, match_id, show_player_status) { const team = setup.teams[team_id]; if (setup.incomplete) { uiu.el(parentNode, 'span', {}, ci18n('match:incomplete')); @@ -178,65 +245,146 @@ function render_players_el(parentNode, setup, team_id, show_player_status) { cflags.render_flag_el(parentNode, nat0); } - let player0_status = ""; - if (!show_player_status || setup.now_on_court) { - player0_status = "now_on_court"; - } else if (team.players[0].now_playing_on_court) { - player0_status = "now_playing"; - } else if (team.players[0].checked_in) { - player0_status = "checked_in"; - } else { - player0_status = "not_checked_in"; + render_player_el(parentNode, team.players[0], match_id, setup.now_on_court, show_player_status); + + if (team.players.length > 1) { + uiu.el(parentNode, 'span', {}, ' / '); + + const nat1 = team.players[1] && team.players[1].nationality; + const p1_el = uiu.el(parentNode, 'span', { + 'style': 'white-space: pre', + }); + if (curt.is_nation_competition && nat1 && (nat1 !== nat0)) { + cflags.render_flag_el(p1_el, nat1); + } + + render_player_el(parentNode, team.players[1], match_id, setup.now_on_court, show_player_status); } +} - let player_element = uiu.el(parentNode, 'span', player0_status, team.players[0].name.replace(' ', '\xa0')); - if (team.players[0].now_playing_on_court && player0_status != "now_on_court") { - let parts = team.players[0].now_playing_on_court.split("_"); +function render_player_el(parentNode, player, match_id, now_on_court, show_player_status) { + let player_status = get_player_status(player, now_on_court, show_player_status); + let player_element = uiu.el(parentNode, 'span', { + 'class' : 'player ' + player_status, + 'data-btp_id' : player.btp_id, + 'data-match_id': match_id, + }, player.name.replace(' ', '\xa0')); + if (player.now_playing_on_court && player_status != "now_on_court") { + let parts = player.now_playing_on_court.split("_"); let court_number = parts[parts.length - 1]; uiu.el(player_element, 'div', 'court', court_number); } - if(team.players[0].now_tablet_on_court) { - let parts = team.players[0].now_tablet_on_court.split("_"); + if(player.now_tablet_on_court) { + let parts = player.now_tablet_on_court.split("_"); let court_number = parts[parts.length - 1]; uiu.el(player_element, 'div', 'tablet_inline', court_number); } +} - if (team.players.length > 1) { - uiu.el(parentNode, 'span', {}, ' / '); +function get_player_status(player, now_on_court, show_player_status) { + let player_status = ""; + if (!show_player_status || now_on_court) { + player_status = "now_on_court"; + } else if (player.now_playing_on_court) { + player_status = "now_playing"; + } else if (player.checked_in) { + player_status = "checked_in"; + } else { + player_status = "not_checked_in"; + } - const nat1 = team.players[1] && team.players[1].nationality; - const p1_el = uiu.el(parentNode, 'span', { - 'style': 'white-space: pre', + return player_status; +} + +function update_players(m) { + if(m.setup.teams) { + m.setup.teams.forEach((team) => { + if(team.players) { + team.players.forEach((player) => { + update_player(m._id, player, m.setup.now_on_court, m.btp_winner === undefined); + }); + } }); - if (curt.is_nation_competition && nat1 && (nat1 !== nat0)) { - cflags.render_flag_el(p1_el, nat1); - } + } - let player1_status = ""; - if (!show_player_status || setup.now_on_court) { - player1_status = "now_on_court"; - } else if (team.players[1].now_playing_on_court) { - player1_status = "now_playing"; - } else if (team.players[1].checked_in) { - player1_status = "checked_in"; - } else { - player1_status = "not_checked_in"; +} + +function update_player(match_id, player, now_on_court, show_player_status) { + uiu.qsEach('.player[data-match_id=' + JSON.stringify(match_id) + '][data-btp_id="' + JSON.stringify(player.btp_id) + '"]' , function(player_el) { + let player_status = get_player_status(player, now_on_court, show_player_status); + + player_el.classList.remove("now_on_court", "now_playing", "checked_in", "not_checked_in"); + player_el.classList.add(player_status); + + //The only Child should be the now_playing_on_court icon or the now_tablet_on_court icon + while (player_el.firstElementChild) { + player_el.removeChild(player_el.lastElementChild); } - let player_element = uiu.el(p1_el, 'span', player1_status, team.players[1].name.replace(' ', '\xa0')); - if (team.players[1].now_playing_on_court && player1_status != "now_on_court") { - let parts = team.players[1].now_playing_on_court.split("_"); + if (player.now_playing_on_court && player_status != "now_on_court") { + let parts = player.now_playing_on_court.split("_"); let court_number = parts[parts.length - 1]; - uiu.el(player_element, 'div', 'court', court_number); + uiu.el(player_el, 'div', 'court', court_number); } - - if(team.players[1].now_tablet_on_court) { - let parts = team.players[1].now_tablet_on_court.split("_"); + + if(player.now_tablet_on_court) { + let parts = player.now_tablet_on_court.split("_"); let court_number = parts[parts.length - 1]; - uiu.el(player_element, 'div', 'tablet_inline', court_number); + uiu.el(player_el, 'div', 'tablet_inline', court_number); } + }); +} + +var active_timers = {'matches': {}, 'players' : {}}; + +function create_timer(timer_state, parent, default_color, exigent_color) { + + if (!timer_state) { + return; + } + + var tv = timer.calc(timer_state); + + if(!tv || !tv.visible){ + return; } + + let el = uiu.el(parent, 'div', {style: ('color:' + default_color +';')}, tv.str); + + var tobj = {} + + var update = function() { + var tv = timer.calc(timer_state); + var visible = tv.visible; + + uiu.text (el, tv.str); + if(tv.exigent && exigent_color){ + el.style.color = exigent_color; + } + + if (visible && tv.next) { + tobj.timeout = setTimeout(update, tv.next); + } else { + tobj.timeout = null; + } + }; + + update(); + + return tobj; +} + +function _extract_match_timer_state(match) { + var presses = match.presses; + + let s = {}; + s.settings = {}; + s.settings.negative_timers = true; + s.lang = "de"; //TODO: Use the language of the BTS Settings + + var rs = calc.remote_state(s, match.setup, presses); + return rs; } function prepare_render(t) { @@ -278,13 +426,13 @@ function prepare_render(t) { function on_edit_button_click(e) { const btn = e.target; - const match_id = btn.getAttribute('data-match__id'); + const match_id = btn.getAttribute('data-match_id'); ui_edit(match_id); } function on_scoresheet_button_click(e) { const btn = e.target; - const match_id = btn.getAttribute('data-match__id'); + const match_id = btn.getAttribute('data-match_id'); ui_scoresheet(match_id); } function on_announce_preparation_matchbutton_click(e) { @@ -320,7 +468,7 @@ function on_begin_to_play_button_click(e) { } function fetchMatchFromEvent(e) { const btn = e.target; - const match_id = btn.getAttribute('data-match__id'); + const match_id = btn.getAttribute('data-match_id'); const match = utils.find(curt.matches, m => m._id === match_id); if (!match) { cerror.silent('Match ' + match_id + ' konnte nicht gefunden werden'); @@ -639,7 +787,7 @@ function render_courts(container, style) { }, c.num); if (court_matches.length === 0) { - uiu.el(tr, 'td', {colspan: 9}, ''); + uiu.el(tr, 'td', {colspan: 11}, ''); } else { let i = 0; for (const cm of court_matches) { @@ -1064,6 +1212,7 @@ return { render_umpire_options, render_upcoming_matches, update_match_score, + update_players, }; })(); @@ -1089,6 +1238,7 @@ if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var i18n_en = require('../bup/js/i18n_en'); var printing = require('../bup/js/printing'); var settings = require('../bup/js/settings'); + var timer = require('../bup/js/timer'); module.exports = cmatch; } diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 88f10fc..8a5cddb 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -124,6 +124,22 @@ function update_score(c) { } } +function update_player_status(c){ + const cval = c.val; + const match_id = cval.match__id; + + // Find the match + const m = utils.find(curt.matches, m => m._id === match_id); + if (!m) { + cerror.silent('Cannot find match to update player status, ID: ' + JSON.stringify(match_id)); + return; + } + m.btp_winner = cval.btp_winner; + m.setup = cval.setup; + + cmatch.update_players(m); +} + function update_current_match(c) { change.change_current_match(c.val); _show_render_matches(); @@ -203,6 +219,7 @@ function ui_show() { const match_create_container = uiu.el(main, 'div'); cmatch.render_create(match_create_container); uiu.el(main, 'div', 'finished_container'); + _show_render_matches(); const footer_links = uiu.el(main, 'div', 'footer_links'); @@ -221,6 +238,7 @@ function ui_show() { _route_single(/t\/([a-z0-9]+)\/$/, ui_show, change.default_handler(_show_render_matches, { score: update_score, court_current_match: update_current_match, + update_player_status: update_player_status, })); function _upload_logo(e) { @@ -429,9 +447,7 @@ function ui_edit() { } warmup_timer_select.onchange = function() { - console.log(last_selected_warmup); if (!last_selected_warmup[3]) { - console.log("Sichern!"); for (const wo of warmup_options) { if (!wo[3]) { From aa93259f47f59266895bc37ee34d44f38e98aa20 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 8 Mar 2024 20:13:14 +0100 Subject: [PATCH 019/224] add missing dependency to timer.js from BUP --- static/cbts.html | 1 + 1 file changed, 1 insertion(+) diff --git a/static/cbts.html b/static/cbts.html index e884198..cf6937d 100644 --- a/static/cbts.html +++ b/static/cbts.html @@ -51,6 +51,7 @@ + From e12078a3083cc5feee168f53c764bafd986ebb07 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 9 Mar 2024 16:19:34 +0100 Subject: [PATCH 020/224] Add an Formular to create custom Announcements --- static/js/ctournament.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 8a5cddb..f247bf7 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -173,6 +173,25 @@ function ui_ticker_push() { }); } +function render_announcement_formular(target) { + const announcements = uiu.el(target, 'div', 'announcements_container'); + const form = uiu.el(announcements, 'form'); + uiu.el(form, 'textarea', { + type: 'textarea', + id: 'custom_announcement', + name: 'custom_announcement', + cols: '50', + rows: '4', + }); + const btp_fetch_btn = uiu.el(form, 'button', { + 'class': 'match_save_button', + role: 'submit', + }, 'Ansage abspielen'); + form_utils.onsubmit(form, function (d) { + announce([d.custom_announcement]); + }); + +} function ui_show() { crouting.set('t/:key/', {key: curt.key}); const bup_lang = ((curt.language && curt.language !== 'auto') ? '&lang=' + encodeURIComponent(curt.language) : ''); @@ -218,6 +237,7 @@ function ui_show() { uiu.el(main, 'div', 'unassigned_container'); const match_create_container = uiu.el(main, 'div'); cmatch.render_create(match_create_container); + render_announcement_formular(main); uiu.el(main, 'div', 'finished_container'); _show_render_matches(); From f3e754a562e234cb17777fc992b616c8f0c7b653 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 10 Mar 2024 17:51:22 +0100 Subject: [PATCH 021/224] Added functionallity that the winner of Quarterfinals has to the job as tabletoperator for the following match, --- bts/btp_sync.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 966a0df..b30fc65 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -632,16 +632,21 @@ function get_last_looser_on_court(app, tkey, court_id, umpire_name) { const match_querry = { 'tournament_key': tkey, 'setup.court_id': court_id, 'end_ts':{$exists: true}}; - let tabletoperators = undefined; - app.db.matches.find(match_querry).sort({'end_ts': -1}).limit(1).exec((err, last_match_on_court) => { if (err) { return reject(err); } - if (last_match_on_court.length === 1 && !umpire_name) { - const team = last_match_on_court[0].setup.teams[last_match_on_court[0].btp_winner % 2]; + const match = last_match_on_court[0]; + const round = match.setup.match_name; + var team = null; + if (round == 'VF' || round == 'QF') { + team = match.setup.teams[match.btp_winner-1]; + } else { + const index = match.btp_winner % 2; + team = match.setup.teams[index]; + } if(team && typeof team.players !== 'undefined') { tabletoperators = []; @@ -650,7 +655,6 @@ function get_last_looser_on_court(app, tkey, court_id, umpire_name) { }); } } - resolve(tabletoperators); }); }); From 1f65e45bf5a232acfd15aaef212af17857b47ae2 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 10 Mar 2024 18:14:10 +0100 Subject: [PATCH 022/224] Add the umpire in call for the Tabletoperator to use the Possibility to setup teblateoperators at the beginning of an tournament --- static/js/announcements.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/js/announcements.js b/static/js/announcements.js index c184a86..bbdae96 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -54,6 +54,8 @@ function createTabletOperator(matchSetup) { tabletOperator = tabletOperator + matchSetup.teams[1].players[0].state; } else if (matchSetup.tabletoperators) { tabletOperator = tabletOperator + createSingleTeam(matchSetup.tabletoperators); + } else if (matchSetup.umpire_name) { + tabletOperator = tabletOperator + matchSetup.umpire_name; } else { tabletOperator = tabletOperator + "Verlierer des vorhergehenden Spiels"; } From b9181706788d4e063b12358392c257455b76c1b4 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 19 Mar 2024 01:53:32 +0100 Subject: [PATCH 023/224] For upcoming matches, matches are also displayed where one or both teams have not yet been determined.Show also Matches in the upcomming list, where one or both teams are not --- bts/btp_parse.js | 10 +- bts/btp_sync.js | 489 ++++++++++++++++++++++++------------------ bts/http_api.js | 12 +- fetch-btp.js | 2 +- static/js/ci18n_de.js | 2 + static/js/ci18n_en.js | 2 + static/js/cmatch.js | 72 ++++++- 7 files changed, 362 insertions(+), 227 deletions(-) diff --git a/bts/btp_parse.js b/bts/btp_parse.js index 156e91e..9d996f6 100644 --- a/bts/btp_parse.js +++ b/bts/btp_parse.js @@ -33,7 +33,7 @@ function filter_matches(all_btp_matches, is_league) { }); } - return all_btp_matches.filter(bm => (bm.IsMatch && bm.IsPlayable && bm.MatchNr && bm.MatchNr[0] && bm.From1)); + return all_btp_matches.filter(bm => (bm.IsPlayable && bm.MatchNr && bm.MatchNr[0] && bm.From1)); } // bts_players: Array of array of players participating. @@ -166,6 +166,12 @@ function get_btp_state(response) { } else { all_btp_matches = btp_t.Matches ? btp_t.Matches[0].Match : []; } + + // Found Links + const all_btp_links = all_btp_matches.filter(m => { + return (m.Link != undefined); + }); + const matches_by_pid = utils.make_index(all_btp_matches, bm => _calc_match_id(bm, is_league)); const all_btp_entries = btp_t.Entries ? btp_t.Entries[0].Entry : []; const all_btp_events = btp_t.Events ? btp_t.Events[0].Event : []; @@ -202,6 +208,7 @@ function get_btp_state(response) { } const matches = filter_matches(all_btp_matches, is_league); + const links = all_btp_links; const players = utils.make_index(all_btp_players, p => p.ID[0]); const officials = utils.make_index(all_btp_officials, o => o.ID[0]); const courts = utils.make_index(all_btp_courts, c => c.ID[0]); @@ -215,6 +222,7 @@ function get_btp_state(response) { draws, events, matches, + links, officials, is_league, match_types: new Map(Object.entries(MATCH_TYPES)), diff --git a/bts/btp_sync.js b/bts/btp_sync.js index b30fc65..829d11d 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -18,126 +18,173 @@ function date_str(dt) { return utils.pad(dt.year, 2, '0') + '-' + utils.pad(dt.month, 2, '0') + '-' + utils.pad(dt.day, 2, '0'); } -function craft_match(app, tkey, btp_id, court_map, event, draw, officials, bm, match_ids_on_court, match_types, is_league) { - if (!bm.IsMatch) { - return; - } +async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, officials, bm, match_ids_on_court, match_types, is_league) { + return new Promise((resolve, reject) => { + + const gtid = event.GameTypeID[0]; + assert((gtid === 1) || (gtid === 2)); + + const scheduled_time_str = (bm.PlannedTime ? time_str(bm.PlannedTime[0]) : undefined); + const scheduled_date = (bm.PlannedTime ? date_str(bm.PlannedTime[0]) : undefined); + const match_name = (bm.RoundName && bm.RoundName[0] ? bm.RoundName[0] : undefined); + const event_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + ' - ' + draw.Name[0]; + const teams = _craft_teams(bm); + + const btp_player_ids = []; + + if(bm.bts_players && bm.bts_players.length > 0) { + for (const team of bm.bts_players) { + if (team && team.length > 0){ + for (const p of team) { + btp_player_ids.push(p.ID[0]); + } + } + } + } - if (!bm.bts_complete) { - // TODO: register them as incomplete, but continue instead of returning - return; - } + const links = {}; + try{ + links.from1 = bm.From1[0]; + links.from2 = bm.From2[0]; - const gtid = event.GameTypeID[0]; - assert((gtid === 1) || (gtid === 2)); + if(bm.WinnerTo) { + links.winner_to = bm.WinnerTo[0]; + } + if(bm.LoserTo) { + links.loser_to = bm.LoserTo[0]; + } + if(bm.Link) { + links.from_link = bm.Link; + } + } catch (err) { + console.log(err); + } - const scheduled_time_str = (bm.PlannedTime ? time_str(bm.PlannedTime[0]) : undefined); - const scheduled_date = (bm.PlannedTime ? date_str(bm.PlannedTime[0]) : undefined); - const match_name = bm.RoundName[0]; - const event_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + ' - ' + draw.Name[0]; - const teams = _craft_teams(bm); + if(teams[0].players.length < 1) { + const link1 = btp_links.find(l => { + return (l.DrawID[0] === bm.DrawID[0] && l.PlanningID[0] === links.from1); + }); - const btp_player_ids = []; - for (const team of bm.bts_players) { - for (const p of team) { - btp_player_ids.push(p.ID[0]); + if(link1){ + links.from1_link = link1.Link[0]; + } } - } - const setup = { - incomplete: !bm.bts_complete, - is_doubles: (gtid === 2), - match_num: bm.MatchNr[0], - counting: '3x21', - team_competition: false, - match_name, - event_name, - teams, - warmup: 'none', - }; + if(teams[1].players.length < 1) { + const link2 = btp_links.find(l => { + return (l.DrawID[0] === bm.DrawID[0] && l.PlanningID[0] === links.from2); + }); - app.db.tournaments.findOne({key: tkey}, (err, tournament) => { - if (err) { - return callback(err); - } - if (tournament.warmup) { - setup.warmup = tournament.warmup; - } - if (tournament.warmup_ready) { - setup.warmup_ready = tournament.warmup_ready; - } - if (tournament.warmup_start) { - setup.warmup_start = tournament.warmup_start; - } - if (tournament.btp_settings.check_in_per_match && teams.length > 1) { - teams[0].players[0].checked_in = (bm.Status & 0b0001) > 0; - if(teams[0].players.length > 1) { - teams[0].players[1].checked_in = (bm.Status & 0b0010) > 0; - } - teams[1].players[0].checked_in = (bm.Status & 0b0100) > 0; - if(teams[1].players.length > 1) { - teams[1].players[1].checked_in = (bm.Status & 0b1000) > 0; + if(link2){ + links.from2_link = link2.Link[0]; } } - }); + const setup = { + is_match: (bm.IsMatch && bm.IsMatch[0] ? true : false), + incomplete: !bm.bts_complete, + is_doubles: (gtid === 2), + match_num: bm.MatchNr[0], + counting: '3x21', + team_competition: false, + match_name, + event_name, + teams, + warmup: 'none', + links: links + }; - if (scheduled_time_str) { - setup.scheduled_time_str = scheduled_time_str; - } - if (scheduled_date) { - setup.scheduled_date = scheduled_date; - } - if (bm.CourtID) { - const btp_court_id = bm.CourtID[0]; - const court_id = court_map.get(btp_court_id); - assert(court_id); - setup.court_id = court_id; - setup.now_on_court = match_ids_on_court.has(bm.ID[0]); - } - if (bm.Official1ID) { - const o = officials.get(bm.Official1ID[0]); - assert(o); - setup.umpire_name = o.FirstName + ' ' + o.Name; - } - if (bm.Official2ID) { - const o = officials.get(bm.Official2ID[0]); - assert(o); - setup.service_judge_name = o.FirstName + ' ' + o.Name; - } + app.db.tournaments.findOne({key: tkey}, (err, tournament) => { + if(err) { + console.log("reject"); + reject(err); + } + + if (tournament.warmup) { + setup.warmup = tournament.warmup; + } - const btp_match_ids = [{ - id: bm.ID[0], - nr: bm.MatchNr[0], - draw: bm.DrawID[0], - planning: bm.PlanningID[0], - }]; - - const match = { - tournament_key: tkey, - btp_id, - btp_match_ids, - btp_player_ids, - setup, - }; - match.team1_won = undefined; - match.btp_winner = undefined; - if (bm.Winner) { - match.btp_winner = bm.Winner[0]; - match.team1_won = (match.btp_winner === 1); - } - if (bm.Sets) { - match.network_score = _parse_score(bm); - } - if (bm.Shuttles) { - match.shuttle_count = bm.Shuttles[0]; - } - if (bm.DisplayOrder) { - match.match_order = bm.DisplayOrder[0]; - } - match._id = 'btp_' + btp_id; + if (tournament.warmup) { + setup.warmup = tournament.warmup; + } + if (tournament.warmup_ready) { + setup.warmup_ready = tournament.warmup_ready; + } + if (tournament.warmup_start) { + setup.warmup_start = tournament.warmup_start; + } + if (tournament.btp_settings.check_in_per_match && teams.length > 1 && teams[0].players.length > 0) { + teams[0].players[0].checked_in = (bm.Status & 0b0001) > 0; + if(teams[0].players.length > 1) { + teams[0].players[1].checked_in = (bm.Status & 0b0010) > 0; + } - return match; + if (teams[1].players.length > 0) { + teams[1].players[0].checked_in = (bm.Status & 0b0100) > 0; + if(teams[1].players.length > 1) { + teams[1].players[1].checked_in = (bm.Status & 0b1000) > 0; + } + } + } + + if (scheduled_time_str) { + setup.scheduled_time_str = scheduled_time_str; + } + if (scheduled_date) { + setup.scheduled_date = scheduled_date; + } + if (bm.CourtID) { + const btp_court_id = bm.CourtID[0]; + const court_id = court_map.get(btp_court_id); + assert(court_id); + setup.court_id = court_id; + setup.now_on_court = match_ids_on_court.has(bm.ID[0]); + } + if (bm.Official1ID) { + const o = officials.get(bm.Official1ID[0]); + assert(o); + setup.umpire_name = o.FirstName + ' ' + o.Name; + } + if (bm.Official2ID) { + const o = officials.get(bm.Official2ID[0]); + assert(o); + setup.service_judge_name = o.FirstName + ' ' + o.Name; + } + + const btp_match_ids = [{ + id: bm.ID[0], + nr: bm.MatchNr[0], + draw: bm.DrawID[0], + planning: bm.PlanningID[0], + }]; + + const match = { + tournament_key: tkey, + btp_id, + btp_match_ids, + btp_player_ids, + setup, + }; + match.team1_won = undefined; + match.btp_winner = undefined; + if (bm.Winner) { + match.btp_winner = bm.Winner[0]; + match.team1_won = (match.btp_winner === 1); + } + if (bm.Sets) { + match.network_score = _parse_score(bm); + } + if (bm.Shuttles) { + match.shuttle_count = bm.Shuttles[0]; + } + if (bm.DisplayOrder) { + match.match_order = bm.DisplayOrder[0]; + } + match._id = 'btp_' + btp_id; + + resolve(match); + }); + }); } function _craft_team(par) { @@ -252,7 +299,7 @@ function _parse_score(bm) { return bm.Sets[0].Set.map(s => [s.T1[0], s.T2[0]]); } -function integrate_matches(app, tkey, btp_state, court_map, callback) { +async function integrate_matches(app, tkey, btp_state, court_map, callback) { const admin = require('./admin'); // avoid dependency cycle const {draws, events, officials} = btp_state; @@ -274,118 +321,134 @@ function integrate_matches(app, tkey, btp_state, court_map, callback) { }; // TODO get all matches upfront here app.db.matches.findOne(query, (err, cur_match) => { - if (err) return cb(err); - - if (cur_match && cur_match.btp_needsync) { - cb(); + if (err) { + console.log(err); + cb(null); return; - } - - let match = craft_match(app, tkey, btp_id, court_map, event, draw, officials, bm, match_ids_on_court); - - if (!match) { - cb(); + }; + if (cur_match && cur_match.btp_needsync) { + cb(null); return; } - - if (cur_match) { - if(cur_match.setup.called_timestamp) { - // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. - match.setup.called_timestamp = cur_match.setup.called_timestamp; - } - - if (cur_match.setup.tabletoperators) { - // tabletoperators is not from btp so we have to coppy it to the match generated by btp. - match.setup.tabletoperators = cur_match.setup.tabletoperators; - } - - for(let team_index = 0; team_index < cur_match.setup.teams.length; team_index++) { - for (let player_index = 0; player_index < cur_match.setup.teams[team_index].players.length; player_index++){ - - if (cur_match.setup.teams[team_index].players[player_index].now_playing_on_court != undefined) { - match.setup.teams[team_index].players[player_index].now_playing_on_court = cur_match.setup.teams[team_index].players[player_index].now_playing_on_court; - } - - if (cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court != undefined) { - match.setup.teams[team_index].players[player_index].now_tablet_on_court = cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court; - } - - if(cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts || match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { - if(!cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { - cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; - } - - if(!match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { - match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; - } - - let max_ts = Math.max( cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts, - match.setup.teams[team_index].players[player_index].last_time_on_court_ts); - - cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; - match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; - } + + craft_match(app, tkey, btp_id, court_map, event, draw, btp_state.links, officials, bm, match_ids_on_court).then(match => { + + if (cur_match) { + if(cur_match.setup.called_timestamp) { + // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. + match.setup.called_timestamp = cur_match.setup.called_timestamp; } - } - - if (cur_match.setup.warmup) { - match.setup.warmup = cur_match.setup.warmup; - } - - if (cur_match.setup.warmup_ready) { - match.setup.warmup_ready = cur_match.setup.warmup_ready; - } - - if(cur_match.setup.warmup_start) { - match.setup.warmup_start = cur_match.setup.warmup_start; - } - - if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { - // No update required - cb(); - return; - } - - // equals checked_in changed and check if it was the only change - let only_change_check_in = false; - - for(let team_index = 0; team_index < cur_match.setup.teams.length; team_index++) { - for (let player_index = 0; player_index < cur_match.setup.teams[team_index].players.length; player_index++){ - cur_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; + + if (cur_match.setup.tabletoperators) { + // tabletoperators is not from btp so we have to coppy it to the match generated by btp. + match.setup.tabletoperators = cur_match.setup.tabletoperators; } - } + + for(let team_index = 0; team_index < cur_match.setup.teams.length; team_index++) { + for (let player_index = 0; player_index < cur_match.setup.teams[team_index].players.length; player_index++){ + + if (cur_match.setup.teams[team_index].players[player_index].now_playing_on_court != undefined) { + match.setup.teams[team_index].players[player_index].now_playing_on_court = cur_match.setup.teams[team_index].players[player_index].now_playing_on_court; + } + + if (cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court != undefined) { + match.setup.teams[team_index].players[player_index].now_tablet_on_court = cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court; + } + + if(cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts || match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { + if(!cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { + cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; + } + + if(!match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { + match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; + } + + let max_ts = Math.max( cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts, + match.setup.teams[team_index].players[player_index].last_time_on_court_ts); + + cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; + match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; + } + } + } + + if(match.setup.now_on_court === false) { + if(cur_match.setup.warmup) { + match.setup.warmup = cur_match.setup.warmup; + } - if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { - only_change_check_in = true; + if (cur_match.setup.warmup_ready) { + match.setup.warmup_ready = cur_match.setup.warmup_ready; + } + + if(cur_match.setup.warmup_start) { + match.setup.warmup_start = cur_match.setup.warmup_start; + } + } + + if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { + // No update required + cb(null); + return; + } + + + // equals checked_in changed and check if it was the only change + let only_change_check_in = false; + + for(let team_index = 0; team_index < cur_match.setup.teams.length; team_index++) { + for (let player_index = 0; player_index < cur_match.setup.teams[team_index].players.length; player_index++){ + cur_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; + } + } + + if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { + only_change_check_in = true; + } + + app.db.matches.update({_id: cur_match._id}, {$set: match}, {}, (err) => { + if (err) { + return; + }; + + if(!only_change_check_in) { + admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup, + from: "btp_sync.js:423"}); + } else { + admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup, + from: "btp_sync.js:429"}); + } + }); + cb(null); + return; } - - app.db.matches.update({_id: cur_match._id}, {$set: match}, {}, (err) => { - if (err) return cb(err); - - if(!only_change_check_in) { - admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup, - from: "btp_sync.js:423"}); - } else { - admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup, - from: "btp_sync.js:429"}); + app.db.matches.insert(match, function(err) { + if (err) { + cb(null); + return; } - cb(); + + admin.notify_change(app, tkey, 'match_add', {match}); + cb(null) + return; }); + }, error => + { + cb(null); return; - } - - app.db.matches.insert(match, function(err) { - if (err) return cb(err); - - admin.notify_change(app, tkey, 'match_add', {match}); - cb(); }); }); - }, callback); + }, (error) => { + if (error){ + console.log(error); + } + callback(null); + }); } // Returns a map btp_court_id => court._id @@ -538,7 +601,7 @@ async function integrate_now_on_court(app, tkey, callback) { const admin = require('./admin'); // avoid dependency cycle const stournament = require('./stournament'); // avoid dependency cycle const btp_manager = require('./btp_manager'); - + // TODO after switching to async, this should happen during court&match construction app.db.tournaments.findOne({key: tkey}, async (err, tournament) => { if (err) { @@ -686,7 +749,7 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { let change = false; - if (tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + if (match.setup.teams[0].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { match.setup.teams[0].players[0].now_tablet_on_court = match_on_court_setup.court_id; match.setup.teams[0].players[0].checked_in = false; change = true; @@ -698,7 +761,7 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { change = true; } - if (tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + if (match.setup.teams[1].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { match.setup.teams[1].players[0].now_tablet_on_court = match_on_court_setup.court_id; match.setup.teams[1].players[0].checked_in = false; change = true; @@ -753,7 +816,7 @@ function set_player_on_court (app, tkey, match_on_court_setup, callback) { let change = false; - if (on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + if (match.setup.teams[0].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { match.setup.teams[0].players[0].now_playing_on_court = match_on_court_setup.court_id; change = true; } @@ -763,7 +826,7 @@ function set_player_on_court (app, tkey, match_on_court_setup, callback) { change = true; } - if (on_court_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + if (match.setup.teams[1].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { match.setup.teams[1].players[0].now_playing_on_court = match_on_court_setup.court_id; change = true; } diff --git a/bts/http_api.js b/bts/http_api.js index f78873a..d815abe 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -386,7 +386,8 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { let change = false; - if (remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && + if (match.setup.teams[0].players.length > 0 && + remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && match.setup.teams[0].players[0].now_playing_on_court) { match.setup.teams[0].players[0].now_playing_on_court = false; change = true; @@ -399,7 +400,8 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { change = true; } - if (remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && + if (match.setup.teams[1].players.length > 0 && + remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && match.setup.teams[1].players[0].now_playing_on_court) { match.setup.teams[1].players[0].now_playing_on_court = false; change = true; @@ -463,7 +465,8 @@ function remove_tablet_on_court (app, tkey, cur_match_id, callback) { let change = false; - if (remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && + if (match.setup.teams[0].players.length > 0 && + remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && ( match.setup.teams[0].players[0].now_tablet_on_court == true || match.setup.teams[0].players[0].checked_in == false)) { match.setup.teams[0].players[0].now_tablet_on_court = false; @@ -480,7 +483,8 @@ function remove_tablet_on_court (app, tkey, cur_match_id, callback) { change = true; } - if (remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && + if (match.setup.teams[1].players.length > 0 && + remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && ( match.setup.teams[1].players[0].now_tablet_on_court == true || match.setup.teams[1].players[0].checked_in == false)) { match.setup.teams[1].players[0].now_tablet_on_court = false; diff --git a/fetch-btp.js b/fetch-btp.js index f329dbb..366bfba 100755 --- a/fetch-btp.js +++ b/fetch-btp.js @@ -286,7 +286,7 @@ async function main() { const btp_id = tkey + '_' + discipline_name + '_' + match_num; const match = btp_sync.craft_match( - btp_conn.app, tkey, btp_id, pseudo_court_map, event, draw, officials, bm, match_ids_on_court); + btp_conn.app, tkey, btp_id, pseudo_court_map, event, draw, officials, bm, match_ids_on_court, []); if (!match) { continue; } diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 31e20f8..582f268 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -43,6 +43,8 @@ var ci18n_de = { 'Create tournament': 'Turnier erstellen', 'create:id:label': 'Turnier-ID (Kleinbuchstaben, keine Leerzeichen):', 'experimental': '(experimentell)', +'Winner': 'Gewinner', +'Loser': 'Verlierer', 'tournament:edit:id': 'Turnier-ID:', 'tournament:edit:language': 'Sprache:', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 2b64eef..7ad7201 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -43,6 +43,8 @@ var ci18n_en = { 'Create tournament': 'Create tournament', 'create:id:label': 'tournament ID (all lowercase, no spaces):', 'experimental': '(experimental)', +'Winner': 'Winner', +'Loser': 'Loser', 'tournament:edit:id': 'Tournament id:', 'tournament:edit:language': 'Language:', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index d87ab9b..27456ad 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -42,6 +42,10 @@ function render_match_table_header(table, include_courts) { } function render_match_row(tr, match, court, style, show_player_status) { + if(!match.setup.is_match) { + return; + } + if (!court && match.setup.court_id) { court = curt.courts_by_id[match.setup.court_id]; } @@ -93,10 +97,10 @@ function render_match_row(tr, match, court, style, show_player_status) { }); create_match_button(players0, 'vlink match_second_call_button', 'match:secondcallteamone', on_second_call_team_one_button_click, match._id); - render_players_el(players0, setup, 0, match._id ,show_player_status); + render_players_el(players0, setup, 0, match ,show_player_status); uiu.el(tr, 'td', 'match_vs', 'v'); const players1 = uiu.el(tr, 'td', ((match.team1_won === false) ? 'match_team_won ' : '') + 'match_team2'); - render_players_el(players1, setup, 1, match._id, show_player_status); + render_players_el(players1, setup, 1, match, show_player_status); create_match_button(players1, 'vlink match_second_call_button', 'match:secondcallteamtwo', on_second_call_team_two_button_click, match._id); if (style === 'default' || style === 'plain') { const to_td = uiu.el(tr, 'td'); @@ -234,18 +238,70 @@ function update_match_score(m) { }); } -function render_players_el(parentNode, setup, team_id, match_id, show_player_status) { +function render_players_el(parentNode, setup, team_id, match, show_player_status) { const team = setup.teams[team_id]; - if (setup.incomplete) { - uiu.el(parentNode, 'span', {}, ci18n('match:incomplete')); - } const nat0 = team.players[0] && team.players[0].nationality; if (curt.is_nation_competition && nat0) { cflags.render_flag_el(parentNode, nat0); } - render_player_el(parentNode, team.players[0], match_id, setup.now_on_court, show_player_status); + if (team.players.length > 0) { + render_player_el(parentNode, team.players[0], match._id, setup.now_on_court, show_player_status); + } else { + + let dependency = '???'; + if(team_id == 0 && match.setup.links.from1_link) { + dependency = match.setup.links.from1_link; + } + else if(team_id == 1 && match.setup.links.from2_link) { + dependency = match.setup.links.from2_link; + } + else { + let match_before = curt.matches.filter(m => { + + return (m.setup.event_name === match.setup.event_name && + ( + m.btp_match_ids[0].planning == match.setup.links.from1 || + m.btp_match_ids[0].planning == match.setup.links.from2 + )); + }); + + let resolved_match = []; + + match_before.forEach(t => { + if(t.setup.is_match){ + resolved_match.push(t); + } + else { + const result = curt.matches.find(m => { + return (m.setup.event_name === t.setup.event_name && + m.setup.is_match && + ( + m.setup.links.from1 == t.setup.links.from1 || + m.setup.links.from2 == t.setup.links.from2 + )); + }); + resolved_match.push(result); + } + }); + + const index = Math.min(resolved_match.length - 1, team_id); + + if(resolved_match.length > 0) { + + if(resolved_match[index].setup.links.winner_to && resolved_match[index].setup.links.winner_to == match.btp_match_ids[0].planning) { + dependency = ci18n('Winner') + " #" + resolved_match[index].setup.match_num + " - " + resolved_match[index].setup.scheduled_date + " " + resolved_match[index].setup.scheduled_time_str; + } + else { + dependency = ci18n('Loser') + " #" + resolved_match[index].setup.match_num + " - " + resolved_match[index].setup.scheduled_date + " " + resolved_match[index].setup.scheduled_time_str; + } + } + } + + uiu.el(parentNode, 'span', {}, dependency); + + } if (team.players.length > 1) { uiu.el(parentNode, 'span', {}, ' / '); @@ -258,7 +314,7 @@ function render_players_el(parentNode, setup, team_id, match_id, show_player_sta cflags.render_flag_el(p1_el, nat1); } - render_player_el(parentNode, team.players[1], match_id, setup.now_on_court, show_player_status); + render_player_el(parentNode, team.players[1], match._id, setup.now_on_court, show_player_status); } } From e792c463f9ff36c483c236bf4f424c240c600485 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 21 Mar 2024 00:08:02 +0100 Subject: [PATCH 024/224] fix: The buttons for the second call was hidden after evtr press on the tablet. Now thez are hidden if the first point is scored. --- static/css/cmatch.css | 4 ++++ static/js/cmatch.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 81060bf..4529f4b 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -115,6 +115,10 @@ padding-left: 0.3em; } +.match_timer { + min-width: 3.6em; +} + .match_timer > div { font-size: 1.2em; font-weight: 900; diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 27456ad..cbf0e20 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -224,10 +224,10 @@ function update_match_score(m) { }); } else { uiu.qsEach('.match_second_call_button[data-match_id=' + JSON.stringify(m._id) + ']', (button_el) => { - button_el.style.visibility = 'hidden'; + button_el.style.visibility = 'visible'; }); uiu.qsEach('.match_begin_to_play_button[data-match_id=' + JSON.stringify(m._id) + ']', (button_el) => { - button_el.style.visibility = 'hidden'; + button_el.style.visibility = 'visible'; }); } From 7c52618a0c10a142b78c13fb1626f1e225febff9 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Mon, 25 Mar 2024 21:52:17 +0100 Subject: [PATCH 025/224] Add advanced support for the Tabletoperatormanagement. It now is possible to add or remove tabletoperators manually. Also alwas the person who waits the longest time will do the service for the next match- independet of the field the match is called. --- bts/admin.js | 58 +++ bts/btp_sync.js | 53 +-- bts/database.js | 1 + bts/http_api.js | 52 ++- bts/stournament.js | 8 + static/cbts.html | 136 +++---- static/css/cmatch.css | 2 + static/css/ctabletoperator.css | 42 +++ static/icons/tabletoperator_add.svg | 482 +++++++++++++++++++++++++ static/icons/tabletoperator_remove.svg | 121 +++++++ static/js/change.js | 11 + static/js/ci18n_de.js | 4 + static/js/ci18n_en.js | 4 + static/js/cmatch.js | 42 ++- static/js/ctabletoperator.js | 105 ++++++ static/js/ctournament.js | 14 +- 16 files changed, 1037 insertions(+), 98 deletions(-) create mode 100644 static/css/ctabletoperator.css create mode 100644 static/icons/tabletoperator_add.svg create mode 100644 static/icons/tabletoperator_remove.svg create mode 100644 static/js/ctabletoperator.js diff --git a/bts/admin.js b/bts/admin.js index 90c4701..aff81de 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -135,6 +135,11 @@ function handle_tournament_get(app, ws, msg) { tournament.umpires = umpires; cb(err); }); + }, function (cb) { + stournament.get_tabletoperators(app.db, tournament.key, function (err, tabletoperators) { + tournament.tabletoperators = tabletoperators; + cb(err); + }); }, function(cb) { stournament.get_matches(app.db, tournament.key, function(err, matches) { tournament.matches = matches; @@ -213,6 +218,57 @@ function handle_match_add(app, ws, msg) { }); } +function handle_tabletoperator_remove(app, ws, msg) { + if (!msg.tournament_key) { + return ws.respond(msg, { message: 'Missing tournament_key' }); + } + if (!msg.tabletoperator) { + return ws.respond(msg, { message: 'Missing tabletoperator' }); + } + const tournament_key = msg.tournament_key; + const tabletoperator = msg.tabletoperator + app.db.tabletoperators.update({ _id: tabletoperator._id, tournament_key: tournament_key }, { $set: { court: -1 } }, { returnUpdatedDocs: true}, function (err, numAffected, changed_tabletoperator) { + if (err) { + ws.respond(msg, err); + return; + } + notify_change(app, tournament_key, 'tabletoperator_removed', { tabletoperator: changed_tabletoperator }); + }); +} + +function handle_tabletoperator_add(app, ws, msg) { + if (!msg.tournament_key) { + return ws.respond(msg, { message: 'Missing tournament_key' }); + } + if (!msg.match) { + return ws.respond(msg, { message: 'Missing match' }); + } + const tournament_key = msg.tournament_key; + const team_id = msg.team_id; + const match = msg.match + const team = match.setup.teams[team_id]; + var tabletoperator = []; + + team.players.forEach((player) => { + tabletoperator.push(player); + }); + const new_tabletoperator = { + tournament_key, + tabletoperator, + 'match_id': 'manually_added', + 'start_ts': Date.now(), + 'end_ts': null, + 'court': null + }; + app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_tabletoperator) { + if (err) { + ws.respond(msg, err); + return; + } + notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_tabletoperator }); + }); +} + function handle_match_edit(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'id', 'setup'])) { return; @@ -388,6 +444,8 @@ module.exports = { async_handle_match_delete, async_handle_tournament_upload_logo, handle_btp_fetch, + handle_tabletoperator_add, + handle_tabletoperator_remove, handle_fetch_allscoresheets_data, handle_create_tournament, handle_courts_add, diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 829d11d..d3b2e35 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -629,8 +629,11 @@ async function integrate_now_on_court(app, tkey, callback) { if(!setup.called_timestamp) { setup.called_timestamp = called_timestamp; try{ - const tabletoperators = await get_last_looser_on_court(app, tkey, court_id, setup.umpire_name); - setup.tabletoperators = tabletoperators; + const promise = serializedAsyncTask(admin, app, tkey, court_id, setup.umpire_name); + promise.then((value) => { + setup.tabletoperators = value; + }); + } catch (err) { callback(err) } @@ -690,35 +693,35 @@ async function integrate_now_on_court(app, tkey, callback) { // TODO clear courts (better in async) } -function get_last_looser_on_court(app, tkey, court_id, umpire_name) { +function serialized(fn) { + let queue = Promise.resolve(); + return (...args) => { + const res = queue.then(() => fn(...args)); + queue = res.catch(() => { }); + return res; + } +} +const serializedAsyncTask = serialized(get_last_looser_on_court); +function get_last_looser_on_court(admin, app, tkey, court_id, umpire_name) { return new Promise((resolve, reject) => { - const match_querry = { 'tournament_key': tkey, - 'setup.court_id': court_id, - 'end_ts':{$exists: true}}; + const tabletoperator_querry = { 'tournament_key': tkey, court: null }; let tabletoperators = undefined; - app.db.matches.find(match_querry).sort({'end_ts': -1}).limit(1).exec((err, last_match_on_court) => { + app.db.tabletoperators.find(tabletoperator_querry).sort({ 'start_ts': 1 }).limit(1).exec((err, tabletoperator) => { if (err) { return reject(err); } - if (last_match_on_court.length === 1 && !umpire_name) { - const match = last_match_on_court[0]; - const round = match.setup.match_name; - var team = null; - if (round == 'VF' || round == 'QF') { - team = match.setup.teams[match.btp_winner-1]; - } else { - const index = match.btp_winner % 2; - team = match.setup.teams[index]; - } - if(team && typeof team.players !== 'undefined') { - tabletoperators = []; - - team.players.forEach((player) => { - tabletoperators.push(player); - }); - } + var returnvalue = undefined; + if (tabletoperator.length == 1) { + returnvalue = tabletoperator[0].tabletoperator + app.db.tabletoperators.update({ _id: tabletoperator[0]._id, tournament_key: tkey }, { $set: { court: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_tabletoperator) { + if (err) { + ws.respond(msg, err); + return; + } + admin.notify_change(app, tkey, 'tabletoperator_removed', { tabletoperator: changed_tabletoperator }); + resolve(returnvalue); + }); } - resolve(tabletoperators); }); }); } diff --git a/bts/database.js b/bts/database.js index 896c2ad..36b3cfb 100644 --- a/bts/database.js +++ b/bts/database.js @@ -16,6 +16,7 @@ const TABLES = [ 'tournaments', 'umpires', 'logs', + 'tabletoperators', ]; diff --git a/bts/http_api.js b/bts/http_api.js index d815abe..056717c 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -152,7 +152,7 @@ function matches_handler(req, res) { let matches = db_matches.map(dbm => create_match_representation(tournament, dbm)); if (tournament.only_now_on_court) { - matches = matches.filter(m => m?.setup.now_on_court); + matches = matches.filter(m => m.setup.now_on_court); } db_courts.sort(utils.cmp_key('num')); @@ -269,7 +269,8 @@ function score_handler(req, res) { if(update.team1_won != undefined && update.team1_won != null) { async.waterfall([ cb => remove_player_on_court(req.app, tournament_key, match_id, cb), - cb => remove_tablet_on_court(req.app, tournament_key, match_id, cb) + cb => remove_tablet_on_court(req.app, tournament_key, match_id, cb), + cb => add_player_to_tabletoperator_list(req.app, tournament_key, match_id, update.end_ts, cb) ], function(err) { if (err) { res.json({ @@ -357,6 +358,53 @@ function score_handler(req, res) { }); } +function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, end_ts, callback) { + const admin = require('./admin'); // avoid dependency cycle + app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { + if (err) { + return reject(err); + } + app.db.tabletoperators.findOne({ 'tournament_key': tournament_key, 'match_id': cur_match_id }, (err, no_tabletoperator) => { + if (err) { + return reject(err); + } + if (no_tabletoperator == null) { + const round = cur_match.setup.match_name; + var team = null; + if (round == 'VF' || round == 'QF') { + team = cur_match.setup.teams[cur_match.btp_winner - 1]; + } else { + const index = cur_match.btp_winner % 2; + team = cur_match.setup.teams[index]; + } + if (team && typeof team.players !== 'undefined') { + var tabletoperator = []; + + team.players.forEach((player) => { + tabletoperator.push(player); + }); + + const new_tabletoperator = { + tournament_key, + tabletoperator, + 'match_id': cur_match_id, + 'start_ts': end_ts, + 'end_ts': null, + 'court': null + }; + app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { + if (err) { + ws.respond(msg, err); + return; + } + admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); + }); + + } + } + }); + }); +} function remove_player_on_court (app, tkey, cur_match_id, callback) { const admin = require('./admin'); // avoid dependency cycle app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { diff --git a/bts/stournament.js b/bts/stournament.js index 6ff3dc5..20423ed 100644 --- a/bts/stournament.js +++ b/bts/stournament.js @@ -31,9 +31,17 @@ function get_matches(db, tournament_key, callback) { }); } +function get_tabletoperators(db, tournament_key, callback) { + db.tabletoperators.find({ tournament_key }, function (err, tabletoperators) { + if (err) return callback(err); + return callback(err, tabletoperators); + }); +} + module.exports = { get_courts, get_matches, get_umpires, + get_tabletoperators, }; diff --git a/static/cbts.html b/static/cbts.html index cf6937d..62cdcd7 100644 --- a/static/cbts.html +++ b/static/cbts.html @@ -1,87 +1,89 @@ - -Badminton Tournament Server - - - - - - - + + Badminton Tournament Server + + + + + + + + -
    Connecting ...
    +
    Connecting ...
    -
    +
    -
    +
    -
    -
    -
    -
    +
    +
    +
    +
    - - + + - - - - - + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - + + - - - - + + + + - - + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 4529f4b..4811f13 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -53,6 +53,7 @@ margin: 0 0.2em -0.2em 0.2em; background-image: url(../icons/second_call.svg); } + .match_rawinfo { margin: 0 0.3em 0 0.3em; background-image: url(../icons/rawinfo.svg); @@ -189,6 +190,7 @@ match_second_call_button, .match_second_call_button > * { display: block; } + .match_second_call_button > button { margin: 0 0 0 2vmin; font-size: 5vmin; diff --git a/static/css/ctabletoperator.css b/static/css/ctabletoperator.css new file mode 100644 index 0000000..4f4aaa1 --- /dev/null +++ b/static/css/ctabletoperator.css @@ -0,0 +1,42 @@ +.tabletoperator_add_button { + display: inline-block; + min-width: 1em; + min-height: 1em; + background-size: 100%; + background-repeat: no-repeat; + background-position: center center; + height: 1.2em; + background-size: cover; + width: 1.9em; + margin: 0 0.2em -0.2em 0.2em; + background-image: url(../icons/tabletoperator_add.svg); +} +.tabletoperator_remove_button { + display: inline-block; + min-width: 1em; + min-height: 1em; + background-size: 100%; + background-repeat: no-repeat; + background-position: center center; + height: 1.2em; + background-size: cover; + width: 1.9em; + margin: 0 0.2em -0.2em 0.2em; + background-image: url(../icons/tabletoperator_remove.svg); +} + +.tabletoperator_remove_button:hover, +.tabletoperator_add_button:hover { + opacity: 0.5; + } +.tabletoperator_remove_button > *, +.tabletoperator_add_button > * { + display: block; + } +.tabletoperator_remove_button > button, +.tabletoperator_add_button > button { + margin: 0 0 0 2vmin; + font-size: 5vmin; + padding: 0.1em 0.1em; + min-width: 5em; + } diff --git a/static/icons/tabletoperator_add.svg b/static/icons/tabletoperator_add.svg new file mode 100644 index 0000000..d971385 --- /dev/null +++ b/static/icons/tabletoperator_add.svg @@ -0,0 +1,482 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/static/icons/tabletoperator_remove.svg b/static/icons/tabletoperator_remove.svg new file mode 100644 index 0000000..0258d87 --- /dev/null +++ b/static/icons/tabletoperator_remove.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/static/js/change.js b/static/js/change.js index 052fa41..f8b3888 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -84,6 +84,17 @@ function default_handler_func(rerender, special_funcs, c) { }); break;} + case 'tabletoperator_add': + curt.tabletoperators.push(c.val.tabletoperator); + rerender(); + break; + case 'tabletoperator_removed': + const changed_t = utils.find(curt.tabletoperators, m => m._id === c.val.tabletoperator._id); + if (changed_t) { + changed_t.court = c.val.tabletoperator.court; + } + rerender(); + break; case 'match_add': curt.matches.push(c.val.match); rerender(); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 582f268..31e5915 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -111,6 +111,10 @@ var ci18n_de = { 'umpires:btp_id': 'BTP-ID {btp_id}', 'umpires:last_on_court': 'letztes Spiel endete {time}', +'tabletoperator:unassigned': 'Nächste TabletbedienerInnen', +'tabletoperator:name': 'Name', +'tabletoperator:add': 'Als Tabletoperator planen', +'tabletoperator:remove': 'Von Liste nehmen', 'csvexport:winners': 'CSV-Export für Siegerurkunden', }; diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 7ad7201..caf7ee5 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -111,6 +111,10 @@ var ci18n_en = { 'umpires:btp_id': 'BTP ID {btp_id}', 'umpires:last_on_court': 'previous match ended at {time}', +'tabletoperator:unassigned': 'Next Tabletoperators', +'tabletoperator:name': 'Name', +'tabletoperator:add': 'Schedule as Tabletoperator', +'tabletoperator:remove': 'Remove from list', 'csvexport:winners': 'CSV export (winner\'s certificates)', }; diff --git a/static/js/cmatch.js b/static/js/cmatch.js index cbf0e20..20c2724 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -95,13 +95,25 @@ function render_match_row(tr, match, court, style, show_player_status) { 'class': ((match.team1_won === true) ? 'match_team_won' : ''), style: 'text-align: right;', }); + if (!court) { + if (setup.teams[0].players.length > 0) { + create_match_button(players0, 'vlink tabletoperator_add_button', 'tabletoperator:add', on_add_to_tabletoperators_team_one_button_click, match._id); + } + } else { + create_match_button(players0, 'vlink match_second_call_button', 'match:secondcallteamone', on_second_call_team_one_button_click, match._id); + } - create_match_button(players0, 'vlink match_second_call_button', 'match:secondcallteamone', on_second_call_team_one_button_click, match._id); - render_players_el(players0, setup, 0, match ,show_player_status); + render_players_el(players0, setup, 0, match, show_player_status); uiu.el(tr, 'td', 'match_vs', 'v'); const players1 = uiu.el(tr, 'td', ((match.team1_won === false) ? 'match_team_won ' : '') + 'match_team2'); render_players_el(players1, setup, 1, match, show_player_status); - create_match_button(players1, 'vlink match_second_call_button', 'match:secondcallteamtwo', on_second_call_team_two_button_click, match._id); + if (!court) { + if (setup.teams[1].players.length > 0) { + create_match_button(players1, 'vlink tabletoperator_add_button', 'tabletoperator:add', on_add_to_tabletoperators_team_two_button_click, match._id); + } + } else { + create_match_button(players1, 'vlink match_second_call_button', 'match:secondcallteamtwo', on_second_call_team_two_button_click, match._id); + } if (style === 'default' || style === 'plain') { const to_td = uiu.el(tr, 'td'); if (setup.umpire_name) { @@ -503,6 +515,30 @@ function on_second_call_team_one_button_click(e) { announceSecondCallTeamOne(match.setup); } } + +function on_add_to_tabletoperators_team_one_button_click(e) { + const match = fetchMatchFromEvent(e); + add_to_tabletoperator(match, 0) +} +function on_add_to_tabletoperators_team_two_button_click(e) { + const match = fetchMatchFromEvent(e); + add_to_tabletoperator(match,1) +} + +function add_to_tabletoperator(match,team_num) { + if (match != null) { + send({ + type: 'tabletoperator_add', + tournament_key: curt.key, + team_id: team_num, + match: match, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +} function on_second_call_team_two_button_click(e) { const match = fetchMatchFromEvent(e); if (match != null) { diff --git a/static/js/ctabletoperator.js b/static/js/ctabletoperator.js new file mode 100644 index 0000000..1d56ef3 --- /dev/null +++ b/static/js/ctabletoperator.js @@ -0,0 +1,105 @@ +'use strict'; + +var ctabletoperator = (function() { + +function render_unassigned(container) { + uiu.empty(container); + uiu.el(container, 'h3', {}, ci18n('tabletoperator:unassigned')); + + const unassigned_tabletoperators = curt.tabletoperators.filter(m => m.court == null); + render_tabletoperator_table(container, unassigned_tabletoperators); + } + +function render_tabletoperator_table(container, tabletoperators) { + + const table = uiu.el(container, 'table', 'tabletoperators_table'); + render_tabletoperator_table_header(table); + const tbody = uiu.el(table, 'tbody'); + + for (const t of tabletoperators) { + const tr = uiu.el(tbody, 'tr'); + render_tabletoperator_row(tr, t); + } +} + +function render_tabletoperator_table_header(table) { + const thead = uiu.el(table, 'thead'); + const title_tr = uiu.el(thead, 'tr'); + uiu.el(title_tr, 'th', {}, ci18n('tabletoperator:name')); +} +function render_tabletoperator_row(tr, tabletoperator) { + const to = tabletoperator.tabletoperator; + const to_td = uiu.el(tr, 'td'); + uiu.el(to_td, 'div', 'tablet', ''); + uiu.el(to_td, 'span', 'match_no_umpire', to[0].name); + if (to.length > 1) { + uiu.el(to_td, 'span', 'match_no_umpire', ' \u200B/ '); + uiu.el(to_td, 'span', 'match_no_umpire', to[1].name); + } + if (tabletoperator.court == null) { + const buttonbar = uiu.el(tr, 'td'); + create_tabletoperator_button(buttonbar, 'vlink tabletoperator_remove_button', 'tabletoperator:remove', on_remove_from_list_button_click, tabletoperator._id); + } +} +function on_remove_from_list_button_click(e) { + const to = fetchTabletOperatorFromEvent(e); + if (to != null) { + send({ + type: 'tabletoperator_remove', + tournament_key: curt.key, + tabletoperator: to, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +} + +function fetchTabletOperatorFromEvent(e) { + const btn = e.target; + const to_id = btn.getAttribute('data-tabletoperator_id'); + const to = utils.find(curt.tabletoperators, to => to._id === to_id); + if (!to) { + cerror.silent('Tabletoperator ' + to_id + ' konnte nicht gefunden werden'); + return null; + } else { + return to; + } +} +function create_tabletoperator_button(targetEl, cssClass, title, listener, tabletoperatorID) { + const btn = uiu.el(targetEl, 'div', { + 'class': cssClass, + 'title': ci18n(title), + 'data-tabletoperator_id': tabletoperatorID, + }); + btn.addEventListener('click', listener); +} +//crouting.register(/t\/([a-z0-9]+)\/umpires$/, function(m) { +// ctournament.switch_tournament(m[1], function() { +// render_unassigned(); +// }); +//}, change.default_handler(render_unassigned)); + + +return { + render_unassigned, +}; + +})(); + +/*@DEV*/ +if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { + var cflags = require('./cflags'); + var ci18n = require('./ci18n.js'); + var change = require('./change.js'); + var cmatch = require('./cmatch.js'); + var crouting = require('./crouting.js'); + var ctournament = require('./ctournament.js'); + var toprow = require('./toprow.js'); + var uiu = require('../bup/js/uiu.js'); + var utils = require('../bup/js/utils.js'); + + module.exports = ctabletoperator; +} +/*/@DEV*/ diff --git a/static/js/ctournament.js b/static/js/ctournament.js index f247bf7..c35059d 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -145,11 +145,19 @@ function update_current_match(c) { _show_render_matches(); } +function _update_all_ui_elements() { + _show_render_matches(); + _show_render_tabletoperators(); + +} function _show_render_matches() { cmatch.render_courts(uiu.qs('.courts_container')); cmatch.render_unassigned(uiu.qs('.unassigned_container')); cmatch.render_finished(uiu.qs('.finished_container')); } +function _show_render_tabletoperators() { + ctabletoperator.render_unassigned(uiu.qs('.unassigned_tableoperators_container')); +} function ui_btp_fetch() { send({ @@ -234,6 +242,7 @@ function ui_show() { cmatch.prepare_render(curt); uiu.el(main, 'div', 'courts_container'); + uiu.el(main, 'div', 'unassigned_tableoperators_container'); uiu.el(main, 'div', 'unassigned_container'); const match_create_container = uiu.el(main, 'div'); cmatch.render_create(match_create_container); @@ -254,8 +263,10 @@ function ui_show() { if (curt.is_nation_competition) { crouting.render_link(footer_links, `t/${curt.key}/nationstats`, ci18n('nationstats')); } + + _show_render_tabletoperators(); } -_route_single(/t\/([a-z0-9]+)\/$/, ui_show, change.default_handler(_show_render_matches, { +_route_single(/t\/([a-z0-9]+)\/$/, ui_show, change.default_handler(_update_all_ui_elements, { score: update_score, court_current_match: update_current_match, update_player_status: update_player_status, @@ -969,6 +980,7 @@ if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var cmatch = require('./cmatch'); var crouting = require('./crouting'); var cumpires = require('./cumpires'); + var ctabletoperator = require('./ctabletoperator'); var debug = require('./debug'); var form_utils = require('./form_utils'); var i18n = require('../bup/js/i18n'); From 64c07091614df596e73eeecf44411017979d9adf Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Fri, 29 Mar 2024 09:24:04 +0100 Subject: [PATCH 026/224] Communication betreen BUP and BTS now WS based. That means that there is no further need to pull the score from BTS. BTS is now able to push the scores exact to those clients which requires this information. This reduce the amount of traffic and the load on client an service enormuously. --- bts/btp_sync.js | 3 +- bts/bupws.js | 210 ++++++++++++++++++++++++++++++++++++++++++++++-- bts/http_api.js | 5 +- 3 files changed, 209 insertions(+), 9 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index d3b2e35..30e845f 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -601,6 +601,7 @@ async function integrate_now_on_court(app, tkey, callback) { const admin = require('./admin'); // avoid dependency cycle const stournament = require('./stournament'); // avoid dependency cycle const btp_manager = require('./btp_manager'); + const bupws = require('./bupws'); // TODO after switching to async, this should happen during court&match construction app.db.tournaments.findOne({key: tkey}, async (err, tournament) => { @@ -673,7 +674,7 @@ async function integrate_now_on_court(app, tkey, callback) { setup: match.setup, from: "btp_sync.js:664"}); admin.notify_change(app, tkey, 'match_called_on_court', match); - + bupws.handle_score_change(app, tkey, court_id); async.waterfall([ wcb => set_player_on_court(app, tkey, match.setup, wcb), wcb => set_player_on_tablet(app, tkey, match.setup, wcb)], (err) => { diff --git a/bts/bupws.js b/bts/bupws.js index 7efa88b..159c1ec 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -1,19 +1,215 @@ 'use strict'; -function handle(/*app, ws*/) { - // TODO do something +const serror = require('./serror'); +const utils = require('./utils'); + +const all_panels = []; + +function on_close(app, ws) { + if (!utils.remove(all_panels, ws)) { + serror.silent('Removing Scoreboard ws, but it was not connected!?'); + } +} + +function on_connect(app, ws) { + all_panels.push(ws); } -function on_close() { - // TODO update client list +function notify_change(tournament_key, court_id, ctype, val) { + for (const panel_ws of all_panels) { + notify_change_ws(panel_ws, tournament_key, court_id, ctype, val); + } } -function on_connect() { - // TODO update client list +function notify_change_ws(ws, tournament_key, court_id, ctype, val) { + if (ws == null) { + notify_change(tournament_key, court_id, ctype, val); + } else { + if (ws.court_id === court_id) { + ws.sendmsg({ + type: 'change', + tournament_key, + ctype, + val, + }); + } + } } +function all_matches_delivery() { + for (const panel_ws of all_panels) { + if (panel_ws.court_id === undefined) { + return true; + } + } +} + +function handle_init(app, ws, msg) { + const tournament_key = msg.tournament_key; + var court_id = msg.panel_settings.court_id; + if (court_id) { + ws.court_id = court_id; + } else { + ws.court_id = undefined; + court_id = undefined; + } + matches_handler(app, ws, tournament_key, court_id); +} + +function handle_score_change(app, tournament_key, court_id) { + matches_handler(app, null, tournament_key, court_id); + if (all_matches_delivery()) { + matches_handler(app, null, tournament_key, undefined); + } +} + +function matches_handler(app, ws, tournament_key, court_id) { + const now = Date.now(); + const show_still = now - 60000; + const query = { + tournament_key, + $or: [ + { + $and: [ + { + team1_won: { + $ne: true, + }, + }, + { + team1_won: { + $ne: false, + }, + }, + ], + }, + { + end_ts: { + $gt: show_still, + }, + }, + ], + }; + if (court_id) { + query['setup.court_id'] = court_id; + } else { + query['setup.court_id'] = { $exists: true }; + } + + app.db.fetch_all([{ + queryFunc: '_findOne', + collection: 'tournaments', + query: { key: tournament_key }, + }, { + collection: 'matches', + query, + }, { + collection: 'courts', + query: { tournament_key }, + }], function (err, tournament, db_matches, db_courts) { + if (err) { + const msg = { + status: 'error', + message: err.message, + }; + notify_change_ws(app, tournament_key, "score-update,", msg); + } + + let matches = db_matches.map(dbm => create_match_representation(tournament, dbm)); + if (tournament.only_now_on_court) { + matches = matches.filter(m => m.setup.now_on_court); + } + + db_courts.sort(utils.cmp_key('num')); + const courts = db_courts.map(function (dc) { + var res = { + court_id: dc._id, + label: dc.num, + }; + if (dc.match_id) { + res.match_id = 'bts_' + dc.match_id; + } + if (dc.called_timestamp) { + res.called_timestamp = dc.called_timestamp; + } + return res; + }); + + const event = create_event_representation(tournament); + event.matches = matches; + event.courts = courts; + const reply = { + status: 'ok', + event, + }; + notify_change_ws(ws, tournament_key, court_id, "score-update",reply) + }); +} + +function create_match_representation(tournament, match) { + const setup = match.setup; + setup.match_id = 'bts_' + match._id; + setup.team_competition = tournament.is_team; + setup.nation_competition = tournament.is_nation_competition; + for (const t of setup.teams) { + if (!t.players) continue; + + for (const p of t.players) { + if (p.lastname) continue; + + const asian_m = /^([A-Z]+)\s+(.*)$/.exec(p.name); + if (asian_m) { + p.lastname = asian_m[1]; + p.firstname = asian_m[2]; + p._guess_info = 'bts_asian'; + continue; + } + + const m = /^(.*)\s+(\S+)$/.exec(p.name); + if (m) { + p.firstname = m[1]; + p.lastname = m[2]; + p._guess_info = 'bts_western'; + } else { + p.firstname = ''; + p.lastname = p.name; + p._guess_info = 'bts_single'; + } + } + } + + const res = { + setup, + network_score: match.network_score, + network_team1_left: match.network_team1_left, + network_team1_serving: match.network_team1_serving, + network_teams_player1_even: match.network_teams_player1_even, + end_ts: match.end_ts !== undefined ? match.end_ts : null, + }; + if (match.presses) { + res.presses_json = JSON.stringify(match.presses); + } + return res; +} + +function create_event_representation(tournament) { + const res = { + id: 'bts_' + tournament.key, + tournament_name: tournament.name, + }; + if (tournament.logo_id) { + res.tournament_logo_url = `/h/${encodeURIComponent(tournament.key)}/logo/${tournament.logo_id}`; + } + res.tournament_logo_background_color = tournament.logo_background_color || '#000000'; + res.tournament_logo_foreground_color = tournament.logo_foreground_color || '#aaaaaa'; + return res; +} + + module.exports = { - handle, on_close, on_connect, + notify_change, + handle_init, + handle_score_change, }; \ No newline at end of file diff --git a/bts/http_api.js b/bts/http_api.js index 056717c..c730851 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -5,6 +5,7 @@ const async = require('async'); const path = require('path'); const admin = require('./admin'); +const bupws = require('./bupws'); const btp_manager = require('./btp_manager'); const stournament = require('./stournament'); const ticker_manager = require('./ticker_manager'); @@ -97,6 +98,7 @@ function create_event_representation(tournament) { return res; } +// TODO might be removed due to refactoring work in bupws.js function matches_handler(req, res) { const tournament_key = req.params.tournament_key; const now = Date.now(); @@ -266,7 +268,7 @@ function score_handler(req, res) { update.btp_needsync = true; } - if(update.team1_won != undefined && update.team1_won != null) { + if (update.team1_won != undefined && update.team1_won != null) { async.waterfall([ cb => remove_player_on_court(req.app, tournament_key, match_id, cb), cb => remove_tablet_on_court(req.app, tournament_key, match_id, cb), @@ -343,6 +345,7 @@ function score_handler(req, res) { presses: match.presses, }); + bupws.handle_score_change(req.app, tournament_key, match.setup.court_id); cb(); }, ], function(err) { From 7d0f2e058b7df1e202dace1523123f57a8ace49f Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Fri, 29 Mar 2024 13:42:31 +0100 Subject: [PATCH 027/224] No it is possible to serve an centralised setting for each client from bts. If is a setting stored in displaysettings and a link between client and displaysetting ist stored in display_court_displaysettings on statup of the client it will be fetched and applied to the client. --- bts/bupws.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++--- bts/database.js | 2 ++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index 159c1ec..0bb6fa8 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -1,5 +1,6 @@ -'use strict'; +'use strict'; +const { forEach } = require('async'); const serror = require('./serror'); const utils = require('./utils'); @@ -44,7 +45,7 @@ function all_matches_delivery() { } } -function handle_init(app, ws, msg) { +async function handle_init(app, ws, msg) { const tournament_key = msg.tournament_key; var court_id = msg.panel_settings.court_id; if (court_id) { @@ -53,9 +54,60 @@ function handle_init(app, ws, msg) { ws.court_id = undefined; court_id = undefined; } + if (msg.initialize_display) { + const remote_adress_seqments = ws._socket.remoteAddress.split('.'); + const client_id = remote_adress_seqments[remote_adress_seqments.length - 1]; + if (client_id) { + let display_setting = await get_display_setting(app, tournament_key, client_id, court_id) + if (display_setting != null) { + ws.court_id = display_setting.court_id; + court_id = display_setting.court_id; + notify_change_ws(ws, tournament_key, court_id, "settings-update", display_setting); + } + } + } matches_handler(app, ws, tournament_key, court_id); } +function get_display_setting(app, tkey, client_id, court_id) { + return new Promise((resolve, reject) => { + const display_court_query = { 'client_id': client_id }; + app.db.display_court_displaysettings.find(display_court_query).limit(1).exec((err, display_court_displaysetting) => { + if (err) { + return reject(err); + } + var returnvalue = null; + if (display_court_displaysetting.length == 1) { + const display_query = { 'id': display_court_displaysetting[0].displaysetting_id }; + app.db.displaysettings.find(display_query).limit(1).exec((err, display_setting) => { + if (err) { + return reject(err); + } + if (display_setting.length == 1) { + returnvalue = display_setting[0]; + returnvalue.court_id = display_court_displaysetting[0].court_id; + returnvalue.displaymode_court_id = display_court_displaysetting[0].court_id; + } + resolve(returnvalue); + }); + } else { + const display_query_default = { 'id': 'default' }; + app.db.displaysettings.find(display_query_default).limit(1).exec((err, display_setting_default) => { + if (err) { + return reject(err); + } + if (display_setting_default.length == 1) { + returnvalue = display_setting_default[0]; + returnvalue.court_id = court_id; + returnvalue.displaymode_court_id = court_id; + } + resolve(returnvalue); + }); + } + }); + }); +} + function handle_score_change(app, tournament_key, court_id) { matches_handler(app, null, tournament_key, court_id); if (all_matches_delivery()) { @@ -112,7 +164,7 @@ function matches_handler(app, ws, tournament_key, court_id) { status: 'error', message: err.message, }; - notify_change_ws(app, tournament_key, "score-update,", msg); + notify_change_ws(app, tournament_key, "score-update", msg); } let matches = db_matches.map(dbm => create_match_representation(tournament, dbm)); diff --git a/bts/database.js b/bts/database.js index 36b3cfb..92d28d7 100644 --- a/bts/database.js +++ b/bts/database.js @@ -17,6 +17,8 @@ const TABLES = [ 'umpires', 'logs', 'tabletoperators', + 'displaysettings', + 'display_court_displaysettings', ]; From c7cb84254d5eb379ad0339bc524c0746e4e84dde Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Fri, 29 Mar 2024 17:36:43 +0100 Subject: [PATCH 028/224] Now it is posiible to persist customized Settings per client an reset this settings in bts. --- bts/bupws.js | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index 0bb6fa8..b849a54 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -45,6 +45,50 @@ function all_matches_delivery() { } } +async function handle_reset_display_settings(app, ws, msg) { + const tournament_key = msg.tournament_key; + const court_id = msg.panel_settings.court_id; + var setting = msg.panel_settings; + + const client_id = determine_client_id(ws); + var client_court_displaysetting = await get_display_court_displaysettings(app, tournament_key, client_id); + if (client_court_displaysetting != null) { + const updatevalues = { + client_id: 'deleted' + } + client_court_displaysetting = await update_client_court_displaysetting(app, client_court_displaysetting, updatevalues); + + } +} + +async function handle_persist_display_settings(app, ws, msg) { + const tournament_key = msg.tournament_key; + const court_id = msg.panel_settings.court_id; + var setting = msg.panel_settings; + + const client_id = determine_client_id(ws); + var client_court_displaysetting = await get_display_court_displaysettings(app, tournament_key, client_id); + if (client_court_displaysetting == null) { + setting.id = tournament_key + "_" + court_id + " _" + Date.now(); + setting = await persist_displaysetting(app, setting); + + client_court_displaysetting = { + client_id: client_id, + court_id: court_id, + displaysetting_id: setting.id, + } + client_court_displaysetting = await persist_client_court_displaysetting(app, client_court_displaysetting); + } else { + setting.id = tournament_key + "_" + court_id + " _" + Date.now(); + setting = await persist_displaysetting(app, setting); + const updatevalues = { + court_id: court_id, + displaysetting_id: setting.id, + } + client_court_displaysetting = await update_client_court_displaysetting(app, client_court_displaysetting, updatevalues); + } +} + async function handle_init(app, ws, msg) { const tournament_key = msg.tournament_key; var court_id = msg.panel_settings.court_id; @@ -55,8 +99,7 @@ async function handle_init(app, ws, msg) { court_id = undefined; } if (msg.initialize_display) { - const remote_adress_seqments = ws._socket.remoteAddress.split('.'); - const client_id = remote_adress_seqments[remote_adress_seqments.length - 1]; + const client_id = determine_client_id(ws); if (client_id) { let display_setting = await get_display_setting(app, tournament_key, client_id, court_id) if (display_setting != null) { @@ -69,6 +112,65 @@ async function handle_init(app, ws, msg) { matches_handler(app, ws, tournament_key, court_id); } +function determine_client_id(ws) { + const remote_adress_seqments = ws._socket.remoteAddress.split('.'); + return remote_adress_seqments[remote_adress_seqments.length - 1]; +} + + +function persist_client_court_displaysetting(app, client_court_displaysetting) { + return new Promise((resolve, reject) => { + app.db.display_court_displaysettings.insert(client_court_displaysetting, function (err, inserted_t) { + if (err) { + reject(err); + } + resolve(inserted_t); + }); + }); +} + +function update_client_court_displaysetting(app, client_court_displaysetting, updatevalues) { + return new Promise((resolve, reject) => { + app.db.display_court_displaysettings.update({ _id: client_court_displaysetting._id }, { $set: updatevalues }, { returnUpdatedDocs: true }, function (err, numAffected, changed_objects) { + if (err) { + reject(err) + } + resolve(changed_objects) + + }); + }); +} + + +function persist_displaysetting(app, setting) { + setting._id = undefined; + return new Promise((resolve, reject) => { + app.db.displaysettings.insert(setting, function (err, inserted_t) { + if (err) { + reject(err); + } + resolve(inserted_t); + }); + }); +} + + +function get_display_court_displaysettings(app, tkey, client_id) { + return new Promise((resolve, reject) => { + const display_court_query = { 'client_id': client_id }; + app.db.display_court_displaysettings.find(display_court_query).limit(1).exec((err, display_court_displaysetting) => { + if (err) { + return reject(err); + } + var returnvalue = null; + if (display_court_displaysetting.length == 1) { + returnvalue = display_court_displaysetting[0]; + } + resolve(returnvalue); + }); + }); +} + function get_display_setting(app, tkey, client_id, court_id) { return new Promise((resolve, reject) => { const display_court_query = { 'client_id': client_id }; @@ -264,4 +366,6 @@ module.exports = { notify_change, handle_init, handle_score_change, + handle_persist_display_settings, + handle_reset_display_settings, }; \ No newline at end of file From 437a7de80b0344aa495c2330b52c44fd27bd45fc Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 2 Apr 2024 17:59:12 +0200 Subject: [PATCH 029/224] Better handling of Player status, show pause timers in Admin view --- bts/admin.js | 44 ++++++++++- bts/btp_conn.js | 8 +- bts/btp_manager.js | 24 +++++- bts/btp_proto.js | 80 ++++++++++++-------- bts/btp_sync.js | 141 +++++++++++++++++++++++++++++++++-- bts/http_api.js | 65 ++++++++++------ static/css/cmatch.css | 58 +++++++++++++- static/css/seg7.ttf | Bin 0 -> 5620 bytes static/css/seven-segment.ttf | Bin 0 -> 29076 bytes static/icons/umpire.svg | 12 +-- static/js/change.js | 1 + static/js/cmatch.js | 50 ++++++++++++- 12 files changed, 402 insertions(+), 81 deletions(-) create mode 100644 static/css/seg7.ttf create mode 100644 static/css/seven-segment.ttf diff --git a/bts/admin.js b/bts/admin.js index aff81de..eee4b7b 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -177,14 +177,21 @@ function _extract_setup(msg_setup) { 'now_on_court', 'umpire_name', 'service_judge_name', + 'highlight', 'is_doubles', + 'is_match', 'incomplete', + 'links', 'scheduled_time_str', 'scheduled_date', 'called_timestamp', 'teams', + 'team_competition', 'tabletoperators', 'override_colors', + 'warmup', + 'warmup_ready', + 'warmup_start', ]); if (!setup.match_name && setup.match_num) { setup.match_name = '# ' + setup.match_num; @@ -292,7 +299,7 @@ function handle_match_edit(app, ws, msg) { return; } - notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, setup, from: "admin.js:239"}); + notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, setup}); if (msg.btp_update) { btp_manager.update_score(app, changed_match); } @@ -300,6 +307,40 @@ function handle_match_edit(app, ws, msg) { }); } + +function handle_match_preparation_call(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'id', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + app.db.matches.update({_id: msg.id, tournament_key}, {$set: {setup}}, {returnUpdatedDocs: true}, function(err, numAffected, changed_match) { + if (err) { + ws.respond(msg, err); + return; + } + if (numAffected !== 1) { + ws.respond(msg, new Error('Cannot find match ' + msg.id + ' of tournament ' + tournament_key + ' in database')); + return; + } + if (changed_match._id !== msg.id) { + const errmsg = 'Match ' + changed_match._id + ' changed by accident, intended to change ' + msg.id + ' (old nedb version?)'; + serror.silent(errmsg); + ws.respond(msg, new Error(errmsg)); + return; + } + + //notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, setup}); + notify_change(app, tournament_key, 'match_preparation_call', {match__id: msg.id, setup}); + + btp_manager.update_highlight(app, changed_match); + + ws.respond(msg, err); + }); +} + async function async_handle_match_delete(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'id'])) { return; @@ -451,6 +492,7 @@ module.exports = { handle_courts_add, handle_match_add, handle_match_edit, + handle_match_preparation_call, handle_ticker_pushall, handle_ticker_reset, handle_tournament_get, diff --git a/bts/btp_conn.js b/bts/btp_conn.js index de3b142..0b236db 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -293,6 +293,9 @@ class BTPConn { }); } + update_highlight(match) { + this.update_score(match); + } update_players(players) { if (this.readonly) { @@ -304,12 +307,11 @@ class BTPConn { } if (! this.key_unicode) { - serror.silent('Trying to send match data, but never logged in. Must retry later'); + serror.silent('Trying to update player data, but never logged in. Must retry later'); return; } - const req = btp_proto.update_players_request( - this.key_unicode, this.password, players); + const req = btp_proto.update_players_request(players, this.key_unicode, this.password); this.send(req, response => { const results = response.Action[0].Result; const rescode = results ? results[0] : 'no-result'; diff --git a/bts/btp_manager.js b/bts/btp_manager.js index 10dad01..e2e1ffd 100644 --- a/bts/btp_manager.js +++ b/bts/btp_manager.js @@ -51,10 +51,6 @@ function update_score(app, match) { return; } - if (typeof match.team1_won !== 'boolean') { - return; // Match not finished yet - } - conn.update_score(match); } @@ -74,6 +70,25 @@ function update_players(app, tkey, players) { conn.update_players(players); } +function update_highlight(app, match) { + assert(match); + const tkey = match.tournament_key; + assert(tkey); + + if (!match) { + return; + } + + const conn = conns_by_tkey.get(tkey); + if (!conn) { + // Do not output an error; this happens if BTP support gets disabled + return; + } + + conn.update_highlight(match); +} + + function init(app, cb) { app.db.tournaments.find({}, (err, tournaments) => { if (err) return cb(err); @@ -101,4 +116,5 @@ module.exports = { reconfigure, update_score, update_players, + update_highlight, }; \ No newline at end of file diff --git a/bts/btp_proto.js b/bts/btp_proto.js index a88ba5f..4401049 100644 --- a/bts/btp_proto.js +++ b/bts/btp_proto.js @@ -77,47 +77,57 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud res.Action.Password = password; } - assert(typeof match.team1_won === 'boolean'); - const winner = match.team1_won ? 1 : 2; assert(match.btp_match_ids); assert(match.btp_match_ids.length > 0); - assert(match.network_score); - const duration_mins = match.duration_ms ? Math.floor(match.duration_ms / 60000) : 0; const shuttle_count = match.shuttle_count; for (const btp_m_id of match.btp_match_ids) { assert(btp_m_id); - const sets = match.network_score.map(ns => { - return { - Set: { - T1: ns[0], - T2: ns[1], - }, - }; - }); - - let scoreStatus = 0; //Won normally - if( (match.presses.length > 0 && match.presses[match.presses.length - 1].type == "retired") || - (match.presses.length > 1 && match.presses[match.presses.length - 2].type == "retired")) { - scoreStatus = 2; //retired - } - if( (match.presses.length > 0 && match.presses[match.presses.length - 1].type == "disqualified") || - (match.presses.length > 1 && match.presses[match.presses.length - 2].type == "disqualified")) { - scoreStatus = 3; //disqualified - } + //TODO: calc Status; const m = { ID: btp_m_id.id, DrawID: btp_m_id.draw, PlanningID: btp_m_id.planning, - Sets: sets, - Winner: winner, - ScoreStatus: scoreStatus, - Duration: duration_mins, Status: 0, + Highlight: (match.setup.highlight ? match.setup.highlight : 0), + DisplayOrder: match.match_order, // BTP also sends a boolean ScoreSheetPrinted here }; + + if(typeof match.team1_won === 'boolean') { + m.Winner = match.team1_won ? 1 : 2; + + const duration_mins = match.duration_ms ? Math.floor(match.duration_ms / 60000) : 0; + m.Duration = duration_mins; + + if(match.network_score) { + const sets = match.network_score.map(ns => { + return { + Set: { + T1: ns[0], + T2: ns[1], + }, + }; + }); + + m.Sets = sets; + } + + let scoreStatus = 0; //Won normally + if( (match.presses.length > 0 && match.presses[match.presses.length - 1].type == "retired") || + (match.presses.length > 1 && match.presses[match.presses.length - 2].type == "retired")) { + scoreStatus = 2; //retired + } + if( (match.presses.length > 0 && match.presses[match.presses.length - 1].type == "disqualified") || + (match.presses.length > 1 && match.presses[match.presses.length - 2].type == "disqualified")) { + scoreStatus = 3; //disqualified + } + + m.ScoreStatus = scoreStatus; + } + if (umpire_btp_id) { m.Official1ID = umpire_btp_id; } @@ -143,7 +153,7 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud const pupdate = { ID: pid, LastTimeOnCourt: end_date, - CheckedIn: true, + CheckedIn: false, }; players.push({Player: pupdate}); } @@ -152,7 +162,8 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud for (const operator of match.setup.tabletoperators) { const pupdate = { ID: operator.btp_id, - CheckedIn: true, + LastTimeOnCourt: end_date, + CheckedIn: false, }; players.push({Player: pupdate}); @@ -162,7 +173,7 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud return res; } -function update_players_request(key_unicode, password, players) { +function update_players_request(players, key_unicode, password) { assert(key_unicode); const res = { Header: { @@ -191,14 +202,17 @@ function update_players_request(key_unicode, password, players) { res.Update.Tournament.Players = btp_players; players.forEach((player) => { - if (player.btp_id && player.last_time_on_court_ts && (player.last_time_on_court_ts + 300000 > Date.now())) { - const end_date = new Date(player.last_time_on_court_ts); - + if (player.btp_id) { const pupdate = { ID: player.btp_id, - LastTimeOnCourt: end_date, CheckedIn: player.checked_in, }; + + if (player.last_time_on_court_ts) { + const end_date = new Date(player.last_time_on_court_ts); + + pupdate.LastTimeOnCourt = end_date; + } btp_players.push({Player: pupdate}); } }); diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 30e845f..1386b6e 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -91,7 +91,8 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, event_name, teams, warmup: 'none', - links: links + links: links, + highlight: bm.Highlight[0], }; app.db.tournaments.findOne({key: tkey}, (err, tournament) => { @@ -415,13 +416,11 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { if(!only_change_check_in) { admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, btp_winner: match.btp_winner, - setup: match.setup, - from: "btp_sync.js:423"}); + setup: match.setup}); } else { admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, btp_winner: match.btp_winner, - setup: match.setup, - from: "btp_sync.js:429"}); + setup: match.setup}); } }); cb(null); @@ -536,6 +535,127 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { }); } +async function integrate_player_state(app, tkey, btp_state, callback) { + const btp_manager = require('./btp_manager'); + + app.db.tournaments.findOne({key: tkey}, (err, tournament) => { + if(err) return callback(err); + + if(!tournament.btp_settings.check_in_per_match) { + let ids_to_change = []; + let players_to_change = []; + async.eachOfSeries(btp_state.matches, async (match, key) => { + let cur_match = undefined; + + try { + const match_from_db = await get_match_form_db (app, tkey, btp_state, match); + cur_match = match_from_db; + } catch (err) { + return; + } + + for (let team_nr = 0; team_nr < 2; team_nr++) { + for (let player_nr = 0; player_nr < 2; player_nr++) { + let id = pause_is_done(match, team_nr, player_nr, tournament.btp_settings); + + if (id != undefined && id != null) { + + if (!cur_match.setup.teams[team_nr].players[player_nr].now_tablet_on_court && + !cur_match.setup.teams[team_nr].players[player_nr].now_playing_on_court && + !cur_match.setup.called_timestamp && + !cur_match.network_score) { + + btp_state.matches[key].bts_players[team_nr][player_nr].CheckedIn[0] = true; + + + const player = cur_match.setup.teams[team_nr].players[player_nr]; + if(ids_to_change.indexOf(id) == -1) { + player.checked_in = true; + ids_to_change.push(id); + players_to_change.push(player); + } + } + } + } + } + }, (err) => { + if(err) return callback(err); + btp_manager.update_players(app, tkey, players_to_change); + return callback(null); + }); + } + + }); +} + +async function get_match_form_db (app, tkey, btp_state, match) { + return new Promise((resolve, reject) => { + const {draws, events, officials} = btp_state; + const draw = draws.get(match.DrawID[0]); + if(!draw) { + return reject("Draw is unset!"); + } + + const event = events.get(draw.EventID[0]); + if (!event) { + return reject("Event is unset"); + } + + const discipline_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + '_' + draw.Name[0]; + const btp_id = tkey + '_' + discipline_name + '_' + match.ID[0]; + + const query = { + btp_id: btp_id, + tournament_key: tkey, + }; + + app.db.matches.findOne(query, (err, cur_match) => { + if (err) { + console.log(err); + return reject(err); + }; + + if(cur_match) { + return resolve(cur_match); + }; + + reject("no match found"); + }); + }); +} + +function pause_is_done(match, team_nr, player_nr, btp_settings) { + if(match.bts_players && match.bts_players.length > team_nr) { + if(match.bts_players[team_nr] && match.bts_players[team_nr].length > player_nr) { + const player = match.bts_players[team_nr][player_nr]; + + if (player.CheckedIn[0]) { + return; + } + + if (player.LastTimeOnCourt && player.LastTimeOnCourt[0]) { + const date = new Date(player.LastTimeOnCourt[0].year, + player.LastTimeOnCourt[0].month - 1, + player.LastTimeOnCourt[0].day, + player.LastTimeOnCourt[0].hour, + player.LastTimeOnCourt[0].minute, + player.LastTimeOnCourt[0].second, + player.LastTimeOnCourt[0].ms); + const last_time_on_court_ts = date.getTime(); + const now = new Date(); + + if ((now - last_time_on_court_ts) > btp_settings.pause_duration_ms) { + return player.ID[0]; + } + return; + } else { + return player.ID[0]; + } + } + } + return; +} + function integrate_umpires(app, tournament_key, btp_state, callback) { const admin = require('./admin'); // avoid dependency cycle const stournament = require('./stournament'); // avoid dependency cycle @@ -642,12 +762,15 @@ async function integrate_now_on_court(app, tkey, callback) { if (setup.tabletoperators) { for (let operator of setup.tabletoperators) { operator.checked_in = false; - operator.last_time_on_court_ts = called_timestamp; } } btp_manager.update_players(app, tkey, setup.tabletoperators); + if (setup.highlight == 6) { + setup.highlight = 0; + } + const match_q = {_id: match_id}; app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { if (err) { @@ -655,6 +778,8 @@ async function integrate_now_on_court(app, tkey, callback) { return; } + btp_manager.update_highlight(app, match); + const court_q = {_id: court_id}; app.db.courts.find(court_q, (err, courts) => { if (err) { @@ -671,8 +796,7 @@ async function integrate_now_on_court(app, tkey, callback) { admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, btp_winner: match.btp_winner, - setup: match.setup, - from: "btp_sync.js:664"}); + setup: match.setup}); admin.notify_change(app, tkey, 'match_called_on_court', match); bupws.handle_score_change(app, tkey, court_id); async.waterfall([ wcb => set_player_on_court(app, tkey, match.setup, wcb), @@ -867,6 +991,7 @@ function fetch(app, tkey, response, callback) { async.waterfall([ cb => integrate_btp_settings(app, tkey, btp_state, cb), + cb => integrate_player_state(app, tkey, btp_state, cb), cb => integrate_umpires(app, tkey, btp_state, cb), cb => integrate_courts(app, tkey, btp_state, cb), (court_map, cb) => integrate_matches(app, tkey, btp_state, court_map, cb), diff --git a/bts/http_api.js b/bts/http_api.js index c730851..958f64f 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -270,9 +270,10 @@ function score_handler(req, res) { if (update.team1_won != undefined && update.team1_won != null) { async.waterfall([ - cb => remove_player_on_court(req.app, tournament_key, match_id, cb), - cb => remove_tablet_on_court(req.app, tournament_key, match_id, cb), + cb => remove_player_on_court(req.app, tournament_key, match_id, update.end_ts, cb), + cb => remove_tablet_on_court(req.app, tournament_key, match_id, update.end_ts, cb), cb => add_player_to_tabletoperator_list(req.app, tournament_key, match_id, update.end_ts, cb) + ], function(err) { if (err) { res.json({ @@ -327,6 +328,18 @@ function score_handler(req, res) { cb(null, match, changed_court); }, + (match, changed_court, cb) => { + if (match.setup.highlight && + match.setup.highlight == 6 && + match.network_score && + match.network_score.length > 0 && + match.network_score[0].length > 1 && + (match.network_score[0][0] > 0 || match.network_score[0][1] > 0)) { + match.setup.highlight = 0; + btp_manager.update_highlight(req.app, match); + } + cb(null, match, changed_court); + }, (match, changed_court, cb) => { if (changed_court) { ticker_manager.pushall(req.app, tournament_key); @@ -408,7 +421,8 @@ function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, en }); }); } -function remove_player_on_court (app, tkey, cur_match_id, callback) { + +function remove_player_on_court (app, tkey, cur_match_id, end_ts, callback) { const admin = require('./admin'); // avoid dependency cycle app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { if (err) return callback(err); @@ -419,6 +433,11 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { } async.each(matches, (match, cb) => { + if(!match.setup) + { + return cb(null); + } + if(match.setup.now_on_court == true) { return cb(null); } @@ -441,6 +460,8 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && match.setup.teams[0].players[0].now_playing_on_court) { match.setup.teams[0].players[0].now_playing_on_court = false; + match.setup.teams[0].players[0].checked_in = false; + match.setup.teams[0].players[0].last_time_on_court_ts = end_ts; change = true; } @@ -448,6 +469,8 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id) && match.setup.teams[0].players[1].now_playing_on_court) { match.setup.teams[0].players[1].now_playing_on_court = false; + match.setup.teams[0].players[1].checked_in = false; + match.setup.teams[0].players[1].last_time_on_court_ts = end_ts; change = true; } @@ -455,6 +478,8 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && match.setup.teams[1].players[0].now_playing_on_court) { match.setup.teams[1].players[0].now_playing_on_court = false; + match.setup.teams[1].players[0].checked_in = false; + match.setup.teams[1].players[0].last_time_on_court_ts = end_ts; change = true; } @@ -462,6 +487,8 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id) && match.setup.teams[1].players[1].now_playing_on_court) { match.setup.teams[1].players[1].now_playing_on_court = false; + match.setup.teams[1].players[1].checked_in = false; + match.setup.teams[1].players[1].last_time_on_court_ts = end_ts; change = true; } @@ -471,7 +498,7 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { if (err) return cb(err); - admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, + admin.notify_change(app, match.tournament_key, 'update_player_status',{ match__id: match._id, btp_winner: match.btp_winner, setup: match.setup}); return cb(null); @@ -485,7 +512,7 @@ function remove_player_on_court (app, tkey, cur_match_id, callback) { } -function remove_tablet_on_court (app, tkey, cur_match_id, callback) { +function remove_tablet_on_court (app, tkey, cur_match_id, end_ts, callback) { const admin = require('./admin'); // avoid dependency cycle app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { if (err) return callback(err); @@ -517,38 +544,34 @@ function remove_tablet_on_court (app, tkey, cur_match_id, callback) { let change = false; if (match.setup.teams[0].players.length > 0 && - remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && - ( match.setup.teams[0].players[0].now_tablet_on_court == true || - match.setup.teams[0].players[0].checked_in == false)) { + remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { match.setup.teams[0].players[0].now_tablet_on_court = false; - match.setup.teams[0].players[0].checked_in = true; + match.setup.teams[0].players[0].checked_in = false; + match.setup.teams[0].players[0].last_time_on_court_ts = end_ts; change = true; } if (match.setup.teams[0].players.length > 1 && - remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id) && - ( match.setup.teams[0].players[1].now_tablet_on_court == true || - match.setup.teams[0].players[1].checked_in == false)) { + remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { match.setup.teams[0].players[1].now_tablet_on_court = false; - match.setup.teams[0].players[1].checked_in = true; + match.setup.teams[0].players[1].checked_in = false; + match.setup.teams[0].players[1].last_time_on_court_ts = end_ts; change = true; } if (match.setup.teams[1].players.length > 0 && - remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && - ( match.setup.teams[1].players[0].now_tablet_on_court == true || - match.setup.teams[1].players[0].checked_in == false)) { + remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { match.setup.teams[1].players[0].now_tablet_on_court = false; - match.setup.teams[1].players[0].checked_in = true; + match.setup.teams[1].players[0].checked_in = false; + match.setup.teams[1].players[0].last_time_on_court_ts = end_ts; change = true; } if (match.setup.teams[1].players.length > 1 && - remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id) && - ( match.setup.teams[1].players[1].now_tablet_on_court == true || - match.setup.teams[1].players[1].checked_in == false)) { + remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { match.setup.teams[1].players[1].now_tablet_on_court = false; - match.setup.teams[1].players[1].checked_in = true; + match.setup.teams[1].players[1].checked_in = false; + match.setup.teams[1].players[1].last_time_on_court_ts = end_ts; change = true; } diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 4811f13..a1c7220 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -85,7 +85,15 @@ text-align: left; } .match_table > tbody > tr:hover > td { - background-color: #ddd; + background-color: #00000030; +} + +.match_table > tbody > tr:nth-of-type(even) > td { + background-color: #00000010; +} + +.match_table > tbody > tr:nth-of-type(even):hover > td { + background-color: #00000030; } .match_cancel_link { @@ -218,6 +226,25 @@ match_second_call_button, background-color: inherit; } +@font-face { + font-family: "SEGMENT"; + src: url("./seg7.ttf"); + } + +.player > .timer { + display: inline-block; + background-color: #cc0000; + vertical-align: middle; + font-size: 0.7em; + padding: 0.2em 0.3em 0.0em 0.3em; + margin: -0.3em 0.2em 0em 0.2em; + border-radius: 0.4em; + font-weight: normal; + text-align: right; + min-width: 2.9em; + font-family: "SEGMENT"; +} + .tablet, .tablet_inline, .umpire, @@ -293,6 +320,35 @@ match_second_call_button, line-height: 100%; } +.highlight_0 { + background-color: #ffffff; +} + +.highlight_1 { + background-color: #ffd838; +} + +.highlight_2 { + background-color: #ff79ab; +} + +.highlight_3 { + background-color: #fbad4b; +} + +.highlight_4 { + background-color: #1dace6; +} + +.highlight_5 { + background-color: #5ce1b8; +} + +.highlight_6 { + background-color: #c56bff; +} + + @media print { .toprow, diff --git a/static/css/seg7.ttf b/static/css/seg7.ttf new file mode 100644 index 0000000000000000000000000000000000000000..85c7b6828fdb61947edb51d9f45a74ce3ffea60d GIT binary patch literal 5620 zcmbVQZ)_aJ6@Rn4cjqM3u|q;gFlFt8IREZ!-<|(7oK+z81<@p?c0xgb68myKJBfYf zd z?;mVSKDs)0JCPVge{XJlX!5U5{PQxA(~hzK<_?#AYH_|#dhHsoyu6AKKS2{k{L-+cn%>qZo+1_aZ9j<2p#K~6 zM-LWrL)mXVco_8HM~{pT9hsyuJ%;ZOpzlu%jpv`c>-ft=%dno)KUthAyX(DG;Ge7O0jvote9OQ0&QD}5(t<@;*iGaG|bpv<``bnD2Gqqsn)Mjm%g1gC<3 zI61Wvf8*yOHL{5|QcMjR1C?ytQlp?T`zN#$@ zJT%g`kKQ7m&bp}}MUN{#j#qot8L|aa=(|bjNAxj9q-C4PBk4Cu$m-EDJX@$0+JA+5=|0*@ zS$cpTr0ukm9-`g!C_O=YC{LqQq!JyXv+xwsG;wOzG<_DDVbDx~rYvcKzXyK{J`Da8 zTn#Pwq!qW(O1hm^(Q3Lw&TGPbC+;uP zT~NNcWewb3j?>-i2*z8#L3O_PW8)VmwNR?qhL7`KmL35TN9awuDjGyq92Td=N6vC* zuk)&N&Y5)+?udKbz2tuEt@d_#&v?J`W+R&;nMgVELgZ3(S@izsNc6|i)6oxOPAmzF z!hc25c^!s~QR0lCw#;anDXXo{F z`nq~TZz1VuM$u_1J)b8^(+D~#_K6$fqC{N8*0gq~AkL@p-cQ6!U0rDs$Rq+_zbwCZ zE1pa5Nz~nw=<4iDcXd%z#*#CP_?5CXV&)*Lt$ttO-l(D!E@VC4SA2HLC3PWK~izJ}2aCL0VJtI}oxL-L2L& zg*N$#^9$I9`K2u;VQwBWV@BDff{n&SC|sBy>%>@yI}A&Wuou+WpN&3yrJ~Dz=+a(; zz2M#9N2VD zXJ99z(e!I)sbNL-V)hBcWsw_faf3X0Gm&H3(xQ_YKedJpX;u|U8#xM}SumbWR85Ud zc}#2<7Nm-BNCL2zF`{!4@*PJ9^VYGjNBO4oe9^(aD1F1k#gd0OV=g4Ow&RWvb~Xzl z72$ur=$^u~ZO%7V+R$V@INoZodw!vO=Ny+wZZ21u>toLi`wMe0F|qKmX{EBO;#SEY z(xxUq3pWd&aZ5cPa zer)J{cFtZhJ}utF(aFyme&PC{mS3ncP&%qyX5$&oWTtX>$$uHh=o;zOo=)0S8=?At3dF8e>%jk4~W zS2M{vvZhYrjBVCNoVcxi!T5*kM#iXSU9)0jkPIGXrKmBw>3TI>_gVZgT>V+RGE@c1 zeInzSSk*)VXFUr>9OpK4IKmY$@Oz03)>k#XfsMJYwxPKq+s#~w_p|Cc*w4bqu$&f5 zSk4d{T1ZFASl#1!UG{6e))wW3OwUA09&y9)@fy2T>UHUR7OdCij$E^ItnSrsDNMEWt@(zryOm-JBbsL4p?h5+5?z3 zjFS|3BI0B9BL1dmG0s~`7|qjDG^AQS#qoV7_0ulAvv?2SJ&3cJ?bJujL|coKM@wUc z(Xzijx4}=gcl2!b2a3b_QrX{LEKV)Xmh$Yzowk zM!taOfa)HigNic^@&$5YPa@g$}2 zr2}*0bDC4OI8n|PONG3jOtkx3{OZw;t`48% Usg%$+x9IX~FZ>xJHD{dv15u4{j{pDw literal 0 HcmV?d00001 diff --git a/static/css/seven-segment.ttf b/static/css/seven-segment.ttf new file mode 100644 index 0000000000000000000000000000000000000000..214053f88d735871f7be7d587415d1965838ca36 GIT binary patch literal 29076 zcmeHwdvqMtndiOLUENZ*TI%YS)U9{5S`W*5w`>a;J65-ijIlAs4^SHikg;ro@gv3< zCQ8`EAv{b57-#hX&&N2P~j&Hu@;MkG>T6Oua@#7N4l!tFQeoXfpo?kIm zx(C-odynipxNWNUON@CwgX`n_#_l+R^FiEi3-%@ZZvTtDkNqNcIb*(0F*f$-{_(Lr zv9qy#xPKY0uh@?hHM>IPI9`Y2mi-5h-97uYucplozLkGw1 zKEnT=e-`EasIMOyJ2*c3!t$ex-Coa_D|O`X9moE*?71Pv4&RJAzddqv{K!MEcXZ(Q zsLpmXf(5=izwzlW-&C>c-&qs7h0o8bZo44&2Oec?=Izj7&l@-{VG5na2fy*WF@xuZ z{*|%W+0bEeCwWb79}s6AU^`ibIHBNJVb%0gzFM4dyI$mvGL^a20Ts~VO>*ygk?m0c zvE1kLjCpawel{D$hYd4d>qXDrnZ48!K91Th`i1fj$~Umn8CTw91Vg5L8JB)4zY1#e zyV-=YhE2F{#MjLx)W2pE=*5J~XML~6xeaWB{~X7tNA=y;$-1J9-?+XgkLmaHbsmfR zUyJj5@f_M)b?m)em-cp)h9J*9z-VKd8O5_k0@V>0Dj=d4IBJ z|5Ww^@Safr5mcg}9gndI8VA}tVNlqH!fWCfFck2(lSUc$u=dJkjN{kwZI@s63+#8} zA^6gm*~;1A`#1I<>;jwCeR`!H)$8?M{U-gZ ztrur!aR+%1oh@Oj*qGxU-(=ro&#@n|pW`0CWWQ!_vbWef?4r)}V8K23>^rmnG5fz} zU!Q$#_SM;M&OSYRX7;JsC&j(RX7;agWyW5) zv-RR{Bcs1-t-mAeL+pC|9c4FQR6O`vPvQy i=MTvR>Yl2V_al?BQxf?6nCSydg0 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 U#t2b^z4)6eo+h+#zGLwJ0uWpRasU7T literal 0 HcmV?d00001 diff --git a/static/icons/umpire.svg b/static/icons/umpire.svg index 1e1c29a..530d1c6 100644 --- a/static/icons/umpire.svg +++ b/static/icons/umpire.svg @@ -35,18 +35,18 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="44.8" - inkscape:cx="19.169409" - inkscape:cy="20.654098" + inkscape:zoom="45.254834" + inkscape:cx="14.804945" + inkscape:cy="11.004062" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="true" inkscape:snap-page="false" inkscape:snap-grids="true" showguides="false" - inkscape:window-width="3840" + inkscape:window-width="5120" inkscape:window-height="2096" - inkscape:window-x="1238" + inkscape:window-x="0" inkscape:window-y="27" inkscape:window-maximized="1" fit-margin-top="0" @@ -77,7 +77,7 @@ id="layer1" transform="translate(-1.7197918,-2.1218714)"> { return (m.setup.event_name === t.setup.event_name && m.setup.is_match && + m.setup.links && + t.setup.links && ( m.setup.links.from1 == t.setup.links.from1 || m.setup.links.from2 == t.setup.links.from2 @@ -300,7 +302,7 @@ function render_players_el(parentNode, setup, team_id, match, show_player_status const index = Math.min(resolved_match.length - 1, team_id); - if(resolved_match.length > 0) { + if(resolved_match.length >= index && resolved_match[index] && resolved_match[index].setup && resolved_match[index].setup.links) { if(resolved_match[index].setup.links.winner_to && resolved_match[index].setup.links.winner_to == match.btp_match_ids[0].planning) { dependency = ci18n('Winner') + " #" + resolved_match[index].setup.match_num + " - " + resolved_match[index].setup.scheduled_date + " " + resolved_match[index].setup.scheduled_time_str; @@ -348,6 +350,11 @@ function render_player_el(parentNode, player, match_id, now_on_court, show_playe let court_number = parts[parts.length - 1]; uiu.el(player_element, 'div', 'tablet_inline', court_number); } + + if(show_player_status) { + var timer_state = _extract_player_timer_state(player); + var timer = create_timer(timer_state, player_element, "#ffffff", "#ffffff"); + } } function get_player_status(player, now_on_court, show_player_status) { @@ -401,6 +408,12 @@ function update_player(match_id, player, now_on_court, show_player_status) { let court_number = parts[parts.length - 1]; uiu.el(player_el, 'div', 'tablet_inline', court_number); } + + if(show_player_status) { + var timer_state = _extract_player_timer_state(player); + var timer = create_timer(timer_state, player_el, "#ffffff", "#ffffff"); + } + }); } @@ -418,7 +431,7 @@ function create_timer(timer_state, parent, default_color, exigent_color) { return; } - let el = uiu.el(parent, 'div', {style: ('color:' + default_color +';')}, tv.str); + let el = uiu.el(parent, 'div', {class: 'timer', style: ('color:' + default_color +';')}, tv.str); var tobj = {} @@ -433,8 +446,10 @@ function create_timer(timer_state, parent, default_color, exigent_color) { if (visible && tv.next) { tobj.timeout = setTimeout(update, tv.next); + el.style.display = "inline-block"; } else { tobj.timeout = null; + el.style.display = "none"; } }; @@ -443,6 +458,20 @@ function create_timer(timer_state, parent, default_color, exigent_color) { return tobj; } +function _extract_player_timer_state(player) { + let s = {}; + s.settings = {}; + s.settings.negative_timers = false; + s.lang = "de"; + s.timer = {}; + s.timer.duration = curt.btp_settings.pause_duration_ms; + s.timer.start = (player.last_time_on_court_ts ? player.last_time_on_court_ts : false); + s.timer.upwards = false; + s.timer.exigent = false; + + return s; +} + function _extract_match_timer_state(match) { var presses = match.presses; @@ -506,7 +535,18 @@ function on_scoresheet_button_click(e) { function on_announce_preparation_matchbutton_click(e) { const match = fetchMatchFromEvent(e); if (match != null) { - announcePreparationMatch(match.setup); + match.setup.highlight = 6; //its magenta + + send({ + type: 'match_preparation_call', + id: match._id, + tournament_key: match.tournament_key, + setup: match.setup, + }, function (err) { + if (err) { + return cerror.net(err); + } + }); } } function on_second_call_team_one_button_click(e) { @@ -821,7 +861,7 @@ function render_match_table(container, matches, include_courts, show_player_stat const tbody = uiu.el(table, 'tbody'); for (const m of matches) { - const tr = uiu.el(tbody, 'tr'); + const tr = uiu.el(tbody, 'tr', {'class' : 'highlight_' + m.setup.highlight , 'data-btp_id': m.btp_id}); render_match_row(tr, m, null, include_courts ? 'default' : 'plain', show_player_status); } } @@ -1265,6 +1305,7 @@ function render_umpire_options(select, curval, is_service_judge) { } function render_create(container) { + /* uiu.empty(container); const form = uiu.el(container, 'form'); @@ -1292,6 +1333,7 @@ function render_create(container) { render_create(container); }); }); + */ } return { From ad156392f9ff64a0f5e196fc3b8d9a08b526737e Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 3 Apr 2024 02:55:55 +0200 Subject: [PATCH 030/224] fix missing await while set new tablet operator --- bts/btp_sync.js | 10 +- static/css/ctabletoperator.css | 4 +- static/icons/tabletoperator_add.svg | 744 +++++++++++-------------- static/icons/tabletoperator_remove.svg | 413 ++++++++++++-- 4 files changed, 716 insertions(+), 455 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 1386b6e..8668547 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -750,10 +750,8 @@ async function integrate_now_on_court(app, tkey, callback) { if(!setup.called_timestamp) { setup.called_timestamp = called_timestamp; try{ - const promise = serializedAsyncTask(admin, app, tkey, court_id, setup.umpire_name); - promise.then((value) => { - setup.tabletoperators = value; - }); + const value = await serializedAsyncTask(admin, app, tkey, court_id, setup.umpire_name); + setup.tabletoperators = value; } catch (err) { callback(err) @@ -841,10 +839,10 @@ function get_last_looser_on_court(admin, app, tkey, court_id, umpire_name) { app.db.tabletoperators.update({ _id: tabletoperator[0]._id, tournament_key: tkey }, { $set: { court: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_tabletoperator) { if (err) { ws.respond(msg, err); - return; + return reject(err); } admin.notify_change(app, tkey, 'tabletoperator_removed', { tabletoperator: changed_tabletoperator }); - resolve(returnvalue); + return resolve(returnvalue); }); } }); diff --git a/static/css/ctabletoperator.css b/static/css/ctabletoperator.css index 4f4aaa1..2d447f5 100644 --- a/static/css/ctabletoperator.css +++ b/static/css/ctabletoperator.css @@ -7,7 +7,7 @@ background-position: center center; height: 1.2em; background-size: cover; - width: 1.9em; + width: 1.0em; margin: 0 0.2em -0.2em 0.2em; background-image: url(../icons/tabletoperator_add.svg); } @@ -20,7 +20,7 @@ background-position: center center; height: 1.2em; background-size: cover; - width: 1.9em; + width: 1.1em; margin: 0 0.2em -0.2em 0.2em; background-image: url(../icons/tabletoperator_remove.svg); } diff --git a/static/icons/tabletoperator_add.svg b/static/icons/tabletoperator_add.svg index d971385..7a3f9f2 100644 --- a/static/icons/tabletoperator_add.svg +++ b/static/icons/tabletoperator_add.svg @@ -2,6 +2,7 @@ + width="8.3378954mm" + height="9.1316557mm" + viewBox="0 0 8.3378956 9.1316557" + version="1.1" + id="svg8" + inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)" + sodipodi:docname="tabletoperator_add.svg"> + id="defs2"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + id="linearGradient893"> + id="stop889" /> - + id="stop891" /> + id="linearGradient937" + osb:paint="solid"> - - + id="stop935" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + gradientTransform="matrix(0.1187258,0.03209227,-0.03020677,0.09981078,8.5923258,3.7722342)" /> - + + inkscape:window-y="27" + inkscape:window-maximized="1" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0"> + id="grid854" + originx="-5.4883454" + originy="-285.41751" /> + id="metadata5"> image/svg+xml - + + id="layer1" + transform="translate(-5.4883453,-2.4508497)"> + + + + + + + + + + + +1 + +1 + + + + + + + style="fill:#cfcfcf;fill-opacity:1;stroke:#000000;stroke-width:0.01163812;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect870-6" + width="0.3904826" + height="0.22072902" + x="10.465944" + y="3.673038" + transform="matrix(0.96535476,0.26094095,-0.28966559,0.95712792,0,0)" /> + +1 + +1 + + + + + + style="fill:#cfcfcf;fill-opacity:1;stroke:#000000;stroke-width:0.01163812;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect870-2" + width="0.39048272" + height="0.22072902" + x="9.5775108" + y="5.0088348" + ry="0" + transform="matrix(0.96535476,0.26094095,-0.28966559,0.95712792,0,0)" /> + style="fill:#cfcfcf;fill-opacity:1;stroke:#000000;stroke-width:0.01163812;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="rect870-6-0" + width="0.3904826" + height="0.22072902" + x="10.423557" + y="5.0088348" + transform="matrix(0.96535476,0.26094095,-0.28966559,0.95712792,0,0)" /> + +1 + +1 + style="opacity:1;fill:#0c0c0c;fill-opacity:1;stroke:none;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" + id="rect990" + width="1.8797476" + height="0.50500685" + x="11.457085" + y="2.3950338" + transform="rotate(13.62988)" + inkscape:transform-center-x="-0.20285084" + inkscape:transform-center-y="-0.44689482" /> + style="opacity:1;fill:#0c0c0c;fill-opacity:1;stroke:none;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" + id="rect990-2" + width="1.8797476" + height="0.50500685" + x="11.311042" + y="3.8068385" + transform="rotate(13.62988)" + inkscape:transform-center-x="-0.20285084" + inkscape:transform-center-y="-0.44689482" /> + style="opacity:1;fill:#0c0c0c;fill-opacity:1;stroke:none;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" + id="rect990-8" + width="1.8797476" + height="0.50500685" + x="11.222168" + y="5.1232448" + transform="rotate(13.62988)" + inkscape:transform-center-x="-0.20285084" + inkscape:transform-center-y="-0.44689482" /> + + style="opacity:1;fill:#0c0c0c;fill-opacity:1;stroke:none;stroke-width:0.26499999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke" + id="rect990-8-3" + width="1.8797476" + height="0.50500685" + x="11.120723" + y="6.6829324" + transform="rotate(13.62988)" + inkscape:transform-center-x="-0.20285084" + inkscape:transform-center-y="-0.44689482" /> diff --git a/static/icons/tabletoperator_remove.svg b/static/icons/tabletoperator_remove.svg index 0258d87..c10c042 100644 --- a/static/icons/tabletoperator_remove.svg +++ b/static/icons/tabletoperator_remove.svg @@ -8,17 +8,30 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="13.49375mm" - height="9.5249996mm" - viewBox="0 0 13.49375 9.5249996" + width="8.0547562mm" + height="8.7418766mm" + viewBox="0 0 8.0547564 8.7418766" version="1.1" id="svg8" inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)" - sodipodi:docname="court_history.svg"> + sodipodi:docname="tabletoperator_remove.svg"> + + + + @@ -27,6 +40,46 @@ offset="0" id="stop935" /> + + + + + originx="-5.6383842" + originy="-285.61781" /> @@ -75,7 +128,7 @@ inkscape:label="Rahmen" inkscape:groupmode="layer" id="layer1" - transform="translate(-1.7197918,-2.1218714)"> + transform="translate(-5.6383839,-2.6403119)"> + + + + + + + + + +1 + +1 + + + + + + + +1 + +1 + + + + + +1 + +1 + + + + + + + + + + + + +1 + +1
    From 4283a0bc548452b0f186550c49df8c72a03a14a2 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 3 Apr 2024 13:03:29 +0200 Subject: [PATCH 031/224] Play the announcements on all open admin pages. This makes it possible to provide announcements Play the announcements on all open admin pages. This makes it possible to provide announcements in several halls (or e.g. in the foyer).in several halls (or e.g. in the foyer).play the anouncements on all admin sites (so it's posible to use the anouncements in different halls) --- bts/admin.js | 56 +++++++++++++++++++++++++++++++++++++++++++++ static/js/change.js | 12 ++++++++++ static/js/cmatch.js | 51 +++++++++++++++++++++++++++++++++-------- 3 files changed, 109 insertions(+), 10 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index eee4b7b..ffd9e12 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -340,6 +340,58 @@ function handle_match_preparation_call(app, ws, msg) { ws.respond(msg, err); }); } +function handle_begin_to_play_call(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + notify_change(app, tournament_key, 'begin_to_play_call', {setup}); + + ws.respond(msg); +} + +function handle_second_call_tabletoperator(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + notify_change(app, tournament_key, 'second_call_tabletoperator', {setup}); + + ws.respond(msg); +} + +function handle_second_call_team_one(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + notify_change(app, tournament_key, 'second_call_team_one', {setup}); + + ws.respond(msg); +} + +function handle_second_call_team_two(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + notify_change(app, tournament_key, 'second_call_team_two', {setup}); + + ws.respond(msg); +} + async function async_handle_match_delete(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'id'])) { @@ -484,6 +536,7 @@ async function async_handle_tournament_upload_logo(app, ws, msg) { module.exports = { async_handle_match_delete, async_handle_tournament_upload_logo, + handle_begin_to_play_call, handle_btp_fetch, handle_tabletoperator_add, handle_tabletoperator_remove, @@ -495,6 +548,9 @@ module.exports = { handle_match_preparation_call, handle_ticker_pushall, handle_ticker_reset, + handle_second_call_tabletoperator, + handle_second_call_team_one, + handle_second_call_team_two, handle_tournament_get, handle_tournament_list, handle_tournament_edit_props, diff --git a/static/js/change.js b/static/js/change.js index 99d79e5..3a8589b 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -131,6 +131,18 @@ function default_handler_func(rerender, special_funcs, c) { case 'match_called_on_court': announceNewMatch(c.val.setup); break; + case 'begin_to_play_call': + announceBeginnToPlay(c.val.setup); + break; + case 'second_call_tabletoperator': + announceSecondCallTabletoperator(c.val.setup); + break; + case 'second_call_team_one': + announceSecondCallTeamOne(c.val.setup); + break; + case 'second_call_team_two': + announceSecondCallTeamTwo(c.val.setup); + break; case 'umpires_changed': curt.umpires = c.val.all_umpires; uiu.qsEach('select[name="umpire_name"]', function(select) { diff --git a/static/js/cmatch.js b/static/js/cmatch.js index e4bc65c..3f853ce 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -549,13 +549,6 @@ function on_announce_preparation_matchbutton_click(e) { }); } } -function on_second_call_team_one_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - announceSecondCallTeamOne(match.setup); - } -} - function on_add_to_tabletoperators_team_one_button_click(e) { const match = fetchMatchFromEvent(e); add_to_tabletoperator(match, 0) @@ -579,23 +572,61 @@ function add_to_tabletoperator(match,team_num) { }); } } +function on_second_call_team_one_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_call_team_one', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +} function on_second_call_team_two_button_click(e) { const match = fetchMatchFromEvent(e); if (match != null) { - announceSecondCallTeamTwo(match.setup); + send({ + type: 'second_call_team_two', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); } } function on_second_call_tabletoperator_button_click(e) { const match = fetchMatchFromEvent(e); if (match != null) { - announceSecondCallTabletoperator(match.setup); + send({ + type: 'second_call_tabletoperator', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); } } function on_begin_to_play_button_click(e) { const match = fetchMatchFromEvent(e); if (match != null) { - announceBeginnToPlay(match.setup); + send({ + type: 'begin_to_play_call', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); } } function fetchMatchFromEvent(e) { From 26d221654128f6a2436f6dc9eaf3116c41f7a635 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Wed, 3 Apr 2024 21:37:43 +0200 Subject: [PATCH 032/224] Bugfix: System hangs if no tabletoperator is available --- bts/btp_sync.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 8668547..48ed5d6 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -761,10 +761,9 @@ async function integrate_now_on_court(app, tkey, callback) { for (let operator of setup.tabletoperators) { operator.checked_in = false; } + btp_manager.update_players(app, tkey, setup.tabletoperators); } - btp_manager.update_players(app, tkey, setup.tabletoperators); - if (setup.highlight == 6) { setup.highlight = 0; } @@ -838,13 +837,13 @@ function get_last_looser_on_court(admin, app, tkey, court_id, umpire_name) { returnvalue = tabletoperator[0].tabletoperator app.db.tabletoperators.update({ _id: tabletoperator[0]._id, tournament_key: tkey }, { $set: { court: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_tabletoperator) { if (err) { - ws.respond(msg, err); return reject(err); } admin.notify_change(app, tkey, 'tabletoperator_removed', { tabletoperator: changed_tabletoperator }); return resolve(returnvalue); }); } + return resolve(returnvalue); }); }); } From f94ac780c77d1608eb6ba936740aa462ed94ce3a Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 4 Apr 2024 02:02:04 +0200 Subject: [PATCH 033/224] create only table rows for Matches that are playable (in the List are also dummy matches that are used to represent links) --- static/js/cmatch.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 3f853ce..68ba55b 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -892,8 +892,10 @@ function render_match_table(container, matches, include_courts, show_player_stat const tbody = uiu.el(table, 'tbody'); for (const m of matches) { - const tr = uiu.el(tbody, 'tr', {'class' : 'highlight_' + m.setup.highlight , 'data-btp_id': m.btp_id}); - render_match_row(tr, m, null, include_courts ? 'default' : 'plain', show_player_status); + if(m.setup.is_match) { + const tr = uiu.el(tbody, 'tr', {'class' : 'highlight_' + m.setup.highlight , 'data-btp_id': m.btp_id}); + render_match_row(tr, m, null, include_courts ? 'default' : 'plain', show_player_status); + } } } From 3c96b26299ca0830e904ec59b36566eb01563f2b Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 4 Apr 2024 02:04:35 +0200 Subject: [PATCH 034/224] bugfix: iterate only over teams/players (in bts/btp_sync.js integrate_player_state()) that exist. --- bts/btp_sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 48ed5d6..f9cfedf 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -554,8 +554,8 @@ async function integrate_player_state(app, tkey, btp_state, callback) { return; } - for (let team_nr = 0; team_nr < 2; team_nr++) { - for (let player_nr = 0; player_nr < 2; player_nr++) { + for (let team_nr = 0; team_nr < cur_match.setup.teams.length; team_nr++) { + for (let player_nr = 0; player_nr < cur_match.setup.teams[team_nr].players.length; player_nr++) { let id = pause_is_done(match, team_nr, player_nr, tournament.btp_settings); if (id != undefined && id != null) { From 325ff704efbd32311b2ad44fa994981004fccfab Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 4 Apr 2024 02:05:15 +0200 Subject: [PATCH 035/224] add some calls of the callback() function that was missing --- bts/btp_sync.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index f9cfedf..089ac5a 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -410,6 +410,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { app.db.matches.update({_id: cur_match._id}, {$set: match}, {}, (err) => { if (err) { + cb(err); return; }; @@ -551,7 +552,7 @@ async function integrate_player_state(app, tkey, btp_state, callback) { const match_from_db = await get_match_form_db (app, tkey, btp_state, match); cur_match = match_from_db; } catch (err) { - return; + return callback(null); // if db is empty yet its possible next time } for (let team_nr = 0; team_nr < cur_match.setup.teams.length; team_nr++) { @@ -730,7 +731,7 @@ async function integrate_now_on_court(app, tkey, callback) { } assert(tournament); if (!tournament.only_now_on_court) { - return; // Nothing to do here + return callback(null); // Nothing to do here } app.db.matches.find({'setup.now_on_court': true}, async (err, now_on_court_matches) => { From 91a8a68f62f9e31d35e04c081e55094b23f92784 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Thu, 4 Apr 2024 07:12:17 +0200 Subject: [PATCH 036/224] Bugfix: Hardened the detection of the tableoperator again. Prevent undefined access and returned undefined only if no tableoperator was found --- bts/btp_sync.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 48ed5d6..5e69f5c 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -833,7 +833,7 @@ function get_last_looser_on_court(admin, app, tkey, court_id, umpire_name) { return reject(err); } var returnvalue = undefined; - if (tabletoperator.length == 1) { + if (tabletoperator && tabletoperator.length == 1) { returnvalue = tabletoperator[0].tabletoperator app.db.tabletoperators.update({ _id: tabletoperator[0]._id, tournament_key: tkey }, { $set: { court: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_tabletoperator) { if (err) { @@ -842,8 +842,9 @@ function get_last_looser_on_court(admin, app, tkey, court_id, umpire_name) { admin.notify_change(app, tkey, 'tabletoperator_removed', { tabletoperator: changed_tabletoperator }); return resolve(returnvalue); }); + } else { + return resolve(returnvalue); } - return resolve(returnvalue); }); }); } From 5c901ff6feac24098d1a1083a32bd378e08eed1a Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Thu, 4 Apr 2024 20:33:38 +0200 Subject: [PATCH 037/224] Bugfix: Handling of match is not stored in db changed from throwing an exception to return null-value --- bts/btp_sync.js | 48 +++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 33ff966..d379337 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -546,34 +546,28 @@ async function integrate_player_state(app, tkey, btp_state, callback) { let ids_to_change = []; let players_to_change = []; async.eachOfSeries(btp_state.matches, async (match, key) => { - let cur_match = undefined; + let cur_match = await get_match_form_db (app, tkey, btp_state, match); + if (cur_match && cur_match != null) { + for (let team_nr = 0; team_nr < cur_match.setup.teams.length; team_nr++) { + for (let player_nr = 0; player_nr < cur_match.setup.teams[team_nr].players.length; player_nr++) { + let id = pause_is_done(match, team_nr, player_nr, tournament.btp_settings); - try { - const match_from_db = await get_match_form_db (app, tkey, btp_state, match); - cur_match = match_from_db; - } catch (err) { - return callback(null); // if db is empty yet its possible next time - } - - for (let team_nr = 0; team_nr < cur_match.setup.teams.length; team_nr++) { - for (let player_nr = 0; player_nr < cur_match.setup.teams[team_nr].players.length; player_nr++) { - let id = pause_is_done(match, team_nr, player_nr, tournament.btp_settings); - - if (id != undefined && id != null) { + if (id != undefined && id != null) { - if (!cur_match.setup.teams[team_nr].players[player_nr].now_tablet_on_court && - !cur_match.setup.teams[team_nr].players[player_nr].now_playing_on_court && - !cur_match.setup.called_timestamp && - !cur_match.network_score) { + if (!cur_match.setup.teams[team_nr].players[player_nr].now_tablet_on_court && + !cur_match.setup.teams[team_nr].players[player_nr].now_playing_on_court && + !cur_match.setup.called_timestamp && + !cur_match.network_score) { - btp_state.matches[key].bts_players[team_nr][player_nr].CheckedIn[0] = true; + btp_state.matches[key].bts_players[team_nr][player_nr].CheckedIn[0] = true; - const player = cur_match.setup.teams[team_nr].players[player_nr]; - if(ids_to_change.indexOf(id) == -1) { - player.checked_in = true; - ids_to_change.push(id); - players_to_change.push(player); + const player = cur_match.setup.teams[team_nr].players[player_nr]; + if(ids_to_change.indexOf(id) == -1) { + player.checked_in = true; + ids_to_change.push(id); + players_to_change.push(player); + } } } } @@ -616,11 +610,11 @@ async function get_match_form_db (app, tkey, btp_state, match) { return reject(err); }; - if(cur_match) { + if (cur_match) { return resolve(cur_match); - }; - - reject("no match found"); + } else { + return resolve(null); + } }); }); } From 452ca2d60345be4109a4fd7a31779cea8a725ed5 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 5 Apr 2024 02:33:26 +0200 Subject: [PATCH 038/224] Some work to find/fix unnecessary re-rendering: - Add a warning to be displayed in the browser console when a row is re-rendered in the match tables. - Add the update_match function in the cmatch and ctournament files that allow to re-render a specific match without re-rendering all other matches. (Note: currently a match is only re-rendered at its previous location). - The function update_match replaces the case 'match_edit' in change.js - some minor fixes. --- bts/btp_sync.js | 32 +++++++++++++++++++++----------- bts/http_api.js | 3 ++- static/css/cmatch.css | 2 +- static/js/change.js | 11 ----------- static/js/cmatch.js | 26 +++++++++++++++++++++++++- static/js/ctournament.js | 17 +++++++++++++++++ 6 files changed, 66 insertions(+), 25 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 089ac5a..d679d54 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -87,7 +87,7 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, match_num: bm.MatchNr[0], counting: '3x21', team_competition: false, - match_name, + //match_name, event_name, teams, warmup: 'none', @@ -127,6 +127,9 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, } } } + if (match_name) { + setup.match_name = match_name; + } if (scheduled_time_str) { setup.scheduled_time_str = scheduled_time_str; @@ -335,6 +338,10 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { craft_match(app, tkey, btp_id, court_map, event, draw, btp_state.links, officials, bm, match_ids_on_court).then(match => { if (cur_match) { + if (cur_match.team1_won === null) { + cur_match.team1_won = undefined; + } + if(cur_match.setup.called_timestamp) { // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. match.setup.called_timestamp = cur_match.setup.called_timestamp; @@ -414,15 +421,18 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { return; }; - if(!only_change_check_in) { - admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); - } else { - admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); - } + // render onli if is_match flag is set. else it's nessasary to have the game (it's a link) in the db, but not to rerender + if (match.setup.is_match) { + if(!only_change_check_in) { + admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup}); + } else { + admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup}); + } + } }); cb(null); return; @@ -552,7 +562,7 @@ async function integrate_player_state(app, tkey, btp_state, callback) { const match_from_db = await get_match_form_db (app, tkey, btp_state, match); cur_match = match_from_db; } catch (err) { - return callback(null); // if db is empty yet its possible next time + return err; // if db is empty yet its possible next time } for (let team_nr = 0; team_nr < cur_match.setup.teams.length; team_nr++) { diff --git a/bts/http_api.js b/bts/http_api.js index 958f64f..6383b35 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -263,7 +263,8 @@ function score_handler(req, res) { duration_ms: req.body.duration_ms, end_ts: req.body.end_ts, }; - if (update.team1_won !== undefined) { + + if (update.team1_won !== undefined && update.team1_won != null) { update.btp_winner = (update.team1_won === true) ? 1 : 2; update.btp_needsync = true; } diff --git a/static/css/cmatch.css b/static/css/cmatch.css index a1c7220..8279947 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -223,7 +223,7 @@ match_second_call_button, } .now_on_court { - background-color: inherit; + background-color: unset; } @font-face { diff --git a/static/js/change.js b/static/js/change.js index 3a8589b..60ff1a6 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -99,17 +99,6 @@ function default_handler_func(rerender, special_funcs, c) { curt.matches.push(c.val.match); rerender(); break; - case 'match_edit': - { - const changed_m = utils.find(curt.matches, m => m._id === c.val.match__id); - if (changed_m) { - changed_m.setup = c.val.setup; - } else { - cerror.silent('Cannot find edited match ' + c.val.match__id); - } - rerender(); - } - break; case 'match_delete': { const match_id = c.val.match__id; diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 68ba55b..f815233 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -42,6 +42,7 @@ function render_match_table_header(table, include_courts) { } function render_match_row(tr, match, court, style, show_player_status) { + console.warn("rerender_row"); if(!match.setup.is_match) { return; } @@ -417,6 +418,28 @@ function update_player(match_id, player, now_on_court, show_player_status) { }); } +function update_match(m) { + uiu.qsEach('.match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { + match_row_el.innerHTML = ''; + console.log("render single row: match_num = " + m.setup.match_num); + console.log(match_row_el.parentNode); + console.log(match_row_el.parentNode.parentNode); + console.log(match_row_el.parentNode.parentNode.parentNode); + switch (match_row_el.parentNode.parentNode.parentNode.className) { + case 'courts_container': + render_match_row(match_row_el, m, null, 'default', false); + break; + case 'finished_container': + render_match_row(match_row_el, m, null, 'default', false); + break; + case 'unassigned_container': + default: + render_match_row(match_row_el, m, null, 'default', true); + break + } + }); +} + var active_timers = {'matches': {}, 'players' : {}}; function create_timer(timer_state, parent, default_color, exigent_color) { @@ -893,7 +916,7 @@ function render_match_table(container, matches, include_courts, show_player_stat for (const m of matches) { if(m.setup.is_match) { - const tr = uiu.el(tbody, 'tr', {'class' : 'highlight_' + m.setup.highlight , 'data-btp_id': m.btp_id}); + const tr = uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); render_match_row(tr, m, null, include_courts ? 'default' : 'plain', show_player_status); } } @@ -1379,6 +1402,7 @@ return { render_umpire_options, render_upcoming_matches, update_match_score, + update_match, update_players, }; diff --git a/static/js/ctournament.js b/static/js/ctournament.js index c35059d..0f9b4fe 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -140,6 +140,22 @@ function update_player_status(c){ cmatch.update_players(m); } +function update_match(c){ + const cval = c.val; + const match_id = cval.match__id; + + // Find the match + const m = utils.find(curt.matches, m => m._id === match_id); + if (!m) { + cerror.silent('Cannot find match to update player status, ID: ' + JSON.stringify(match_id)); + return; + } + m.btp_winner = cval.btp_winner; + m.setup = cval.setup; + + cmatch.update_match(m); +} + function update_current_match(c) { change.change_current_match(c.val); _show_render_matches(); @@ -270,6 +286,7 @@ _route_single(/t\/([a-z0-9]+)\/$/, ui_show, change.default_handler(_update_all_u score: update_score, court_current_match: update_current_match, update_player_status: update_player_status, + match_edit: update_match, })); function _upload_logo(e) { From ab8ccc18546332329f912881fc7fa0ee7306d63d Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 7 Apr 2024 14:06:12 +0200 Subject: [PATCH 039/224] New: Feature: Tabletoperator can also be choosen from finished matches. Finished Matches will be sort descending last finished match at the top of the list --- static/js/announcements.js | 7 ++++--- static/js/cmatch.js | 22 +++++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/static/js/announcements.js b/static/js/announcements.js index bbdae96..77231ab 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -50,9 +50,10 @@ function createTeamAnnouncement(matchSetup) { function createTabletOperator(matchSetup) { var tabletOperator = "Tabletbedienung: "; - if (matchSetup.teams[1].players[0].state) { - tabletOperator = tabletOperator + matchSetup.teams[1].players[0].state; - } else if (matchSetup.tabletoperators) { + //if (matchSetup.teams[1].players[0].state) { + // tabletOperator = tabletOperator + matchSetup.teams[1].players[0].state; + //} else + if (matchSetup.tabletoperators) { tabletOperator = tabletOperator + createSingleTeam(matchSetup.tabletoperators); } else if (matchSetup.umpire_name) { tabletOperator = tabletOperator + matchSetup.umpire_name; diff --git a/static/js/cmatch.js b/static/js/cmatch.js index f815233..145250e 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -41,7 +41,7 @@ function render_match_table_header(table, include_courts) { uiu.el(title_tr, 'th', {}, ''); } -function render_match_row(tr, match, court, style, show_player_status) { +function render_match_row(tr, match, court, style, show_player_status, show_add_tabletoperator) { console.warn("rerender_row"); if(!match.setup.is_match) { return; @@ -96,7 +96,7 @@ function render_match_row(tr, match, court, style, show_player_status) { 'class': ((match.team1_won === true) ? 'match_team_won' : ''), style: 'text-align: right;', }); - if (!court) { + if (show_add_tabletoperator) { if (setup.teams[0].players.length > 0) { create_match_button(players0, 'vlink tabletoperator_add_button', 'tabletoperator:add', on_add_to_tabletoperators_team_one_button_click, match._id); } @@ -108,7 +108,7 @@ function render_match_row(tr, match, court, style, show_player_status) { uiu.el(tr, 'td', 'match_vs', 'v'); const players1 = uiu.el(tr, 'td', ((match.team1_won === false) ? 'match_team_won ' : '') + 'match_team2'); render_players_el(players1, setup, 1, match, show_player_status); - if (!court) { + if (show_add_tabletoperator) { if (setup.teams[1].players.length > 0) { create_match_button(players1, 'vlink tabletoperator_add_button', 'tabletoperator:add', on_add_to_tabletoperators_team_two_button_click, match._id); } @@ -427,14 +427,14 @@ function update_match(m) { console.log(match_row_el.parentNode.parentNode.parentNode); switch (match_row_el.parentNode.parentNode.parentNode.className) { case 'courts_container': - render_match_row(match_row_el, m, null, 'default', false); + render_match_row(match_row_el, m, null, 'default', false, false); break; case 'finished_container': - render_match_row(match_row_el, m, null, 'default', false); + render_match_row(match_row_el, m, null, 'default', false,true); break; case 'unassigned_container': default: - render_match_row(match_row_el, m, null, 'default', true); + render_match_row(match_row_el, m, null, 'default', true,true); break } }); @@ -904,7 +904,7 @@ crouting.register(/t\/([a-z0-9]+)\/m\/([-a-zA-Z0-9_ ]+)\/scoresheet$/, function( ui_scoresheet(match_id); })); -function render_match_table(container, matches, include_courts, show_player_status) { +function render_match_table(container, matches, include_courts, show_player_status, show_add_tabletoperator) { if(!show_player_status) { show_player_status = false; @@ -917,7 +917,7 @@ function render_match_table(container, matches, include_courts, show_player_stat for (const m of matches) { if(m.setup.is_match) { const tr = uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); - render_match_row(tr, m, null, include_courts ? 'default' : 'plain', show_player_status); + render_match_row(tr, m, null, include_courts ? 'default' : 'plain', show_player_status, show_add_tabletoperator); } } } @@ -927,7 +927,7 @@ function render_unassigned(container) { uiu.el(container, 'h3', {}, ci18n('Unassigned Matches')); const unassigned_matches = curt.matches.filter(m => calc_section(m) === 'unassigned'); - render_match_table(container, unassigned_matches, curt.only_now_on_court, true); + render_match_table(container, unassigned_matches, curt.only_now_on_court, true,true); } function render_upcoming_matches(container) { @@ -952,8 +952,8 @@ function render_finished(container) { uiu.empty(container); uiu.el(container, 'h3', {}, ci18n('Finished Matches')); - const matches = curt.matches.filter(m => calc_section(m) === 'finished'); - render_match_table(container, matches, true); + const matches = curt.matches.filter(m => calc_section(m) === 'finished').sort((a, b) => {return b.end_ts - a.end_ts}); + render_match_table(container, matches, true, false, true); } function render_courts(container, style) { From 24233d8e3f1435d2bce7ead647c45aa4cf0c4568 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 7 Apr 2024 16:57:00 +0200 Subject: [PATCH 040/224] Enhancement: Doubles can do the tabletservice on two courts so we split them if we put them manually to the operatorlist --- bts/admin.js | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index ffd9e12..53ac49b 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -254,25 +254,24 @@ function handle_tabletoperator_add(app, ws, msg) { const team_id = msg.team_id; const match = msg.match const team = match.setup.teams[team_id]; - var tabletoperator = []; - team.players.forEach((player) => { + var tabletoperator = []; tabletoperator.push(player); - }); - const new_tabletoperator = { - tournament_key, - tabletoperator, - 'match_id': 'manually_added', - 'start_ts': Date.now(), - 'end_ts': null, - 'court': null - }; - app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_tabletoperator) { - if (err) { - ws.respond(msg, err); - return; - } - notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_tabletoperator }); + const new_tabletoperator = { + tournament_key, + tabletoperator, + 'match_id': 'manually_added', + 'start_ts': Date.now(), + 'end_ts': null, + 'court': null + }; + app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_tabletoperator) { + if (err) { + ws.respond(msg, err); + return; + } + notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_tabletoperator }); + }); }); } From 47b287d4a46f604e2aed4799690dbc520d5e2fae Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 7 Apr 2024 17:54:01 +0200 Subject: [PATCH 041/224] New Function: Add the possibility to add an tabletoperator by inserting his name manually. There is no further limitation that the tabletoperator is part of active players of the tournament --- bts/admin.js | 64 ++++++++++++++++++++++------------ static/css/ctabletoperator.css | 35 +++++++++++++------ static/js/cmatch.js | 20 +++-------- static/js/ctabletoperator.js | 43 +++++++++++++++++++---- 4 files changed, 106 insertions(+), 56 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 53ac49b..4fd5f70 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -247,32 +247,50 @@ function handle_tabletoperator_add(app, ws, msg) { if (!msg.tournament_key) { return ws.respond(msg, { message: 'Missing tournament_key' }); } - if (!msg.match) { - return ws.respond(msg, { message: 'Missing match' }); - } const tournament_key = msg.tournament_key; - const team_id = msg.team_id; - const match = msg.match - const team = match.setup.teams[team_id]; - team.players.forEach((player) => { - var tabletoperator = []; - tabletoperator.push(player); - const new_tabletoperator = { - tournament_key, - tabletoperator, - 'match_id': 'manually_added', - 'start_ts': Date.now(), - 'end_ts': null, - 'court': null + var team = null; + if (msg.match) { + const team_id = msg.team_id; + const match = msg.match + const team = match.setup.teams[team_id]; + } else if (msg.tabletoperator_name) { + team = { + "players": [ + { + "asian_name": false, + "name": msg.tabletoperator_name, + "firstname": "", + "lastname": "", + "btp_id": -1 + } + ], + "name": "N/N" }; - app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_tabletoperator) { - if (err) { - ws.respond(msg, err); - return; - } - notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_tabletoperator }); + + } + if (team != null) { + team.players.forEach((player) => { + var tabletoperator = []; + tabletoperator.push(player); + const new_tabletoperator = { + tournament_key, + tabletoperator, + 'match_id': 'manually_added', + 'start_ts': Date.now(), + 'end_ts': null, + 'court': null + }; + app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_tabletoperator) { + if (err) { + ws.respond(msg, err); + return; + } + notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_tabletoperator }); + }); }); - }); + } else { + return ws.respond(msg, { message: 'Not enough Information to add a tabletoperator to list' }); + } } function handle_match_edit(app, ws, msg) { diff --git a/static/css/ctabletoperator.css b/static/css/ctabletoperator.css index 2d447f5..f4cb744 100644 --- a/static/css/ctabletoperator.css +++ b/static/css/ctabletoperator.css @@ -1,3 +1,15 @@ +.tabletoperator_add_custom_button { + display: inline-block; + background-size: 100%; + background-repeat: no-repeat; + background-position: center center; + height: 25px; + background-size: cover; + width: 25px; + margin: 0 0.2em -0.2em 0.2em; + background-image: url(../icons/tabletoperator_add.svg); + border-width: 0px; +} .tabletoperator_add_button { display: inline-block; min-width: 1em; @@ -8,7 +20,7 @@ height: 1.2em; background-size: cover; width: 1.0em; - margin: 0 0.2em -0.2em 0.2em; + margin: 0 0.2em 0 0.2em; background-image: url(../icons/tabletoperator_add.svg); } .tabletoperator_remove_button { @@ -25,18 +37,21 @@ background-image: url(../icons/tabletoperator_remove.svg); } +.tabletoperator_add_custom_button:hover, .tabletoperator_remove_button:hover, .tabletoperator_add_button:hover { - opacity: 0.5; - } + opacity: 0.5; +} +.tabletoperator_add_custom_button > *, .tabletoperator_remove_button > *, .tabletoperator_add_button > * { - display: block; - } + display: block; +} +.tabletoperator_add_custom_button > button, .tabletoperator_remove_button > button, .tabletoperator_add_button > button { - margin: 0 0 0 2vmin; - font-size: 5vmin; - padding: 0.1em 0.1em; - min-width: 5em; - } + margin: 0 0 0 2vmin; + font-size: 5vmin; + padding: 0.1em 0.1em; + min-width: 5em; +} diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 145250e..b38aeab 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -574,27 +574,14 @@ function on_announce_preparation_matchbutton_click(e) { } function on_add_to_tabletoperators_team_one_button_click(e) { const match = fetchMatchFromEvent(e); - add_to_tabletoperator(match, 0) + ctabletoperator.add_to_tabletoperator(match, 0) } function on_add_to_tabletoperators_team_two_button_click(e) { const match = fetchMatchFromEvent(e); - add_to_tabletoperator(match,1) + ctabletoperator.add_to_tabletoperator(match,1) } -function add_to_tabletoperator(match,team_num) { - if (match != null) { - send({ - type: 'tabletoperator_add', - tournament_key: curt.key, - team_id: team_num, - match: match, - }, err => { - if (err) { - return cerror.net(err); - } - }); - } -} + function on_second_call_team_one_button_click(e) { const match = fetchMatchFromEvent(e); if (match != null) { @@ -1419,6 +1406,7 @@ if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var countries = require('./countries'); var crouting = require('./crouting'); var ctournament = require('./ctournament'); + var ctabletoperator = require('./ctabletoperator'); var form_utils = require('../bup/js/form_utils'); var uiu = require('../bup/js/uiu'); var utils = require('../bup/js/utils'); diff --git a/static/js/ctabletoperator.js b/static/js/ctabletoperator.js index 1d56ef3..bb4c669 100644 --- a/static/js/ctabletoperator.js +++ b/static/js/ctabletoperator.js @@ -5,10 +5,10 @@ var ctabletoperator = (function() { function render_unassigned(container) { uiu.empty(container); uiu.el(container, 'h3', {}, ci18n('tabletoperator:unassigned')); - const unassigned_tabletoperators = curt.tabletoperators.filter(m => m.court == null); render_tabletoperator_table(container, unassigned_tabletoperators); - } + render_tabletoperator_formular(container); +} function render_tabletoperator_table(container, tabletoperators) { @@ -75,15 +75,44 @@ function create_tabletoperator_button(targetEl, cssClass, title, listener, table }); btn.addEventListener('click', listener); } -//crouting.register(/t\/([a-z0-9]+)\/umpires$/, function(m) { -// ctournament.switch_tournament(m[1], function() { -// render_unassigned(); -// }); -//}, change.default_handler(render_unassigned)); +function render_tabletoperator_formular(target) { + const announcements = uiu.el(target, 'div', '_tabletoperator_container'); + const form = uiu.el(announcements, 'form'); + uiu.el(form, 'input', { + type: 'input', + id: 'tabletoperator_name', + name: 'tabletoperator_name' + }); + const btp_fetch_btn = uiu.el(form, 'button', { + 'class': 'vlink tabletoperator_add_custom_button', + height: 50, + role: 'submit', + }); + form_utils.onsubmit(form, function (d) { + add_to_tabletoperator(null, null, d.tabletoperator_name) + }); +} + +function add_to_tabletoperator(match, team_num, tabletoperator_name) { + if ((match != null && team_num) || tabletoperator_name) { + send({ + type: 'tabletoperator_add', + tournament_key: curt.key, + team_id: team_num, + tabletoperator_name: tabletoperator_name, + match: match, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +} return { render_unassigned, + add_to_tabletoperator }; })(); From dbe2a53858fea13c1cde34bda5670f3395a7571d Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Mon, 8 Apr 2024 22:01:50 +0200 Subject: [PATCH 042/224] Bugfix: Wrong declatration uf a constant corrupts manual tableetoperator manageement --- bts/admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bts/admin.js b/bts/admin.js index 4fd5f70..b9988ec 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -252,7 +252,7 @@ function handle_tabletoperator_add(app, ws, msg) { if (msg.match) { const team_id = msg.team_id; const match = msg.match - const team = match.setup.teams[team_id]; + team = match.setup.teams[team_id]; } else if (msg.tabletoperator_name) { team = { "players": [ From 43045f60b58bd595c5416ecea6f7c6eca4472497 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Mon, 8 Apr 2024 22:15:12 +0200 Subject: [PATCH 043/224] Bugfix: 0 is like undefined so Teams with index 0 were ignored if they shoul be adde to tableoperator list --- static/js/ctabletoperator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/ctabletoperator.js b/static/js/ctabletoperator.js index bb4c669..5e79453 100644 --- a/static/js/ctabletoperator.js +++ b/static/js/ctabletoperator.js @@ -95,7 +95,7 @@ function render_tabletoperator_formular(target) { } function add_to_tabletoperator(match, team_num, tabletoperator_name) { - if ((match != null && team_num) || tabletoperator_name) { + if (match != null || tabletoperator_name) { send({ type: 'tabletoperator_add', tournament_key: curt.key, From a4d6200033f41a621a92cc2564bcac671b9859d8 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Tue, 9 Apr 2024 22:25:22 +0200 Subject: [PATCH 044/224] New Feature: Scores entered in BTP will now take affect in admin GUI. Also the tabletoperator will be determined in this case --- bts/btp_sync.js | 23 +++++++++--- bts/http_api.js | 78 +++++++++++++++++++++------------------- static/js/ctournament.js | 19 ++++++++-- 3 files changed, 76 insertions(+), 44 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index ad35d55..76a20ba 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -404,13 +404,23 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { // equals checked_in changed and check if it was the only change let only_change_check_in = false; - + let result_enterd_in_btp = false; + for(let team_index = 0; team_index < cur_match.setup.teams.length; team_index++) { for (let player_index = 0; player_index < cur_match.setup.teams[team_index].players.length; player_index++){ cur_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; } } - + + if (!cur_match.team1_won && cur_match.team1_won != match.team1_won) { + if (!match.end_ts) { + result_enterd_in_btp = true; + match.end_ts = Date.now(); + const http_api = require('./http_api'); + http_api.add_player_to_tabletoperator_list_by_match(app, tkey, match, match.end_ts); + } + } + if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { only_change_check_in = true; } @@ -423,10 +433,13 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { // render onli if is_match flag is set. else it's nessasary to have the game (it's a link) in the db, but not to rerender if (match.setup.is_match) { - if(!only_change_check_in) { + if (!only_change_check_in || result_enterd_in_btp) { admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); + btp_winner: match.btp_winner, + network_score: result_enterd_in_btp == true ? match.network_score : null, + end_ts: result_enterd_in_btp == true ? match.end_ts : null, + setup: match.setup + }); } else { admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, btp_winner: match.btp_winner, diff --git a/bts/http_api.js b/bts/http_api.js index 6383b35..3bf2746 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -273,7 +273,7 @@ function score_handler(req, res) { async.waterfall([ cb => remove_player_on_court(req.app, tournament_key, match_id, update.end_ts, cb), cb => remove_tablet_on_court(req.app, tournament_key, match_id, update.end_ts, cb), - cb => add_player_to_tabletoperator_list(req.app, tournament_key, match_id, update.end_ts, cb) + cb => add_player_to_tabletoperator_list(req.app, tournament_key, match_id, update.end_ts) ], function(err) { if (err) { @@ -375,54 +375,57 @@ function score_handler(req, res) { }); } -function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, end_ts, callback) { +function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, end_ts) { const admin = require('./admin'); // avoid dependency cycle app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { if (err) { return reject(err); } - app.db.tabletoperators.findOne({ 'tournament_key': tournament_key, 'match_id': cur_match_id }, (err, no_tabletoperator) => { - if (err) { - return reject(err); + add_player_to_tabletoperator_list_by_match(app, tournament_key, cur_match, end_ts) + }); +} + +function add_player_to_tabletoperator_list_by_match(app, tournament_key, cur_match, end_ts) { + app.db.tabletoperators.findOne({ 'tournament_key': tournament_key, 'match_id': cur_match._id }, (err, no_tabletoperator) => { + if (err) { + return reject(err); + } + if (no_tabletoperator == null) { + const round = cur_match.setup.match_name; + var team = null; + if (round == 'VF' || round == 'QF') { + team = cur_match.setup.teams[cur_match.btp_winner - 1]; + } else { + const index = cur_match.btp_winner % 2; + team = cur_match.setup.teams[index]; } - if (no_tabletoperator == null) { - const round = cur_match.setup.match_name; - var team = null; - if (round == 'VF' || round == 'QF') { - team = cur_match.setup.teams[cur_match.btp_winner - 1]; - } else { - const index = cur_match.btp_winner % 2; - team = cur_match.setup.teams[index]; - } - if (team && typeof team.players !== 'undefined') { - var tabletoperator = []; + if (team && typeof team.players !== 'undefined') { + var tabletoperator = []; - team.players.forEach((player) => { - tabletoperator.push(player); - }); + team.players.forEach((player) => { + tabletoperator.push(player); + }); - const new_tabletoperator = { - tournament_key, - tabletoperator, - 'match_id': cur_match_id, - 'start_ts': end_ts, - 'end_ts': null, - 'court': null - }; - app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { - if (err) { - ws.respond(msg, err); - return; - } - admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); - }); + const new_tabletoperator = { + tournament_key, + tabletoperator, + 'match_id': cur_match._id, + 'start_ts': end_ts, + 'end_ts': null, + 'court': null + }; + app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { + if (err) { + ws.respond(msg, err); + return; + } + admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); + }); - } } - }); + } }); } - function remove_player_on_court (app, tkey, cur_match_id, end_ts, callback) { const admin = require('./admin'); // avoid dependency cycle app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { @@ -624,4 +627,5 @@ module.exports = { matches_handler, matchinfo_handler, score_handler, + add_player_to_tabletoperator_list_by_match, }; diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 0f9b4fe..73d4b9d 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -150,10 +150,25 @@ function update_match(c){ cerror.silent('Cannot find match to update player status, ID: ' + JSON.stringify(match_id)); return; } + const old_section = cmatch.calc_section(m); m.btp_winner = cval.btp_winner; + if (m.btp_winner && !m.team1_won) { + m.team1_won = (m.btp_winner === 1); + if (cval.network_score) { + m.network_score = cval.network_score; + } + if (cval.end_ts) { + m.end_ts = cval.end_ts; + } + } m.setup = cval.setup; - - cmatch.update_match(m); + const new_section = cmatch.calc_section(m); + + if (old_section === new_section) { + cmatch.update_match(m); + } else { + _show_render_matches(); + } } function update_current_match(c) { From d5b7815327ce80b75a4b3579b8c4b9c396e52750 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Tue, 9 Apr 2024 22:39:15 +0200 Subject: [PATCH 045/224] New Feature: Scores entered in BTP will now take affect in admin GUI. Also the tabletoperator will be determined in this case --- bts/btp_sync.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 76a20ba..fb048b6 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -415,6 +415,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { if (!cur_match.team1_won && cur_match.team1_won != match.team1_won) { if (!match.end_ts) { result_enterd_in_btp = true; + match.setup.warmup = 'none'; match.end_ts = Date.now(); const http_api = require('./http_api'); http_api.add_player_to_tabletoperator_list_by_match(app, tkey, match, match.end_ts); From da41fe6f755437b0a213066c0506704eec1b5dee Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 10 Apr 2024 13:17:51 +0200 Subject: [PATCH 046/224] Prevent the admin screen from re-rendering all content every time a change is made. Now the changed game/element is specifically re-rendered. --- bts/admin.js | 6 +-- bts/btp_sync.js | 25 ++++++---- bts/http_api.js | 7 ++- static/js/change.js | 49 ++++++++++--------- static/js/cmatch.js | 100 ++++++++++++++++++++++++++++++++------- static/js/ctournament.js | 55 +++++++++++++-------- 6 files changed, 167 insertions(+), 75 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index b9988ec..723a08d 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -294,7 +294,7 @@ function handle_tabletoperator_add(app, ws, msg) { } function handle_match_edit(app, ws, msg) { - if (!_require_msg(ws, msg, ['tournament_key', 'id', 'setup'])) { + if (!_require_msg(ws, msg, ['tournament_key', 'id', 'match'])) { return; } const tournament_key = msg.tournament_key; @@ -316,7 +316,7 @@ function handle_match_edit(app, ws, msg) { return; } - notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, setup}); + notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, match}); if (msg.btp_update) { btp_manager.update_score(app, changed_match); } @@ -350,7 +350,7 @@ function handle_match_preparation_call(app, ws, msg) { } //notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, setup}); - notify_change(app, tournament_key, 'match_preparation_call', {match__id: msg.id, setup}); + notify_change(app, tournament_key, 'match_preparation_call', {match__id: msg.id, match: changed_match}); btp_manager.update_highlight(app, changed_match); diff --git a/bts/btp_sync.js b/bts/btp_sync.js index fb048b6..e1e6ce7 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -341,6 +341,10 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { if (cur_match.team1_won === null) { cur_match.team1_won = undefined; } + + if(!match.network_score && cur_match.network_score) { + match.network_score = cur_match.network_score; + } if(cur_match.setup.called_timestamp) { // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. @@ -381,6 +385,14 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { } } + match.btp_needsync = cur_match.btp_needsync; + match.network_team1_left = cur_match.network_team1_left; + match.network_team1_serving = cur_match.network_team1_serving; + match.network_teams_player1_even = cur_match.network_teams_player1_even; + match.presses = cur_match.presses; + match.duration_ms = cur_match.duration_ms; + match.end_ts = cur_match.end_ts; + if(match.setup.now_on_court === false) { if(cur_match.setup.warmup) { match.setup.warmup = cur_match.setup.warmup; @@ -400,8 +412,6 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { cb(null); return; } - - // equals checked_in changed and check if it was the only change let only_change_check_in = false; let result_enterd_in_btp = false; @@ -424,7 +434,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { only_change_check_in = true; - } + } app.db.matches.update({_id: cur_match._id}, {$set: match}, {}, (err) => { if (err) { @@ -436,11 +446,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { if (match.setup.is_match) { if (!only_change_check_in || result_enterd_in_btp) { admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, - btp_winner: match.btp_winner, - network_score: result_enterd_in_btp == true ? match.network_score : null, - end_ts: result_enterd_in_btp == true ? match.end_ts : null, - setup: match.setup - }); + match: match}); } else { admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, btp_winner: match.btp_winner, @@ -811,8 +817,7 @@ async function integrate_now_on_court(app, tkey, callback) { } admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); + match: match}); admin.notify_change(app, tkey, 'match_called_on_court', match); bupws.handle_score_change(app, tkey, court_id); async.waterfall([ wcb => set_player_on_court(app, tkey, match.setup, wcb), diff --git a/bts/http_api.js b/bts/http_api.js index 3bf2746..ef4707e 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -253,6 +253,7 @@ function score_handler(req, res) { _id: match_id, tournament_key, }; + const update = { network_score: req.body.network_score, network_team1_left: req.body.network_team1_left, @@ -262,11 +263,13 @@ function score_handler(req, res) { presses: req.body.presses, duration_ms: req.body.duration_ms, end_ts: req.body.end_ts, + 'setup.now_on_court': true, }; if (update.team1_won !== undefined && update.team1_won != null) { update.btp_winner = (update.team1_won === true) ? 1 : 2; update.btp_needsync = true; + update['setup.now_on_court'] = false; } if (update.team1_won != undefined && update.team1_won != null) { @@ -318,8 +321,8 @@ function score_handler(req, res) { (match, court, changed_court, cb) => { if (changed_court) { admin.notify_change(req.app, tournament_key, 'court_current_match', { - match_id, - court_id: court._id, + match__id: match_id, + match: match, }); } cb(null, match, changed_court); diff --git a/static/js/change.js b/static/js/change.js index 60ff1a6..f200b03 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -21,15 +21,15 @@ function change_score(cval) { m.team1_won = cval.team1_won; } -function change_current_match(cval) { - // Do not use courts_by_id since that may not be initialized in all views - const court = utils.find(curt.courts, c => c._id === cval.court_id); - if (court) { - court.match_id = cval.match_id; - } else { - cerror.silent('Cannot find court ' + JSON.stringify(cval.court_id)); - } -} +//function change_current_match(cval) { +// // Do not use courts_by_id since that may not be initialized in all views +// const court = utils.find(curt.courts, c => c._id === cval.court_id); +// if (court) { +// court.match_id = cval.match_id; +// } else { +// cerror.silent('Cannot find court ' + JSON.stringify(cval.court_id)); +// } +//} function default_handler_func(rerender, special_funcs, c) { if (special_funcs && special_funcs[c.ctype]) { @@ -84,17 +84,19 @@ function default_handler_func(rerender, special_funcs, c) { }); break;} - case 'tabletoperator_add': - curt.tabletoperators.push(c.val.tabletoperator); - rerender(); - break; - case 'tabletoperator_removed': - const changed_t = utils.find(curt.tabletoperators, m => m._id === c.val.tabletoperator._id); - if (changed_t) { - changed_t.court = c.val.tabletoperator.court; - } - rerender(); - break; + //case 'tabletoperator_add': + // curt.tabletoperators.push(c.val.tabletoperator); + // console.log("Need to rerender Tabletoperators"); + // //rerender(); + // break; + //case 'tabletoperator_removed': + // const changed_t = utils.find(curt.tabletoperators, m => m._id === c.val.tabletoperator._id); + // if (changed_t) { + // changed_t.court = c.val.tabletoperator.court; + // } + // console.log("Need to rerender Tabletoperators"); + // //rerender(); + // break; case 'match_add': curt.matches.push(c.val.match); rerender(); @@ -114,8 +116,8 @@ function default_handler_func(rerender, special_funcs, c) { rerender(); break; case 'match_preparation_call': - announcePreparationMatch(c.val.setup); - rerender(); + announcePreparationMatch(c.val.match.setup); + ctournament.update_match(c); break; case 'match_called_on_court': announceNewMatch(c.val.setup); @@ -159,7 +161,7 @@ function default_handler_func(rerender, special_funcs, c) { return { default_handler, - change_current_match, + //change_current_match, }; })(); @@ -168,6 +170,7 @@ return { if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var cerror = require('./cerror'); var cmatch = require('./cmatch'); + var ctournament = require('./ctournament'); var uiu = require('../bup/js/uiu'); var utils = require('./utils'); diff --git a/static/js/cmatch.js b/static/js/cmatch.js index b38aeab..41601c5 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -418,26 +418,92 @@ function update_player(match_id, player, now_on_court, show_player_status) { }); } -function update_match(m) { - uiu.qsEach('.match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { - match_row_el.innerHTML = ''; - console.log("render single row: match_num = " + m.setup.match_num); - console.log(match_row_el.parentNode); - console.log(match_row_el.parentNode.parentNode); - console.log(match_row_el.parentNode.parentNode.parentNode); - switch (match_row_el.parentNode.parentNode.parentNode.className) { - case 'courts_container': - render_match_row(match_row_el, m, null, 'default', false, false); +function update_match(m, old_section, new_section) { + if(old_section != new_section) { + switch (old_section) { + case 'finished': + case 'unassigned': + uiu.qsEach('.match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { + match_row_el.remove(); + }); break; - case 'finished_container': - render_match_row(match_row_el, m, null, 'default', false,true); + default: + uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { + while(match_row_el.childElementCount > 1){ + match_row_el.removeChild(match_row_el.lastChild); + } + }); + break; + } + + switch (new_section) { + case 'finished': + uiu.qsEach('.finished_container', (finished_container) => { + let tbody = finished_container.querySelector('.match_table > tbody'); + uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); + }); + case 'unassigned': + uiu.qsEach('.unassigned_container', (unassigned_container) => { + let tbody = unassigned_container.querySelector('.match_table > tbody'); + uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); + }); break; - case 'unassigned_container': default: - render_match_row(match_row_el, m, null, 'default', true,true); - break + break; } - }); + } else { + switch (old_section) { + case 'finished': + case 'unassigned': + uiu.qsEach('.match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { + match_row_el.innerHTML = ''; + }); + break; + default: + uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { + while(match_row_el.childElementCount > 1){ + match_row_el.removeChild(match_row_el.lastChild); + } + }); + break; + } + } + + switch (new_section) { + case 'finished': + uiu.qsEach('.finished_container > table > tbody > .match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { + render_match_row(match_row_el, m, null, 'default', false, true); + }); + break; + case 'unassigned': + uiu.qsEach( '.unassigned_container > table > tbody > .match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { + match_row_el.setAttribute('class', 'match highlight_' + (m.setup.highlight ? m.setup.highlight : 0)); + render_match_row(match_row_el, m, null, 'default', true, true); + }); + break; + default: + uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { + render_match_row(match_row_el, m, null, 'plain', false, false); + }); + break; + } + + + //uiu.qsEach('.match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { + // console.log("render single row: match_num = " + m.setup.match_num); + // switch (new_section) { + // case 'finished': + // render_match_row(match_row_el, m, null, 'default', false, true); + // break; + // case 'unassigned': + // render_match_row(match_row_el, m, null, 'default', true, true); + // break; + // default: + // uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { + // render_match_row(match_row_el, m, null, 'plain', false, false); + // break; + // } + //}); } var active_timers = {'matches': {}, 'players' : {}}; @@ -952,7 +1018,7 @@ function render_courts(container, style) { const expected_section = 'court_' + c._id; const court_matches = curt.matches.filter(m => calc_section(m) === expected_section); - const tr = uiu.el(tbody, 'tr'); + const tr = uiu.el(tbody, 'tr', {class:"court_row", "data-court_id":c._id} ); const rowspan = Math.max(1, court_matches.length); uiu.el(tr, 'th', { 'class': 'court_num', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 73d4b9d..7fd70d5 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -120,7 +120,13 @@ function update_score(c) { if (old_section === new_section) { cmatch.update_match_score(m); } else { - _show_render_matches(); + if(new_section == 'finished' || new_section == 'unassigned'){ + m.setup.now_on_court = false; + } + else{ + m.setup.now_on_court = true; + } + cmatch.update_match(m, old_section, new_section); } } @@ -147,33 +153,38 @@ function update_match(c){ // Find the match const m = utils.find(curt.matches, m => m._id === match_id); if (!m) { - cerror.silent('Cannot find match to update player status, ID: ' + JSON.stringify(match_id)); + cerror.silent('Cannot find match to update, ID: ' + JSON.stringify(match_id)); return; } const old_section = cmatch.calc_section(m); - m.btp_winner = cval.btp_winner; - if (m.btp_winner && !m.team1_won) { - m.team1_won = (m.btp_winner === 1); - if (cval.network_score) { - m.network_score = cval.network_score; - } - if (cval.end_ts) { - m.end_ts = cval.end_ts; - } - } - m.setup = cval.setup; + m.network_score = cval.match.network_score; + m.presses = cval.match.presses; + m.team1_won = cval.match.team1_won; + m.shuttle_count = cval.match.shuttle_count; + m.setup = cval.match.setup; + m.btp_winner = cval.match.btp_winner; const new_section = cmatch.calc_section(m); - - if (old_section === new_section) { - cmatch.update_match(m); - } else { - _show_render_matches(); + cmatch.update_match(m, old_section, new_section); +} + +function tabletoperator_add(c) { + curt.tabletoperators.push(c.val.tabletoperator); + + _show_render_tabletoperators(); +} + +function tabletoperator_removed(c) { + const changed_t = utils.find(curt.tabletoperators, m => m._id === c.val.tabletoperator._id); + if (changed_t) { + changed_t.court = c.val.tabletoperator.court; } + + _show_render_tabletoperators(); } function update_current_match(c) { - change.change_current_match(c.val); - _show_render_matches(); + //change.change_current_match(c.val); + update_match(c); } function _update_all_ui_elements() { @@ -190,6 +201,7 @@ function _show_render_tabletoperators() { ctabletoperator.render_unassigned(uiu.qs('.unassigned_tableoperators_container')); } + function ui_btp_fetch() { send({ type: 'btp_fetch', @@ -302,6 +314,8 @@ _route_single(/t\/([a-z0-9]+)\/$/, ui_show, change.default_handler(_update_all_u court_current_match: update_current_match, update_player_status: update_player_status, match_edit: update_match, + tabletoperator_add: tabletoperator_add, + tabletoperator_removed: tabletoperator_removed, })); function _upload_logo(e) { @@ -996,6 +1010,7 @@ return { switch_tournament, ui_show, ui_list, + update_match, }; })(); From 700886a96ab63e3181336f2ad72aea8023d9e7d5 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 10 Apr 2024 20:35:17 +0200 Subject: [PATCH 047/224] bundle tournamen meta informations at the top of the window and add a switch to enable/disable the announcements on every client --- bts/admin.js | 13 ++++++++++++ static/css/admin.css | 15 ++++++++++++++ static/css/cmatch.css | 1 + static/js/announcements.js | 26 +++++++++++++++++++++++ static/js/change.js | 3 +++ static/js/cmatch.js | 1 + static/js/ctournament.js | 42 +++++++++++++++++++++++++++++++++++--- 7 files changed, 98 insertions(+), 3 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 723a08d..00b39bd 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -370,6 +370,18 @@ function handle_begin_to_play_call(app, ws, msg) { ws.respond(msg); } +function handle_free_announce(app, ws, msg) { + if (!_require_msg(ws, msg, ['text'])) { + return; + } + const tournament_key = msg.tournament_key; + const text = msg.text; + + notify_change(app, tournament_key, 'free_announce', {text}); + + ws.respond(msg); +} + function handle_second_call_tabletoperator(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { return; @@ -565,6 +577,7 @@ module.exports = { handle_match_preparation_call, handle_ticker_pushall, handle_ticker_reset, + handle_free_announce, handle_second_call_tabletoperator, handle_second_call_team_one, handle_second_call_team_two, diff --git a/static/css/admin.css b/static/css/admin.css index 323249e..df34701 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -162,3 +162,18 @@ table.striped-table { padding-left: 0.3em; white-space: pre-wrap; } + +.metadata_container{ + background-color: #aaaa; + display: flex; + flex-direction: row; + justify-content: space-around; + margin: 20px 20px 20px 0px; + padding: 10px; +} + +.announcements_container > form{ + display: flex; + flex-direction: column; + justify-content: space-evenly; +} \ No newline at end of file diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 8279947..2cb77e5 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -1,6 +1,7 @@ .match_save_button { font-size: 120%; padding: 9px 20px; + margin: 10px 0px 10px 0px; } .match_num { diff --git a/static/js/announcements.js b/static/js/announcements.js index 77231ab..b734c00 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -1,5 +1,9 @@ + function announceNewMatch(matchSetup) { + if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + return; + } var field = createFieldAnnouncement(matchSetup); var matchNumber = createMatchNumberAnnouncement(matchSetup); var eventName = createEventAnnouncement(matchSetup); @@ -10,6 +14,9 @@ function announceNewMatch(matchSetup) { } function announcePreparationMatch(matchSetup) { + if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + return; + } var preparation = createPreparationAnnouncement(); var matchNumber = createMatchNumberAnnouncement(matchSetup); var eventName = createEventAnnouncement(matchSetup); @@ -18,24 +25,39 @@ function announcePreparationMatch(matchSetup) { announce([preparation, matchNumber, eventName, round, teams]); } function announceSecondCallTeamOne(matchSetup) { + if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + return; + } announceSecondCall(matchSetup, matchSetup.teams[0]); } function announceSecondCallTeamTwo(matchSetup) { + if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + return; + } announceSecondCall(matchSetup, matchSetup.teams[1]); } function announceSecondCallTabletoperator(matchSetup) { + if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + return; + } const call = createFieldAnnouncement(matchSetup) + createSecondCallAnnouncement() + createTabletOperator(matchSetup); announce([call]); } function announceSecondCall(matchSetup, team) { + if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + return; + } var secondCall = createSecondCallAnnouncement() + createSingleTeam(team.players); var field = createFieldAnnouncement(matchSetup); announce([secondCall, field]); } function announceBeginnToPlay(matchSetup, team) { + if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + return; + } announce([createFieldAnnouncement(matchSetup) + "Bitte mit dem Spielen beginnen!"]); } @@ -170,6 +192,10 @@ function createPreparationAnnouncement() { } function announce(callArray) { + if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + return; + } + // Seems like the getVoices() is an asynchronous function where it is not always guaranteed that you get a // result immediately. The wait for the result must therefore be handled: // https://stackoverflow.com/questions/21513706/getting-the-list-of-voices-in-speechsynthesis-web-speech-api diff --git a/static/js/change.js b/static/js/change.js index f200b03..2c00273 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -38,6 +38,9 @@ function default_handler_func(rerender, special_funcs, c) { } switch (c.ctype) { + case 'free_announce': + announce([c.val.text]); + break; case 'props': { curt.name = c.val.name; curt.is_team = c.val.is_team; diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 41601c5..72b7fb2 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -43,6 +43,7 @@ function render_match_table_header(table, include_courts) { function render_match_row(tr, match, court, style, show_player_status, show_add_tabletoperator) { console.warn("rerender_row"); + if(!match.setup.is_match) { return; } diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 7fd70d5..bc79062 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -226,6 +226,7 @@ function ui_ticker_push() { function render_announcement_formular(target) { const announcements = uiu.el(target, 'div', 'announcements_container'); + const heading = uiu.el(announcements, 'h3', {}, 'Freie Ansage'); const form = uiu.el(announcements, 'form'); uiu.el(form, 'textarea', { type: 'textarea', @@ -239,10 +240,41 @@ function render_announcement_formular(target) { role: 'submit', }, 'Ansage abspielen'); form_utils.onsubmit(form, function (d) { - announce([d.custom_announcement]); + //announce([d.custom_announcement]); + send({ + type: 'free_announce', + tournament_key: curt.key, + text: d.custom_announcement, + }, function (err) { + if (err) { + return cerror.net(err); + } + }); }); +} + +function render_enable_announcement(target) { + const announcements = uiu.el(target, 'div', 'enable_announcements_container'); + const heading = uiu.el(announcements, 'h3', {}, 'Ansagen auf diesem Gerät'); + const form = uiu.el(announcements, 'form'); + const enable_announcements =uiu.el(form, 'input', { + type: 'checkbox', + id: 'enable_announcements', + name: 'enable_announcements' + }); + + enable_announcements.checked = (window.localStorage.getItem('enable_announcements') === 'true'); + uiu.el(form, 'label', {for: 'enable_announcements'}, 'Ansagen auf diesem Gerät aktivieren'); + enable_announcements.addEventListener('change', change_announcements); +} + +function change_announcements(e) { + let enable_announcements = document.getElementById('enable_announcements'); + console.log(enable_announcements.checked); + window.localStorage.setItem('enable_announcements', enable_announcements.checked); } + function ui_show() { crouting.set('t/:key/', {key: curt.key}); const bup_lang = ((curt.language && curt.language !== 'auto') ? '&lang=' + encodeURIComponent(curt.language) : ''); @@ -284,12 +316,16 @@ function ui_show() { cmatch.prepare_render(curt); + const meta_div = uiu.el(main, 'div', 'metadata_container'); + + uiu.el(meta_div, 'div', 'unassigned_tableoperators_container'); + render_announcement_formular(meta_div); + render_enable_announcement(meta_div); + uiu.el(main, 'div', 'courts_container'); - uiu.el(main, 'div', 'unassigned_tableoperators_container'); uiu.el(main, 'div', 'unassigned_container'); const match_create_container = uiu.el(main, 'div'); cmatch.render_create(match_create_container); - render_announcement_formular(main); uiu.el(main, 'div', 'finished_container'); _show_render_matches(); From 3bf1ae9bb914b1b89579d4ae1d00e8d34bf06255 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Wed, 10 Apr 2024 22:04:09 +0200 Subject: [PATCH 048/224] Enhancement: Walkover and Retirements will not be recognized for the tabletoperator job --- bts/http_api.js | 71 ++++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/bts/http_api.js b/bts/http_api.js index ef4707e..e64feaa 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -389,45 +389,48 @@ function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, en } function add_player_to_tabletoperator_list_by_match(app, tournament_key, cur_match, end_ts) { - app.db.tabletoperators.findOne({ 'tournament_key': tournament_key, 'match_id': cur_match._id }, (err, no_tabletoperator) => { - if (err) { - return reject(err); - } - if (no_tabletoperator == null) { - const round = cur_match.setup.match_name; - var team = null; - if (round == 'VF' || round == 'QF') { - team = cur_match.setup.teams[cur_match.btp_winner - 1]; - } else { - const index = cur_match.btp_winner % 2; - team = cur_match.setup.teams[index]; + if (cur_match.network_score) { + // walkovers and retirements will not be recorgnized. + app.db.tabletoperators.findOne({ 'tournament_key': tournament_key, 'match_id': cur_match._id }, (err, no_tabletoperator) => { + if (err) { + return reject(err); } - if (team && typeof team.players !== 'undefined') { - var tabletoperator = []; + if (no_tabletoperator == null) { + const round = cur_match.setup.match_name; + var team = null; + if (round == 'VF' || round == 'QF') { + team = cur_match.setup.teams[cur_match.btp_winner - 1]; + } else { + const index = cur_match.btp_winner % 2; + team = cur_match.setup.teams[index]; + } + if (team && typeof team.players !== 'undefined') { + var tabletoperator = []; - team.players.forEach((player) => { - tabletoperator.push(player); - }); + team.players.forEach((player) => { + tabletoperator.push(player); + }); - const new_tabletoperator = { - tournament_key, - tabletoperator, - 'match_id': cur_match._id, - 'start_ts': end_ts, - 'end_ts': null, - 'court': null - }; - app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { - if (err) { - ws.respond(msg, err); - return; - } - admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); - }); + const new_tabletoperator = { + tournament_key, + tabletoperator, + 'match_id': cur_match._id, + 'start_ts': end_ts, + 'end_ts': null, + 'court': null + }; + app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { + if (err) { + ws.respond(msg, err); + return; + } + admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); + }); + } } - } - }); + }); + } } function remove_player_on_court (app, tkey, cur_match_id, end_ts, callback) { const admin = require('./admin'); // avoid dependency cycle From a7d5860dac53bd68268160d8e2542160d1a7e896 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 11 Apr 2024 03:04:51 +0200 Subject: [PATCH 049/224] add better Upcoming View and take looser of qf as tabletoperators. --- bts/http_api.js | 9 +++-- static/css/admin.css | 3 +- static/css/cmatch.css | 23 ++++++++++- static/css/court.css | 25 ++++++++++++ static/js/change.js | 27 ++++++------- static/js/cmatch.js | 86 +++++++++++++++++++++++++++------------- static/js/ctournament.js | 86 ++++++++++++++++++++++++++++++++-------- 7 files changed, 196 insertions(+), 63 deletions(-) diff --git a/bts/http_api.js b/bts/http_api.js index e64feaa..39422f0 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -398,12 +398,13 @@ function add_player_to_tabletoperator_list_by_match(app, tournament_key, cur_mat if (no_tabletoperator == null) { const round = cur_match.setup.match_name; var team = null; - if (round == 'VF' || round == 'QF') { - team = cur_match.setup.teams[cur_match.btp_winner - 1]; - } else { + // Not nessasaary if not TR 15/5 + //if (round == 'VF' || round == 'QF') { + // team = cur_match.setup.teams[cur_match.btp_winner - 1]; + //} else { const index = cur_match.btp_winner % 2; team = cur_match.setup.teams[index]; - } + //} if (team && typeof team.players !== 'undefined') { var tabletoperator = []; diff --git a/static/css/admin.css b/static/css/admin.css index df34701..6be16da 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -176,4 +176,5 @@ table.striped-table { display: flex; flex-direction: column; justify-content: space-evenly; -} \ No newline at end of file +} + diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 2cb77e5..14b47e4 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -115,6 +115,7 @@ .match_team_won { font-weight: bold; } + .match_score{ font-weight: bold; padding-left: 0.2em; @@ -125,6 +126,24 @@ padding-left: 0.3em; } +.match_score_current > .tablet, +.match_score_current > .umpire { + min-width: 1em; + width: 1em; + min-height: 0.65em; + height: 0.65em; + margin: 0px 5px; + /*cursor: none;*/ +} + +.match_score_current > .match_no_umpire, +.match_score_current > span { + padding: 0 0; + font-size: 35px; + font-weight: 300; + /*cursor: none;*/ +} + .match_timer { min-width: 3.6em; } @@ -349,7 +368,9 @@ match_second_call_button, background-color: #c56bff; } - +.preperation { + font-weight: bold; +} @media print { .toprow, diff --git a/static/css/court.css b/static/css/court.css index 76c060b..eb7a800 100644 --- a/static/css/court.css +++ b/static/css/court.css @@ -22,3 +22,28 @@ margin: -0.1em 0.3em 0.1em 0.3em; line-height: 100%; } +.main_upcoming > div > .match_table > tbody > .court_row > .court_num { + display: inline-block; + background-size: cover; + opacity: 1; + color: #fff; + text-align: center; + background-image: url(../icons/court.svg); + height: 1.0em; + min-height: 1.0em; + width: 1.3em; + min-width: 1.3em; + font-weight: 900; + font-size: 1.8em; + -webkit-text-stroke-width: 1px; + -webkit-text-stroke-color: black; + text-shadow: + 3px 3px 3px #000, + -1px -1px 3px #000, + 1px -1px 3px #000, + -1px 1px 3px #000, + 1px 1px 3px #000; + margin: -0.05em 0.3em 0.05em 0.3em; + line-height: 100%; + padding: 0.1em 0.2em 0em 0.2em; +} diff --git a/static/js/change.js b/static/js/change.js index 2c00273..aa19812 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -85,21 +85,16 @@ function default_handler_func(rerender, special_funcs, c) { uiu.qsEach('input[name="ticker_password"]', function(el) { el.value = curt.ticker_password; }); - break;} - //case 'tabletoperator_add': - // curt.tabletoperators.push(c.val.tabletoperator); - // console.log("Need to rerender Tabletoperators"); - // //rerender(); - // break; - //case 'tabletoperator_removed': - // const changed_t = utils.find(curt.tabletoperators, m => m._id === c.val.tabletoperator._id); - // if (changed_t) { - // changed_t.court = c.val.tabletoperator.court; - // } - // console.log("Need to rerender Tabletoperators"); - // //rerender(); - // break; + case 'tabletoperator_add': + //nothing to do here + break; + case 'tabletoperator_removed': + //nothing todo here + break; + case 'match_edit': + ctournament.update_match(c); + break; case 'match_add': curt.matches.push(c.val.match); rerender(); @@ -121,6 +116,7 @@ function default_handler_func(rerender, special_funcs, c) { case 'match_preparation_call': announcePreparationMatch(c.val.match.setup); ctournament.update_match(c); + ctournament.update_upcoming_match(c); break; case 'match_called_on_court': announceNewMatch(c.val.setup); @@ -157,6 +153,9 @@ function default_handler_func(rerender, special_funcs, c) { case 'ticker_status': uiu.text_qs('.ticker_status', 'Ticker status: ' + c.val); break; + case 'update_player_status': + //nothing todo here + break; default: cerror.silent('Unsupported change type ' + c.ctype); } diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 72b7fb2..b5e07a9 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -43,7 +43,7 @@ function render_match_table_header(table, include_courts) { function render_match_row(tr, match, court, style, show_player_status, show_add_tabletoperator) { console.warn("rerender_row"); - + if(!match.setup.is_match) { return; } @@ -97,27 +97,34 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ 'class': ((match.team1_won === true) ? 'match_team_won' : ''), style: 'text-align: right;', }); - if (show_add_tabletoperator) { - if (setup.teams[0].players.length > 0) { - create_match_button(players0, 'vlink tabletoperator_add_button', 'tabletoperator:add', on_add_to_tabletoperators_team_one_button_click, match._id); + if (style === 'default' || style === 'plain') { + if (show_add_tabletoperator) { + if (setup.teams[0].players.length > 0) { + create_match_button(players0, 'vlink tabletoperator_add_button', 'tabletoperator:add', on_add_to_tabletoperators_team_one_button_click, match._id); + } + } else { + create_match_button(players0, 'vlink match_second_call_button', 'match:secondcallteamone', on_second_call_team_one_button_click, match._id); } - } else { - create_match_button(players0, 'vlink match_second_call_button', 'match:secondcallteamone', on_second_call_team_one_button_click, match._id); } render_players_el(players0, setup, 0, match, show_player_status); uiu.el(tr, 'td', 'match_vs', 'v'); const players1 = uiu.el(tr, 'td', ((match.team1_won === false) ? 'match_team_won ' : '') + 'match_team2'); render_players_el(players1, setup, 1, match, show_player_status); - if (show_add_tabletoperator) { - if (setup.teams[1].players.length > 0) { - create_match_button(players1, 'vlink tabletoperator_add_button', 'tabletoperator:add', on_add_to_tabletoperators_team_two_button_click, match._id); + if (style === 'default' || style === 'plain') { + if (show_add_tabletoperator) { + if (setup.teams[1].players.length > 0) { + create_match_button(players1, 'vlink tabletoperator_add_button', 'tabletoperator:add', on_add_to_tabletoperators_team_two_button_click, match._id); + } + } else { + create_match_button(players1, 'vlink match_second_call_button', 'match:secondcallteamtwo', on_second_call_team_two_button_click, match._id); } - } else { - create_match_button(players1, 'vlink match_second_call_button', 'match:secondcallteamtwo', on_second_call_team_two_button_click, match._id); } + + + + const to_td = uiu.el(tr, 'td'); if (style === 'default' || style === 'plain') { - const to_td = uiu.el(tr, 'td'); if (setup.umpire_name) { uiu.el(to_td, 'div', 'umpire', ''); uiu.el(to_td, 'span', {}, setup.umpire_name); @@ -135,22 +142,42 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } else { uiu.el(to_td, 'div', 'no_umpire', ''); uiu.el(to_td, 'span', 'match_no_umpire', ci18n('No umpire')); - } + } + } else if(style === 'upcoming' && setup.highlight == 6) { + uiu.el(to_td, 'span', 'preperation', 'Spiel in Vorbereitung!'); + } + + if (style === 'default' || style === 'plain') { create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); - + } + + const score_td = uiu.el(tr, 'td'); + if (court && (court.match_id !== match._id) && (typeof match.team1_won !== 'boolean') && setup.umpire_name) { + const ready_text = (style === 'public') ? ci18n('Ready') : ci18n(' Ready to start '); + uiu.el(score_td, 'span', {}, ready_text); } + const score_span = uiu.el(score_td, 'span', { + 'class': ('match_score' + ((match.setup.now_on_court === true) ? ' match_score_current' : '')), + 'data-match_id': match._id, + }, calc_score_str(match)); - if (style === 'default' || style === 'plain'/* || style === 'public' WIP */) { - const score_td = uiu.el(tr, 'td'); - if (court && (court.match_id !== match._id) && (typeof match.team1_won !== 'boolean') && setup.umpire_name) { - const ready_text = (style === 'public') ? ci18n('Ready') : ci18n(' Ready to start '); - uiu.el(score_td, 'span', {}, ready_text); + if(style === 'public' && calc_score_str(match) === '') { + if (setup.umpire_name) { + uiu.el(score_span, 'div', 'umpire', ''); + uiu.el(score_span, 'span', {}, setup.umpire_name); + if (setup.service_judge_name) { + uiu.el(score_span, 'span', {}, ' \u200B+ '); + uiu.el(score_span, 'span', {}, setup.service_judge_name); + } + } else if (setup.tabletoperators && setup.tabletoperators.length > 0){ + uiu.el(score_span, 'div', 'tablet', ''); + uiu.el(score_span, 'span', 'match_no_umpire', setup.tabletoperators[0].name ); + if (setup.tabletoperators.length > 1) { + uiu.el(score_span, 'span', 'match_no_umpire', ' \u200B/ '); + uiu.el(score_span, 'span', 'match_no_umpire', setup.tabletoperators[1].name ); + } } - uiu.el(score_td, 'span', { - 'class': ('match_score' + ((match.setup.now_on_court === true) ? ' match_score_current' : '')), - 'data-match_id': match._id, - }, calc_score_str(match)); } if ((style === 'default' || style === 'plain') && match.setup.now_on_court != undefined) { @@ -211,6 +238,7 @@ function create_match_button(targetEl, cssClass, title, listener, matchId,) { btn.addEventListener('click', listener); } function update_match_score(m) { + console.log('In Matchscore update'); uiu.qsEach('.match_score[data-match_id=' + JSON.stringify(m._id) + ']', function(score_el) { uiu.text(score_el, calc_score_str(m)); }); @@ -353,7 +381,7 @@ function render_player_el(parentNode, player, match_id, now_on_court, show_playe uiu.el(player_element, 'div', 'tablet_inline', court_number); } - if(show_player_status) { + if(show_player_status && player_status != "now_on_court") { var timer_state = _extract_player_timer_state(player); var timer = create_timer(timer_state, player_element, "#ffffff", "#ffffff"); } @@ -411,7 +439,7 @@ function update_player(match_id, player, now_on_court, show_player_status) { uiu.el(player_el, 'div', 'tablet_inline', court_number); } - if(show_player_status) { + if(show_player_status && player_status != "now_on_court") { var timer_state = _extract_player_timer_state(player); var timer = create_timer(timer_state, player_el, "#ffffff", "#ffffff"); } @@ -484,7 +512,12 @@ function update_match(m, old_section, new_section) { break; default: uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { - render_match_row(match_row_el, m, null, 'plain', false, false); + const closest = match_row_el.closest('.main_upcoming'); + if(Boolean(closest)) { + render_match_row(match_row_el, m, null, 'public'); + } else { + render_match_row(match_row_el, m, null, 'plain', false, false); + } }); break; } @@ -1023,7 +1056,6 @@ function render_courts(container, style) { const rowspan = Math.max(1, court_matches.length); uiu.el(tr, 'th', { 'class': 'court_num', - style: ((style === 'public') ? 'padding-right: 0.5em' : ''), rowspan, title: c._id, }, c.num); diff --git a/static/js/ctournament.js b/static/js/ctournament.js index bc79062..b5084b6 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -167,6 +167,34 @@ function update_match(c){ cmatch.update_match(m, old_section, new_section); } +function update_upcoming_match(c){ + const cval = c.val; + const match_id = cval.match__id; + + // Find the match + const m = utils.find(curt.matches, m => m._id === match_id); + if (!m) { + cerror.silent('Cannot find match to update, ID: ' + JSON.stringify(match_id)); + return; + } + const old_section = cmatch.calc_section(m); + m.network_score = cval.match.network_score; + m.presses = cval.match.presses; + m.team1_won = cval.match.team1_won; + m.shuttle_count = cval.match.shuttle_count; + m.setup = cval.match.setup; + m.btp_winner = cval.match.btp_winner; + const new_section = cmatch.calc_section(m); + cmatch.update_match(m, old_section, new_section); + + console.log(new_section); + + if(old_section != new_section || new_section == 'unassigned') { + const upcoming_container = uiu.qs('.upcoming_container'); + cmatch.render_upcoming_matches(upcoming_container); + } +} + function tabletoperator_add(c) { curt.tabletoperators.push(c.val.tabletoperator); @@ -187,6 +215,11 @@ function update_current_match(c) { update_match(c); } +function update_upcoming_current_match(c) { + //change.change_current_match(c.val); + update_upcoming_match(c); +} + function _update_all_ui_elements() { _show_render_matches(); _show_render_tabletoperators(); @@ -300,27 +333,24 @@ function ui_show() { const main = uiu.qs('.main'); uiu.empty(main); - const settings_btn = uiu.el(main, 'div', 'tournament_settings_link vlink', ci18n('edit tournament')); - settings_btn.addEventListener('click', ui_edit); + const meta_div = uiu.el(main, 'div', 'metadata_container'); - if (curt.btp_enabled) { - const btp_fetch_btn = uiu.el(main, 'button', 'tournament_btp_fetch', ci18n('update from BTP')); - btp_fetch_btn.addEventListener('click', ui_btp_fetch); - } - if (curt.ticker_enabled) { - const ticker_push_btn = uiu.el(main, 'button', 'tournament_ticker_push', ci18n('update ticker')); - ticker_push_btn.addEventListener('click', ui_ticker_push); - } + uiu.el(meta_div, 'div', 'unassigned_tableoperators_container'); + render_announcement_formular(meta_div); + + const meta_right_div = uiu.el(meta_div, 'div', 'metadata_right_container'); + + render_enable_announcement(meta_right_div); + + render_settings(meta_right_div); uiu.el(main, 'h1', 'tournament_name ct_name', curt.name || curt.key); cmatch.prepare_render(curt); - const meta_div = uiu.el(main, 'div', 'metadata_container'); + + - uiu.el(meta_div, 'div', 'unassigned_tableoperators_container'); - render_announcement_formular(meta_div); - render_enable_announcement(meta_div); uiu.el(main, 'div', 'courts_container'); uiu.el(main, 'div', 'unassigned_container'); @@ -354,6 +384,22 @@ _route_single(/t\/([a-z0-9]+)\/$/, ui_show, change.default_handler(_update_all_u tabletoperator_removed: tabletoperator_removed, })); +function render_settings(target) { + const settings_div = uiu.el(target, 'div', 'metadata_right_container'); + uiu.el(settings_div, 'h3', {}, 'Turnier-Einstellungen'); + const settings_btn = uiu.el(settings_div, 'div', 'tournament_settings_link vlink', ci18n('edit tournament')); + settings_btn.addEventListener('click', ui_edit); + + if (curt.btp_enabled) { + const btp_fetch_btn = uiu.el(settings_div, 'button', 'tournament_btp_fetch', ci18n('update from BTP')); + btp_fetch_btn.addEventListener('click', ui_btp_fetch); + } + if (curt.ticker_enabled) { + const ticker_push_btn = uiu.el(settings_div, 'button', 'tournament_ticker_push', ci18n('update ticker')); + ticker_push_btn.addEventListener('click', ui_ticker_push); + } +} + function _upload_logo(e) { const input = e.target; if (!input.files.length) return; @@ -849,7 +895,7 @@ function render_upcoming(container) { const courts_container = uiu.el(container, 'div'); cmatch.render_courts(courts_container, 'public'); - const upcoming_container = uiu.el(container, 'div'); + const upcoming_container = uiu.el(container, 'div', 'upcoming_container'); cmatch.render_upcoming_matches(upcoming_container); } @@ -870,7 +916,14 @@ function ui_upcoming() { fullscreen.toggle(); }); } -_route_single(/t\/([a-z0-9]+)\/upcoming/, ui_upcoming); +_route_single(/t\/([a-z0-9]+)\/upcoming/, ui_upcoming, change.default_handler(_update_all_ui_elements, { + score: update_score, + court_current_match: update_upcoming_current_match, + //update_player_status: update_player_status, + match_edit: update_upcoming_match, + //tabletoperator_add: tabletoperator_add, + //tabletoperator_removed: tabletoperator_removed, +})); function init() { @@ -1047,6 +1100,7 @@ return { ui_show, ui_list, update_match, + update_upcoming_match, }; })(); From d243f208818e4894d5aa2e34f0c7d92284b5eee7 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 13 Apr 2024 01:39:30 +0200 Subject: [PATCH 050/224] Fix Bug in rerender upcoming view --- bts/bupws.js | 38 ++++++++++++++++++++------------------ static/js/ctournament.js | 9 +++++++-- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index b849a54..f7f609c 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -269,26 +269,28 @@ function matches_handler(app, ws, tournament_key, court_id) { notify_change_ws(app, tournament_key, "score-update", msg); } - let matches = db_matches.map(dbm => create_match_representation(tournament, dbm)); - if (tournament.only_now_on_court) { - matches = matches.filter(m => m.setup.now_on_court); + if(db_matches){ + let matches = db_matches.map(dbm => create_match_representation(tournament, dbm)); + if (tournament.only_now_on_court) { + matches = matches.filter(m => m.setup.now_on_court); + } + + db_courts.sort(utils.cmp_key('num')); + const courts = db_courts.map(function (dc) { + var res = { + court_id: dc._id, + label: dc.num, + }; + if (dc.match_id) { + res.match_id = 'bts_' + dc.match_id; + } + if (dc.called_timestamp) { + res.called_timestamp = dc.called_timestamp; + } + return res; + }); } - db_courts.sort(utils.cmp_key('num')); - const courts = db_courts.map(function (dc) { - var res = { - court_id: dc._id, - label: dc.num, - }; - if (dc.match_id) { - res.match_id = 'bts_' + dc.match_id; - } - if (dc.called_timestamp) { - res.called_timestamp = dc.called_timestamp; - } - return res; - }); - const event = create_event_representation(tournament); event.matches = matches; event.courts = courts; diff --git a/static/js/ctournament.js b/static/js/ctournament.js index b5084b6..987e9a6 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -225,6 +225,11 @@ function _update_all_ui_elements() { _show_render_tabletoperators(); } +function _update_all_ui_elements_upcoming() { + cmatch.render_courts(uiu.qs('.courts_container'), 'public'); + cmatch.render_upcoming_matches(uiu.qs('.upcoming_container')); +} + function _show_render_matches() { cmatch.render_courts(uiu.qs('.courts_container')); cmatch.render_unassigned(uiu.qs('.unassigned_container')); @@ -892,7 +897,7 @@ _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit); function render_upcoming(container) { cmatch.prepare_render(curt); - const courts_container = uiu.el(container, 'div'); + const courts_container = uiu.el(container, 'div', 'courts_container'); cmatch.render_courts(courts_container, 'public'); const upcoming_container = uiu.el(container, 'div', 'upcoming_container'); @@ -916,7 +921,7 @@ function ui_upcoming() { fullscreen.toggle(); }); } -_route_single(/t\/([a-z0-9]+)\/upcoming/, ui_upcoming, change.default_handler(_update_all_ui_elements, { +_route_single(/t\/([a-z0-9]+)\/upcoming/, ui_upcoming, change.default_handler(_update_all_ui_elements_upcoming, { score: update_score, court_current_match: update_upcoming_current_match, //update_player_status: update_player_status, From 8f7ee32299eb69c81512b805f4cecb24c1930192 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 13 Apr 2024 11:05:59 +0200 Subject: [PATCH 051/224] Enhancement: Added reppeat auf the annoncement "In Vobereitung" at the end of the preparation call --- static/js/announcements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/announcements.js b/static/js/announcements.js index b734c00..a83d1cd 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -22,7 +22,7 @@ function announcePreparationMatch(matchSetup) { var eventName = createEventAnnouncement(matchSetup); var round = createRoundAnnouncement(matchSetup); var teams = createTeamAnnouncement(matchSetup); - announce([preparation, matchNumber, eventName, round, teams]); + announce([preparation, matchNumber, eventName, round, teams, preparation]); } function announceSecondCallTeamOne(matchSetup) { if(!(window.localStorage.getItem('enable_announcements') === 'true')) { From a398f8de9e08ad2e9de68157b757eb3ac3fc51c6 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 13 Apr 2024 11:28:12 +0200 Subject: [PATCH 052/224] Enhancement: Added announcement of the Umpire if on ist set to the match --- static/js/announcements.js | 39 ++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/static/js/announcements.js b/static/js/announcements.js index a83d1cd..86e5a07 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -4,13 +4,14 @@ function announceNewMatch(matchSetup) { if(!(window.localStorage.getItem('enable_announcements') === 'true')) { return; } - var field = createFieldAnnouncement(matchSetup); - var matchNumber = createMatchNumberAnnouncement(matchSetup); - var eventName = createEventAnnouncement(matchSetup); - var round = createRoundAnnouncement(matchSetup); - var teams = createTeamAnnouncement(matchSetup); - var tabletOperator = createTabletOperator(matchSetup); - announce([field, matchNumber, eventName, round, teams, tabletOperator, field]); + const field = createFieldAnnouncement(matchSetup); + const matchNumber = createMatchNumberAnnouncement(matchSetup); + const eventName = createEventAnnouncement(matchSetup); + const round = createRoundAnnouncement(matchSetup); + const teams = createTeamAnnouncement(matchSetup); + const umpire = createUmpire(matchSetup); + const tabletOperator = createTabletOperator(matchSetup); + announce([field, matchNumber, eventName, round, teams, umpire, tabletOperator, field]); } function announcePreparationMatch(matchSetup) { @@ -86,6 +87,14 @@ function createTabletOperator(matchSetup) { return tabletOperator; } +function createUmpire(matchSetup) { + if (matchSetup.umpire_name && matchSetup.umpire_name != null) { + return "Schiedsrichter: " + matchSetup.umpire_name; + } + return null; +} + + function createSingleTeam(playersSetup) { var team = playersSetup[0].name; if (playersSetup.length == 2) { @@ -220,13 +229,15 @@ function announce(callArray) { } } callArray.forEach(function (part) { - var words = new SpeechSynthesisUtterance(part); - words.lang = "de-DE"; - words.rate = 1.05; - words.pitch = 0.9; - words.volume = 2.0; - words.voice = voice; - window.speechSynthesis.speak(words); + if (part && part != null) { + var words = new SpeechSynthesisUtterance(part); + words.lang = "de-DE"; + words.rate = 1.05; + words.pitch = 0.9; + words.volume = 2.0; + words.voice = voice; + window.speechSynthesis.speak(words); + } }); }); } \ No newline at end of file From 27fa039c508e77f871547e23a59fa5fba73ac7a8 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 13 Apr 2024 11:29:29 +0200 Subject: [PATCH 053/224] Bugfix: NPE will be thwon if no matches are defined and service crashed afterwards --- bts/bupws.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index f7f609c..6f8e67e 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -289,16 +289,17 @@ function matches_handler(app, ws, tournament_key, court_id) { } return res; }); - } - - const event = create_event_representation(tournament); - event.matches = matches; - event.courts = courts; - const reply = { - status: 'ok', - event, - }; - notify_change_ws(ws, tournament_key, court_id, "score-update",reply) + + + const event = create_event_representation(tournament); + event.matches = matches; + event.courts = courts; + const reply = { + status: 'ok', + event, + }; + notify_change_ws(ws, tournament_key, court_id, "score-update", reply) + } }); } From 96567ebb658682ba9f3d13d55242c41c50a4a906 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 13 Apr 2024 13:02:24 +0200 Subject: [PATCH 054/224] Enhancement: Improve Umpire and Tabletoperatorhandling: Now it is possible to choose if both are required or only umpire will be called. Tabletoperator will be removed from list as well. --- bts/admin.js | 2 +- bts/btp_sync.js | 8 +++++--- static/js/announcements.js | 18 ++++-------------- static/js/change.js | 2 +- static/js/ci18n_de.js | 2 +- static/js/ci18n_en.js | 2 +- static/js/ctournament.js | 15 +++++++++++++++ 7 files changed, 28 insertions(+), 21 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 00b39bd..1c0e76b 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -57,7 +57,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'is_team', 'is_nation_competition', 'only_now_on_court', 'warmup', 'warmup_ready', 'warmup_start', 'ticker_enabled', 'ticker_url', 'ticker_password', - 'language', 'dm_style', + 'language', 'dm_style','tabletoperator_with_umpire_enabled', 'logo_background_color', 'logo_foreground_color']); if (msg.props.btp_timezone) { diff --git a/bts/btp_sync.js b/bts/btp_sync.js index e1e6ce7..0d35359 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -775,8 +775,10 @@ async function integrate_now_on_court(app, tkey, callback) { if(!setup.called_timestamp) { setup.called_timestamp = called_timestamp; try{ - const value = await serializedAsyncTask(admin, app, tkey, court_id, setup.umpire_name); - setup.tabletoperators = value; + const value = await serializedAsyncTask(admin, app, tkey, court_id); + if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)){ + setup.tabletoperators = value; + } } catch (err) { callback(err) @@ -848,7 +850,7 @@ function serialized(fn) { } } const serializedAsyncTask = serialized(get_last_looser_on_court); -function get_last_looser_on_court(admin, app, tkey, court_id, umpire_name) { +function get_last_looser_on_court(admin, app, tkey, court_id) { return new Promise((resolve, reject) => { const tabletoperator_querry = { 'tournament_key': tkey, court: null }; let tabletoperators = undefined; diff --git a/static/js/announcements.js b/static/js/announcements.js index 86e5a07..ccd972d 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -72,19 +72,10 @@ function createTeamAnnouncement(matchSetup) { } function createTabletOperator(matchSetup) { - var tabletOperator = "Tabletbedienung: "; - //if (matchSetup.teams[1].players[0].state) { - // tabletOperator = tabletOperator + matchSetup.teams[1].players[0].state; - //} else - if (matchSetup.tabletoperators) { - tabletOperator = tabletOperator + createSingleTeam(matchSetup.tabletoperators); - } else if (matchSetup.umpire_name) { - tabletOperator = tabletOperator + matchSetup.umpire_name; - } else { - tabletOperator = tabletOperator + "Verlierer des vorhergehenden Spiels"; - } - - return tabletOperator; + if (matchSetup.tabletoperators && matchSetup.tabletoperators != null) { + return "Tabletbedienung: " + createSingleTeam(matchSetup.tabletoperators); + } + return null; } function createUmpire(matchSetup) { @@ -94,7 +85,6 @@ function createUmpire(matchSetup) { return null; } - function createSingleTeam(playersSetup) { var team = playersSetup[0].name; if (playersSetup.length == 2) { diff --git a/static/js/change.js b/static/js/change.js index aa19812..c45e90f 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -69,7 +69,7 @@ function default_handler_func(rerender, special_funcs, c) { const CHECKBOXES = [ 'is_team', 'is_nation_competition', 'only_now_on_court', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', - 'ticker_enabled']; + 'ticker_enabled', 'tabletoperator_with_umpire_enabled']; for (const cb_name of CHECKBOXES) { uiu.qsEach('input[name="' + cb_name + '"]', function(el) { el.checked = curt[cb_name]; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 31e5915..2deaf4d 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -69,7 +69,7 @@ var ci18n_de = { 'tournament:edit:btp:password': 'BTP-Passwort:', 'tournament:edit:btp:timezone': 'BTP-Zeitzone:', 'tournament:edit:btp:system timezone': 'Systemeinstellung ({tz})', - +'tournament:edit:tableroperator_with_umpire': 'Schiedrichter und Tabletbediener aufrufen', 'tournament:edit:ticker_enabled': 'Ticker aktivieren', 'tournament:edit:ticker_url': 'Ticker-Adresse:', 'tournament:edit:ticker_password': 'Ticker-Passwort:', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index caf7ee5..48010c3 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -73,7 +73,7 @@ var ci18n_en = { 'tournament:edit:ticker_enabled': 'Activate online ticker', 'tournament:edit:ticker_url': 'Ticker URL:', 'tournament:edit:ticker_password': 'Ticker password:', - +'tournament:edit:tableroperator_with_umpire': 'Announce Umpire and Tabletoperator ', 'to_stats:header': 'Technical Officials Statistics', 'to_stats:name': 'Name', 'to_stats:umpire': 'U', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 987e9a6..1e9a3f3 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -736,6 +736,20 @@ function ui_edit() { value: (curt.ticker_password || ''), }); + const to_withumpire_attrs_label = uiu.el(ticker_fieldset, 'label'); + const to_withumpire_attrs = { + type: 'checkbox', + name: 'tabletoperator_with_umpire_enabled', + }; + if (curt.tabletoperator_with_umpire_enabled) { + to_withumpire_attrs.checked = 'checked'; + } + uiu.el(to_withumpire_attrs_label, 'input', to_withumpire_attrs); + uiu.el(to_withumpire_attrs_label, 'span', {}, ci18n('tournament:edit:tableroperator_with_umpire')); + + + + uiu.el(form, 'button', { role: 'submit', }, ci18n('Change')); @@ -759,6 +773,7 @@ function ui_edit() { ticker_enabled: (!! data.ticker_enabled), ticker_url: data.ticker_url, ticker_password: data.ticker_password, + tabletoperator_with_umpire_enabled: (!!data.tabletoperator_with_umpire_enabled), }; send({ type: 'tournament_edit_props', From efb32d7ef931b115bc105f1b01bd69dc2383f8e3 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 13 Apr 2024 13:48:53 +0200 Subject: [PATCH 055/224] Enhancment: Now it is possible to switch Teblateoperatormanagement on and of --- bts/admin.js | 2 +- bts/btp_sync.js | 11 ++++++----- bts/http_api.js | 17 ++++++++++++----- static/js/change.js | 2 +- static/js/ci18n_de.js | 3 ++- static/js/ci18n_en.js | 1 + static/js/ctournament.js | 14 ++++++++++++-- 7 files changed, 35 insertions(+), 15 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 1c0e76b..d805df9 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -57,7 +57,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'is_team', 'is_nation_competition', 'only_now_on_court', 'warmup', 'warmup_ready', 'warmup_start', 'ticker_enabled', 'ticker_url', 'ticker_password', - 'language', 'dm_style','tabletoperator_with_umpire_enabled', + 'language', 'dm_style', 'tabletoperator_enabled','tabletoperator_with_umpire_enabled', 'logo_background_color', 'logo_foreground_color']); if (msg.props.btp_timezone) { diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 0d35359..b91e577 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -774,12 +774,13 @@ async function integrate_now_on_court(app, tkey, callback) { const setup = match.setup; if(!setup.called_timestamp) { setup.called_timestamp = called_timestamp; - try{ - const value = await serializedAsyncTask(admin, app, tkey, court_id); - if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)){ - setup.tabletoperators = value; + try { + if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { + const value = await serializedAsyncTask(admin, app, tkey, court_id); + if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { + setup.tabletoperators = value; + } } - } catch (err) { callback(err) } diff --git a/bts/http_api.js b/bts/http_api.js index 39422f0..69b7e67 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -379,12 +379,18 @@ function score_handler(req, res) { } function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, end_ts) { - const admin = require('./admin'); // avoid dependency cycle - app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { + app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { if (err) { - return reject(err); - } - add_player_to_tabletoperator_list_by_match(app, tournament_key, cur_match, end_ts) + return callback(err); + } + if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { + app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { + if (err) { + return reject(err); + } + add_player_to_tabletoperator_list_by_match(app, tournament_key, cur_match, end_ts) + }); + } }); } @@ -425,6 +431,7 @@ function add_player_to_tabletoperator_list_by_match(app, tournament_key, cur_mat ws.respond(msg, err); return; } + const admin = require('./admin'); // avoid dependency cycle admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); }); diff --git a/static/js/change.js b/static/js/change.js index c45e90f..c5df7a0 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -69,7 +69,7 @@ function default_handler_func(rerender, special_funcs, c) { const CHECKBOXES = [ 'is_team', 'is_nation_competition', 'only_now_on_court', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', - 'ticker_enabled', 'tabletoperator_with_umpire_enabled']; + 'ticker_enabled', 'tabletoperator_enabled','tabletoperator_with_umpire_enabled']; for (const cb_name of CHECKBOXES) { uiu.qsEach('input[name="' + cb_name + '"]', function(el) { el.checked = curt[cb_name]; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 2deaf4d..0525a1c 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -68,7 +68,8 @@ var ci18n_de = { 'tournament:edit:btp:ip': 'IP:', 'tournament:edit:btp:password': 'BTP-Passwort:', 'tournament:edit:btp:timezone': 'BTP-Zeitzone:', -'tournament:edit:btp:system timezone': 'Systemeinstellung ({tz})', +'tournament:edit:btp:system timezone': 'Systemeinstellung ({tz})', +'tournament:edit:tableroperator_enabled': 'Tabletbediener einsetzen', 'tournament:edit:tableroperator_with_umpire': 'Schiedrichter und Tabletbediener aufrufen', 'tournament:edit:ticker_enabled': 'Ticker aktivieren', 'tournament:edit:ticker_url': 'Ticker-Adresse:', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 48010c3..9ed3d39 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -73,6 +73,7 @@ var ci18n_en = { 'tournament:edit:ticker_enabled': 'Activate online ticker', 'tournament:edit:ticker_url': 'Ticker URL:', 'tournament:edit:ticker_password': 'Ticker password:', +'tournament:edit:tableroperator_enabled': 'Use Tabletoperators', 'tournament:edit:tableroperator_with_umpire': 'Announce Umpire and Tabletoperator ', 'to_stats:header': 'Technical Officials Statistics', 'to_stats:name': 'Name', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 1e9a3f3..e8cae3b 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -736,6 +736,17 @@ function ui_edit() { value: (curt.ticker_password || ''), }); + const to_attrs_label = uiu.el(ticker_fieldset, 'label'); + const to_attrs = { + type: 'checkbox', + name: 'tabletoperator_enabled', + }; + if (curt.tabletoperator_enabled) { + to_attrs.checked = 'checked'; + } + uiu.el(to_attrs_label, 'input', to_attrs); + uiu.el(to_attrs_label, 'span', {}, ci18n('tournament:edit:tableroperator_enabled')); + const to_withumpire_attrs_label = uiu.el(ticker_fieldset, 'label'); const to_withumpire_attrs = { type: 'checkbox', @@ -748,8 +759,6 @@ function ui_edit() { uiu.el(to_withumpire_attrs_label, 'span', {}, ci18n('tournament:edit:tableroperator_with_umpire')); - - uiu.el(form, 'button', { role: 'submit', }, ci18n('Change')); @@ -774,6 +783,7 @@ function ui_edit() { ticker_url: data.ticker_url, ticker_password: data.ticker_password, tabletoperator_with_umpire_enabled: (!!data.tabletoperator_with_umpire_enabled), + tabletoperator_enabled: (!!data.tabletoperator_enabled), }; send({ type: 'tournament_edit_props', From 7a3c3b9812a14df905cf770a267b4b20cacdd664 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 13 Apr 2024 14:31:07 +0200 Subject: [PATCH 056/224] Enhancement: Second call is now also possible for umpires and tabetoperators. --- static/js/announcements.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/static/js/announcements.js b/static/js/announcements.js index ccd972d..2b52e8f 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -40,11 +40,20 @@ function announceSecondCallTeamTwo(matchSetup) { } function announceSecondCallTabletoperator(matchSetup) { - if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + if (!(window.localStorage.getItem('enable_announcements') === 'true')) { return; } - const call = createFieldAnnouncement(matchSetup) + createSecondCallAnnouncement() + createTabletOperator(matchSetup); - announce([call]); + const umpireCall = createUmpire(matchSetup);; + if (umpireCall != null) { + const call = createFieldAnnouncement(matchSetup) + createSecondCallAnnouncement() + umpireCall; + announce([call]); + } + + const tabletOperatorCall = createTabletOperator(matchSetup);; + if (tabletOperatorCall != null) { + const call = createFieldAnnouncement(matchSetup) + createSecondCallAnnouncement() + tabletOperatorCall; + announce([call]); + } } function announceSecondCall(matchSetup, team) { From 6f31d247d47a21d5fecd1ab096854b00b7de2211 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 13 Apr 2024 14:36:56 +0200 Subject: [PATCH 057/224] Enhancement: Now both: Umpire and Tabletoperator will be displayed if both a called for am match --- static/js/cmatch.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index b5e07a9..8274a7a 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -132,14 +132,17 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ uiu.el(to_td, 'span', {}, ' \u200B+ '); uiu.el(to_td, 'span', {}, setup.service_judge_name); } - } else if (setup.tabletoperators && setup.tabletoperators.length > 0){ + } + if (setup.tabletoperators && setup.tabletoperators.length > 0) { uiu.el(to_td, 'div', 'tablet', ''); uiu.el(to_td, 'span', 'match_no_umpire', setup.tabletoperators[0].name ); if (setup.tabletoperators.length > 1) { uiu.el(to_td, 'span', 'match_no_umpire', ' \u200B/ '); uiu.el(to_td, 'span', 'match_no_umpire', setup.tabletoperators[1].name ); } - } else { + } + + if (!setup.umpire_name && (!setup.tabletoperators || setup.tabletoperators.length == 0)) { uiu.el(to_td, 'div', 'no_umpire', ''); uiu.el(to_td, 'span', 'match_no_umpire', ci18n('No umpire')); From c07720f9ba15c4a3db3a9f672cb3461692a632e3 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 13 Apr 2024 15:28:15 +0200 Subject: [PATCH 058/224] Enhancement: Now it is possible to choose between winner and looser of quaterfinals to to the tabletoperator job. Also you can switch betrween "Klapptafelbedinung" und "Tabletbedienung" --- bts/admin.js | 8 +++++++- bts/btp_sync.js | 12 +++++++++-- bts/http_api.js | 14 ++++++------- bts/stournament.js | 11 ++++++++++ static/js/announcements.js | 2 +- static/js/change.js | 3 ++- static/js/ci18n_de.js | 6 ++++-- static/js/ci18n_en.js | 6 ++++-- static/js/ctournament.js | 41 +++++++++++++++++++------------------- 9 files changed, 66 insertions(+), 37 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index d805df9..19ed6ea 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -57,7 +57,8 @@ function handle_tournament_edit_props(app, ws, msg) { 'is_team', 'is_nation_competition', 'only_now_on_court', 'warmup', 'warmup_ready', 'warmup_start', 'ticker_enabled', 'ticker_url', 'ticker_password', - 'language', 'dm_style', 'tabletoperator_enabled','tabletoperator_with_umpire_enabled', + 'language', 'dm_style', 'tabletoperator_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', + 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', 'logo_background_color', 'logo_foreground_color']); if (msg.props.btp_timezone) { @@ -145,6 +146,11 @@ function handle_tournament_get(app, ws, msg) { tournament.matches = matches; cb(err); }); + }, function (cb) { + stournament.get_displays(app.db, tournament.key, function (err, displays) { + tournament.displays = displays; + cb(err); + }); }], function(err) { tournament.btp_status = btp_manager.get_status(tournament.key); tournament.ticker_status = ticker_manager.get_status(tournament.key); diff --git a/bts/btp_sync.js b/bts/btp_sync.js index b91e577..574eb34 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -427,8 +427,16 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { result_enterd_in_btp = true; match.setup.warmup = 'none'; match.end_ts = Date.now(); - const http_api = require('./http_api'); - http_api.add_player_to_tabletoperator_list_by_match(app, tkey, match, match.end_ts); + + app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { + if (err) { + return callback(err); + } + if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { + const http_api = require('./http_api'); + http_api.add_player_to_tabletoperator_list_by_match(app, tournament, tkey, match, match.end_ts); + } + }); } } diff --git a/bts/http_api.js b/bts/http_api.js index 69b7e67..05d3bad 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -388,13 +388,13 @@ function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, en if (err) { return reject(err); } - add_player_to_tabletoperator_list_by_match(app, tournament_key, cur_match, end_ts) + add_player_to_tabletoperator_list_by_match(app, tournament, tournament_key, cur_match, end_ts) }); } }); } -function add_player_to_tabletoperator_list_by_match(app, tournament_key, cur_match, end_ts) { +function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_key, cur_match, end_ts) { if (cur_match.network_score) { // walkovers and retirements will not be recorgnized. app.db.tabletoperators.findOne({ 'tournament_key': tournament_key, 'match_id': cur_match._id }, (err, no_tabletoperator) => { @@ -404,13 +404,13 @@ function add_player_to_tabletoperator_list_by_match(app, tournament_key, cur_mat if (no_tabletoperator == null) { const round = cur_match.setup.match_name; var team = null; - // Not nessasaary if not TR 15/5 - //if (round == 'VF' || round == 'QF') { - // team = cur_match.setup.teams[cur_match.btp_winner - 1]; - //} else { + + if (tournament.tabletoperator_winner_of_quaterfinals_enabled && (round == 'VF' || round == 'QF')) { + team = cur_match.setup.teams[cur_match.btp_winner - 1]; + } else { const index = cur_match.btp_winner % 2; team = cur_match.setup.teams[index]; - //} + } if (team && typeof team.players !== 'undefined') { var tabletoperator = []; diff --git a/bts/stournament.js b/bts/stournament.js index 20423ed..c90680d 100644 --- a/bts/stournament.js +++ b/bts/stournament.js @@ -38,10 +38,21 @@ function get_tabletoperators(db, tournament_key, callback) { }); } +function get_displays(db, tournament_key, callback) { + db.display_court_displaysettings.find({}, function (err, display_court_displaysettings) { + if (err) return callback(err); + + // TODO: Append not registered Displays and set status online/offline of registered displays by using ite registered ws in bubws + + return callback(err, display_court_displaysettings); + }); +} + module.exports = { get_courts, get_matches, get_umpires, get_tabletoperators, + get_displays, }; diff --git a/static/js/announcements.js b/static/js/announcements.js index 2b52e8f..e7c0fc0 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -82,7 +82,7 @@ function createTeamAnnouncement(matchSetup) { function createTabletOperator(matchSetup) { if (matchSetup.tabletoperators && matchSetup.tabletoperators != null) { - return "Tabletbedienung: " + createSingleTeam(matchSetup.tabletoperators); + return (curt.tabletoperator_use_manual_counting_boards_enabled ? "Klapptafelbedienung:" : "Tabletbedienung: ") + createSingleTeam(matchSetup.tabletoperators); } return null; } diff --git a/static/js/change.js b/static/js/change.js index c5df7a0..78c8ef9 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -69,7 +69,8 @@ function default_handler_func(rerender, special_funcs, c) { const CHECKBOXES = [ 'is_team', 'is_nation_competition', 'only_now_on_court', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', - 'ticker_enabled', 'tabletoperator_enabled','tabletoperator_with_umpire_enabled']; + 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', + 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled']; for (const cb_name of CHECKBOXES) { uiu.qsEach('input[name="' + cb_name + '"]', function(el) { el.checked = curt[cb_name]; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 0525a1c..2cdcbf6 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -69,8 +69,10 @@ var ci18n_de = { 'tournament:edit:btp:password': 'BTP-Passwort:', 'tournament:edit:btp:timezone': 'BTP-Zeitzone:', 'tournament:edit:btp:system timezone': 'Systemeinstellung ({tz})', -'tournament:edit:tableroperator_enabled': 'Tabletbediener einsetzen', -'tournament:edit:tableroperator_with_umpire': 'Schiedrichter und Tabletbediener aufrufen', +'tournament:edit:tabletoperator_enabled': 'Tabletbediener einsetzen', +'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Gewinner der Viertelfinals bedienen das Tablet', +'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', +'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', 'tournament:edit:ticker_enabled': 'Ticker aktivieren', 'tournament:edit:ticker_url': 'Ticker-Adresse:', 'tournament:edit:ticker_password': 'Ticker-Passwort:', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 9ed3d39..82de34e 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -73,8 +73,10 @@ var ci18n_en = { 'tournament:edit:ticker_enabled': 'Activate online ticker', 'tournament:edit:ticker_url': 'Ticker URL:', 'tournament:edit:ticker_password': 'Ticker password:', -'tournament:edit:tableroperator_enabled': 'Use Tabletoperators', -'tournament:edit:tableroperator_with_umpire': 'Announce Umpire and Tabletoperator ', +'tournament:edit:tabletoperator_enabled': 'Use Tabletoperators', +'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Winner of Quarterfinals hav to do tabletoperatorservice', +'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', +'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', 'to_stats:header': 'Technical Officials Statistics', 'to_stats:name': 'Name', 'to_stats:umpire': 'U', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index e8cae3b..6df278a 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -736,28 +736,12 @@ function ui_edit() { value: (curt.ticker_password || ''), }); - const to_attrs_label = uiu.el(ticker_fieldset, 'label'); - const to_attrs = { - type: 'checkbox', - name: 'tabletoperator_enabled', - }; - if (curt.tabletoperator_enabled) { - to_attrs.checked = 'checked'; - } - uiu.el(to_attrs_label, 'input', to_attrs); - uiu.el(to_attrs_label, 'span', {}, ci18n('tournament:edit:tableroperator_enabled')); - - const to_withumpire_attrs_label = uiu.el(ticker_fieldset, 'label'); - const to_withumpire_attrs = { - type: 'checkbox', - name: 'tabletoperator_with_umpire_enabled', - }; - if (curt.tabletoperator_with_umpire_enabled) { - to_withumpire_attrs.checked = 'checked'; - } - uiu.el(to_withumpire_attrs_label, 'input', to_withumpire_attrs); - uiu.el(to_withumpire_attrs_label, 'span', {}, ci18n('tournament:edit:tableroperator_with_umpire')); + create_checkbox(curt, ticker_fieldset,'tabletoperator_enabled') + create_checkbox(curt, ticker_fieldset, 'tabletoperator_with_umpire_enabled') + create_checkbox(curt, ticker_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled') + create_checkbox(curt, ticker_fieldset, 'tabletoperator_use_manual_counting_boards_enabled') + uiu.el(form, 'button', { role: 'submit', @@ -783,6 +767,8 @@ function ui_edit() { ticker_url: data.ticker_url, ticker_password: data.ticker_password, tabletoperator_with_umpire_enabled: (!!data.tabletoperator_with_umpire_enabled), + tabletoperator_winner_of_quaterfinals_enabled: (!!data.tabletoperator_winner_of_quaterfinals_enabled), + tabletoperator_use_manual_counting_boards_enabled: (!!data.tabletoperator_use_manual_counting_boards_enabled), tabletoperator_enabled: (!!data.tabletoperator_enabled), }; send({ @@ -920,6 +906,19 @@ function ui_edit() { _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit); +function create_checkbox(curt, parent_el, filed_id) { + const label = uiu.el(parent_el, 'label'); + const attrs = { + type: 'checkbox', + name: filed_id, + }; + if (curt[filed_id]) { + attrs.checked = 'checked'; + } + uiu.el(label, 'input', attrs); + uiu.el(label, 'span', {}, ci18n('tournament:edit:' + filed_id)); +} + function render_upcoming(container) { cmatch.prepare_render(curt); const courts_container = uiu.el(container, 'div', 'courts_container'); From d1d53e154a6c858aa1aff7988d45137d8e92922a Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 13 Apr 2024 16:26:22 +0200 Subject: [PATCH 059/224] New Function: Annoncement now also available in English (experimental) --- static/js/announcements.js | 78 ++++++------ static/js/ci18n_de.js | 253 +++++++++++++++++++++---------------- static/js/ci18n_en.js | 253 +++++++++++++++++++++---------------- 3 files changed, 321 insertions(+), 263 deletions(-) diff --git a/static/js/announcements.js b/static/js/announcements.js index e7c0fc0..8e63bea 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -68,28 +68,28 @@ function announceBeginnToPlay(matchSetup, team) { if(!(window.localStorage.getItem('enable_announcements') === 'true')) { return; } - announce([createFieldAnnouncement(matchSetup) + "Bitte mit dem Spielen beginnen!"]); + announce([createFieldAnnouncement(matchSetup) + ci18n('announcements:begin_to_play')]); } function createSecondCallAnnouncement() { - return "Zweiter Aufruf fuer:"; + return ci18n('announcements:second_call'); } function createTeamAnnouncement(matchSetup) { - var teams = createSingleTeam(matchSetup.teams[0].players) + ", gegen " + createSingleTeam(matchSetup.teams[1].players); + var teams = createSingleTeam(matchSetup.teams[0].players) + "," + ci18n('announcements:vs') + createSingleTeam(matchSetup.teams[1].players); return teams; } function createTabletOperator(matchSetup) { if (matchSetup.tabletoperators && matchSetup.tabletoperators != null) { - return (curt.tabletoperator_use_manual_counting_boards_enabled ? "Klapptafelbedienung:" : "Tabletbedienung: ") + createSingleTeam(matchSetup.tabletoperators); + return (curt.tabletoperator_use_manual_counting_boards_enabled ? ci18n('announcements:counting_board_service') : ci18n('announcements:table_service')) + createSingleTeam(matchSetup.tabletoperators); } return null; } function createUmpire(matchSetup) { if (matchSetup.umpire_name && matchSetup.umpire_name != null) { - return "Schiedsrichter: " + matchSetup.umpire_name; + return ci18n('announcements:umpire') + matchSetup.umpire_name; } return null; } @@ -97,7 +97,7 @@ function createUmpire(matchSetup) { function createSingleTeam(playersSetup) { var team = playersSetup[0].name; if (playersSetup.length == 2) { - team = team + " und " + playersSetup[1].name + team = team + ci18n('announcements:and')+ playersSetup[1].name } return team; } @@ -105,23 +105,23 @@ function createSingleTeam(playersSetup) { function createRoundAnnouncement(matchSetup) { var round = matchSetup.match_name; if (round == "R16") { - round = "Achtelfinale"; + round = ci18n('announcements:round_16'); } else if (round == "VF") { - round = "Viertelfinale"; + round = ci18n('announcements:quaterfinal'); } else if (round == "HF") { - round = "Halbfinale"; + round = ci18n('announcements:semifinal'); } else if (round == "Finale") { - round = "Finale"; + round = ci18n('announcements:final'); } else if (round.indexOf('/') !== -1) { var roundParts = round.split("/") var diff = roundParts[1] - roundParts[0]; if (diff > 1) { - round = "Zwischenrunde"; + round = ci18n('announcements:intermediate_round'); } else { - round = "Spiel um Platz " + roundParts[0] + " und " + roundParts[1]; + round = ci18n('announcements:game_for_place') + roundParts[0] + ci18n('announcements:and') + roundParts[1]; } } else if (round.indexOf('-') !== -1) { - round = "Zwischenrunde"; + round = ci18n('announcements:intermediate_round'); } else { round = ""; } @@ -131,43 +131,43 @@ function createEventAnnouncement(matchSetup) { var eventParts = matchSetup.event_name.split(" "); var eventName = ""; if (eventParts[0] == 'JE') { - eventName = "Jungeneinzel" + eventName = ci18n('announcements:boys_singles'); } else if (eventParts[0] == 'JD') { - eventName = "Jungendoppel" + eventName = ci18n('announcements:boys_doubles'); } else if (eventParts[0] == 'ME') { - eventName = "Maedcheneinzel" + eventName = ci18n('announcements:girls_singles'); } else if (eventParts[0] == 'MD') { - eventName = "Maedchendoppel" + eventName = ci18n('announcements:girls_doubles') } else if (eventParts[0] == 'GD' || eventParts[0] == 'MX') { - eventName = "Gemischtesdoppel" + eventName = ci18n('announcements:mixed_doubles') } else if (eventParts[0] == 'HE') { - eventName = "Herreneinzel" + eventName = ci18n('announcements:men_singles'); } else if (eventParts[0] == 'HD') { - eventName = "Herrendoppel" + eventName = ci18n('announcements:men_doubles'); } else if (eventParts[0] == 'DE') { - eventName = "Dameneinzel" + eventName = ci18n('announcements:women_singles'); } else if (eventParts[0] == 'DD') { - eventName = "Damendoppel" + eventName = ci18n('announcements:women_doubles'); } if (eventName == "") { if (eventParts[1] == 'JE') { - eventName = "Jungeneinzel" + eventName = ci18n('announcements:boys_singles'); } else if (eventParts[1] == 'JD') { - eventName = "Jungendoppel" + eventName = ci18n('announcements:boys_doubles'); } else if (eventParts[1] == 'ME') { - eventName = "Maedcheneinzel" + eventName = ci18n('announcements:girls_singles'); } else if (eventParts[1] == 'MD') { - eventName = "Maedchendoppel" + eventName = ci18n('announcements:girls_doubles') } else if (eventParts[1] == 'GD' || eventParts[1] == 'MX') { - eventName = "Gemischtesdoppel" + eventName = ci18n('announcements:mixed_doubles') } else if (eventParts[1] == 'HE') { - eventName = "Herreneinzel" + eventName = ci18n('announcements:men_singles'); } else if (eventParts[1] == 'HD') { - eventName = "Herrendoppel" + eventName = ci18n('announcements:men_doubles'); } else if (eventParts[1] == 'DE') { - eventName = "Dameneinzel" + eventName = ci18n('announcements:women_singles'); } else if (eventParts[1] == 'DD') { - eventName = "Damendoppel" + eventName = ci18n('announcements:women_doubles'); } if (eventParts[0]) { eventName = eventName + " " + eventParts[0]; @@ -182,13 +182,13 @@ function createEventAnnouncement(matchSetup) { function createMatchNumberAnnouncement(matchSetup) { var number = matchSetup.match_num; - return "Spiel Nummer " + number + "!"; + return ci18n('announcements:match_number') + number + "!"; } function createFieldAnnouncement(matchSetup) { if (matchSetup.court_id) { var court = matchSetup.court_id.split("_")[1]; - return "Auf Spielfeld " + court + "!"; + return ci18n('announcements:on_court') + court + "!"; } else { return ""; } @@ -196,7 +196,7 @@ function createFieldAnnouncement(matchSetup) { } function createPreparationAnnouncement() { - return "In Vorbereitung:"; + return ci18n('announcements:preparation'); } function announce(callArray) { @@ -222,7 +222,7 @@ function announce(callArray) { allVoicesObtained.then(voices => { var voice = null; for (var i = 0; i < voices.length; i++) { - if (voices[i].voiceURI == "Google Deutsch") { + if (voices[i].voiceURI == ci18n('announcements:voice')) { voice = voices[i]; break; } @@ -230,10 +230,10 @@ function announce(callArray) { callArray.forEach(function (part) { if (part && part != null) { var words = new SpeechSynthesisUtterance(part); - words.lang = "de-DE"; - words.rate = 1.05; - words.pitch = 0.9; - words.volume = 2.0; + words.lang = ci18n('announcements:lang'); + words.rate = 1; + words.pitch = 0; + words.volume = 1; words.voice = voice; window.speechSynthesis.speak(words); } diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 2cdcbf6..4bc4078 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -1,124 +1,153 @@ var ci18n_de = { -'_code': 'de', -'_name': 'Deutsch (Deutschland)', + '_code': 'de', + '_name': 'Deutsch (Deutschland)', -'Unassigned Matches': 'Nicht zugewiesene Spiele', -'Next Matches': 'Nächste Spiele', -'edit tournament': 'Turnier bearbeiten', -'Court': 'Court', -'Match': 'Spiel', -'Players': 'Spieler', -'Umpire': 'Schiedsrichter', -'State': 'Status', -'Umpire:': 'Schiedsrichter:', -'Service judge:': 'Aufschlagrichter:', -'Finished Matches': 'Abgeschlossene Spiele', -'Time:': 'Zeit:', -'(Singles)': '(Einzel)', -'e.g. MX O55': 'z.B. MX O55', -'e.g. semi-finals': 'z.B. Halbfinale', -'Number:': '#:', -'Cancel': 'Abbrechen', -'Change': 'Ändern', -'No umpire': 'Kein Schiedsrichter', -'No service judge': 'Kein Aufschlagrichter', -'Edit match': 'Match bearbeiten', -'Back': 'Zurück', -'PDF': 'PDF', -'Print': 'Drucken', -'Add Match': 'Spiel hinzufügen', -' Ready to start ': ' Spielbereit ', -'Ready': 'Bereit', -'Not assigned': 'Nicht zugewiesen', -'team competition': 'Mannschafts-Wettbewerb', -'nation competition': 'Internationales Turnier', -'update from BTP': 'Von BTP aktualisieren', -'update ticker': 'Ticker aktualisieren', -'Tournaments': 'Turniere', -'referee view': 'Referee-Ansicht', -'Connecting ...': 'Verbinde ...', -'Connected': 'Verbunden', -'Connection lost': 'Verbindung verloren', -'Create tournament': 'Turnier erstellen', -'create:id:label': 'Turnier-ID (Kleinbuchstaben, keine Leerzeichen):', -'experimental': '(experimentell)', -'Winner': 'Gewinner', -'Loser': 'Verlierer', + 'Unassigned Matches': 'Nicht zugewiesene Spiele', + 'Next Matches': 'Nächste Spiele', + 'edit tournament': 'Turnier bearbeiten', + 'Court': 'Court', + 'Match': 'Spiel', + 'Players': 'Spieler', + 'Umpire': 'Schiedsrichter', + 'State': 'Status', + 'Umpire:': 'Schiedsrichter:', + 'Service judge:': 'Aufschlagrichter:', + 'Finished Matches': 'Abgeschlossene Spiele', + 'Time:': 'Zeit:', + '(Singles)': '(Einzel)', + 'e.g. MX O55': 'z.B. MX O55', + 'e.g. semi-finals': 'z.B. Halbfinale', + 'Number:': '#:', + 'Cancel': 'Abbrechen', + 'Change': 'Ändern', + 'No umpire': 'Kein Schiedsrichter', + 'No service judge': 'Kein Aufschlagrichter', + 'Edit match': 'Match bearbeiten', + 'Back': 'Zurück', + 'PDF': 'PDF', + 'Print': 'Drucken', + 'Add Match': 'Spiel hinzufügen', + ' Ready to start ': ' Spielbereit ', + 'Ready': 'Bereit', + 'Not assigned': 'Nicht zugewiesen', + 'team competition': 'Mannschafts-Wettbewerb', + 'nation competition': 'Internationales Turnier', + 'update from BTP': 'Von BTP aktualisieren', + 'update ticker': 'Ticker aktualisieren', + 'Tournaments': 'Turniere', + 'referee view': 'Referee-Ansicht', + 'Connecting ...': 'Verbinde ...', + 'Connected': 'Verbunden', + 'Connection lost': 'Verbindung verloren', + 'Create tournament': 'Turnier erstellen', + 'create:id:label': 'Turnier-ID (Kleinbuchstaben, keine Leerzeichen):', + 'experimental': '(experimentell)', + 'Winner': 'Gewinner', + 'Loser': 'Verlierer', -'tournament:edit:id': 'Turnier-ID:', -'tournament:edit:language': 'Sprache:', -'tournament:edit:language:auto': 'Nicht gesetzt (Browser-Einstellung)', -'tournament:edit:name': 'Name:', -'tournament:edit:courts': 'Felder:', -'tournament:edit:dm_style': 'Standard-Ansicht:', -'tournament:edit:only_now_on_court': 'Spiele müssen auf Feld gezogen werden', -'tournament:edit:warmup_timer_behavior': 'Verhalten des Vorbereitungs-Countdowns:', -'tournament:edit:warmup_timer_behavior:bwf-2016': 'BWF ab 2016 (ab Auslosung)', -'tournament:edit:warmup_timer_behavior:legacy': 'Deutschland (ab Auslosung)', -'tournament:edit:warmup_timer_behavior:choise': 'ab Auslosung (individuelle Zeit)', -'tournament:edit:warmup_timer_behavior:call-down': 'ab Aufruf (Countdown)', -'tournament:edit:warmup_timer_behavior:call-up': 'ab Aufruf (Timer)', -'tournament:edit:warmup_timer_behavior:none': 'Sofort beginen', -'tournament:edit:warmup_ready': 'Spielbereit in Sekunden:', -'tournament:edit:warmup_start': 'Spielstart nach Sekunden:', -'tournament:edit:btp:enabled': 'BTP-Anbindung aktivieren', -'tournament:edit:btp:autofetch_enabled': 'Automatisch synchronisieren', -'tournament:edit:btp:readonly': 'Nur lesen', -'tournament:edit:btp:ip': 'IP:', -'tournament:edit:btp:password': 'BTP-Passwort:', -'tournament:edit:btp:timezone': 'BTP-Zeitzone:', -'tournament:edit:btp:system timezone': 'Systemeinstellung ({tz})', -'tournament:edit:tabletoperator_enabled': 'Tabletbediener einsetzen', -'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Gewinner der Viertelfinals bedienen das Tablet', -'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', -'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', -'tournament:edit:ticker_enabled': 'Ticker aktivieren', -'tournament:edit:ticker_url': 'Ticker-Adresse:', -'tournament:edit:ticker_password': 'Ticker-Passwort:', + 'announcements:begin_to_play': 'Bitte mit dem Spielen beginnen!', + 'announcements:second_call': '"Zweiter Aufruf fuer:"', + 'announcements:vs': ' gegen ', + 'announcements:counting_board_service': 'Klapptafelbedienung:', + 'announcements:table_service': 'Tabletbedienung:', + 'announcements:umpire': 'Schiedsrichter:', + 'announcements:and': ' und ', + 'announcements:preparation': 'In Vorbereitung:', + 'announcements:on_court': 'Auf Spielfeld ', + 'announcements:match_number': 'Spiel Nummer ', + 'announcements:boys_singles': 'Jungeneinzel', + 'announcements:boys_doubles': 'Jungendoppel', + 'announcements:girls_singles': 'Maedchenneinzel', + 'announcements:girls_doubles': 'Maedchendoppel', + 'announcements:mixed_doubles': 'Gemischtes Doppel', + 'announcements:men_singles': 'Herreneinzel', + 'announcements:men_doubles': 'Herrendoppel', + 'announcements:women_singles': 'Dameneinzel', + 'announcements:women_doubles': 'Damendoppel', + 'announcements:round_16': 'Achtelfinale', + 'announcements:quaterfinal': 'Viertelfinale', + 'announcements:semifinal': 'Halbfinale', + 'announcements:final': 'Finale', + 'announcements:intermediate_round': 'Zwischenrunde', + 'announcements:game_for_place': 'Spiel um Platz', + 'announcements:voice': 'Google Deutsch', + 'announcements:lang': 'de-DE', + -'to_stats:header': 'Statistik der Technischen Offiziellen', -'to_stats:name': 'Name', -'to_stats:umpire': 'SR', -'to_stats:service_judge': 'AR', -'to_stats:total': 'Total', + 'tournament:edit:id': 'Turnier-ID:', + 'tournament:edit:language': 'Sprache:', + 'tournament:edit:language:auto': 'Nicht gesetzt (Browser-Einstellung)', + 'tournament:edit:name': 'Name:', + 'tournament:edit:courts': 'Felder:', + 'tournament:edit:dm_style': 'Standard-Ansicht:', + 'tournament:edit:only_now_on_court': 'Spiele müssen auf Feld gezogen werden', + 'tournament:edit:warmup_timer_behavior': 'Verhalten des Vorbereitungs-Countdowns:', + 'tournament:edit:warmup_timer_behavior:bwf-2016': 'BWF ab 2016 (ab Auslosung)', + 'tournament:edit:warmup_timer_behavior:legacy': 'Deutschland (ab Auslosung)', + 'tournament:edit:warmup_timer_behavior:choise': 'ab Auslosung (individuelle Zeit)', + 'tournament:edit:warmup_timer_behavior:call-down': 'ab Aufruf (Countdown)', + 'tournament:edit:warmup_timer_behavior:call-up': 'ab Aufruf (Timer)', + 'tournament:edit:warmup_timer_behavior:none': 'Sofort beginen', + 'tournament:edit:warmup_ready': 'Spielbereit in Sekunden:', + 'tournament:edit:warmup_start': 'Spielstart nach Sekunden:', + 'tournament:edit:btp:enabled': 'BTP-Anbindung aktivieren', + 'tournament:edit:btp:autofetch_enabled': 'Automatisch synchronisieren', + 'tournament:edit:btp:readonly': 'Nur lesen', + 'tournament:edit:btp:ip': 'IP:', + 'tournament:edit:btp:password': 'BTP-Passwort:', + 'tournament:edit:btp:timezone': 'BTP-Zeitzone:', + 'tournament:edit:btp:system timezone': 'Systemeinstellung ({tz})', + 'tournament:edit:tabletoperator_enabled': 'Tabletbediener einsetzen', + 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Gewinner der Viertelfinals bedienen das Tablet', + 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', + 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', + 'tournament:edit:ticker_enabled': 'Ticker aktivieren', + 'tournament:edit:ticker_url': 'Ticker-Adresse:', + 'tournament:edit:ticker_password': 'Ticker-Passwort:', -'match:rawinfo': 'Technische Informationen', -'match:preparationcall': 'Aufruf: Spiel in Vorbereitung', -'match:begintoplay': 'Aufruf: Mit dem Spielen beginnen', -'match:secondcallteamone': 'Aufruf: Zweiter Aufruf Team 1', -'match:secondcallteamtwo': 'Aufruf: Zweiter Aufruf Team 2', -'match:secondcaltabletoperator': 'Aufruf: Zweiter Aufruf Tabletbediener', -'match:scoresheet': 'Schiedsrichterzettel', -'match:edit': 'Bearbeiten', -'match:incomplete': '[Unvollständig!] ', -'match:override_colors': 'Spezielle Farben', -'match:edit:scheduled_date': 'Datum:', -'match:edit:delete': 'Löschen', -'match:edit:now_on_court': 'Jetzt auf dem Feld', -'match:delete:really': 'Wirklich Spiel {match_id} löschen?', -'tournament:edit:logo': 'Logo', -'tournament:edit:logo:nologo': 'Kein Logo', -'tournament:edit:logo:upload': 'Logo hochladen', -'tournament:edit:logo:background': 'Logo-Hintergrund:', -'tournament:edit:logo:foreground': 'Farbe für Text auf Logo:', + 'to_stats:header': 'Statistik der Technischen Offiziellen', + 'to_stats:name': 'Name', + 'to_stats:umpire': 'SR', + 'to_stats:service_judge': 'AR', + 'to_stats:total': 'Total', -'nationstats': 'Nationen-Statistiken', -'nationstats:summary': '{player_count} Spieler aus {nation_count} verschiedenen Ländern', -'nationstats:summary:umpires': '{umpire_count} Schiedsrichter aus {nation_count} verschiedenen Ländern', + 'match:rawinfo': 'Technische Informationen', + 'match:preparationcall': 'Aufruf: Spiel in Vorbereitung', + 'match:begintoplay': 'Aufruf: Mit dem Spielen beginnen', + 'match:secondcallteamone': 'Aufruf: Zweiter Aufruf Team 1', + 'match:secondcallteamtwo': 'Aufruf: Zweiter Aufruf Team 2', + 'match:secondcaltabletoperator': 'Aufruf: Zweiter Aufruf Tabletbediener', + 'match:scoresheet': 'Schiedsrichterzettel', + 'match:edit': 'Bearbeiten', + 'match:incomplete': '[Unvollständig!] ', + 'match:override_colors': 'Spezielle Farben', + 'match:edit:scheduled_date': 'Datum:', + 'match:edit:delete': 'Löschen', + 'match:edit:now_on_court': 'Jetzt auf dem Feld', + 'match:delete:really': 'Wirklich Spiel {match_id} löschen?', + 'tournament:edit:logo': 'Logo', + 'tournament:edit:logo:nologo': 'Kein Logo', + 'tournament:edit:logo:upload': 'Logo hochladen', + 'tournament:edit:logo:background': 'Logo-Hintergrund:', + 'tournament:edit:logo:foreground': 'Farbe für Text auf Logo:', -'umpires:status:heading': 'Schiedsrichter', -'umpires:status:ready': 'Bereit', -'umpires:status:paused': 'In Pause', -'umpires:paused_since': 'seit {time}', -'umpires:btp_id': 'BTP-ID {btp_id}', -'umpires:last_on_court': 'letztes Spiel endete {time}', + 'nationstats': 'Nationen-Statistiken', + 'nationstats:summary': '{player_count} Spieler aus {nation_count} verschiedenen Ländern', + 'nationstats:summary:umpires': '{umpire_count} Schiedsrichter aus {nation_count} verschiedenen Ländern', -'tabletoperator:unassigned': 'Nächste TabletbedienerInnen', -'tabletoperator:name': 'Name', -'tabletoperator:add': 'Als Tabletoperator planen', -'tabletoperator:remove': 'Von Liste nehmen', -'csvexport:winners': 'CSV-Export für Siegerurkunden', + 'umpires:status:heading': 'Schiedsrichter', + 'umpires:status:ready': 'Bereit', + 'umpires:status:paused': 'In Pause', + 'umpires:paused_since': 'seit {time}', + 'umpires:btp_id': 'BTP-ID {btp_id}', + 'umpires:last_on_court': 'letztes Spiel endete {time}', + + 'tabletoperator:unassigned': 'Nächste TabletbedienerInnen', + 'tabletoperator:name': 'Name', + 'tabletoperator:add': 'Als Tabletoperator planen', + 'tabletoperator:remove': 'Von Liste nehmen', + 'csvexport:winners': 'CSV-Export für Siegerurkunden', }; /*@DEV*/ diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 82de34e..13fe0ab 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -1,124 +1,153 @@ var ci18n_en = { -'_code': 'en', -'_name': 'English', + '_code': 'en', + '_name': 'English', -'Unassigned Matches': 'Unassigned Matches', -'Next Matches': 'Next Matches', -'edit tournament': 'edit tournament', -'Court': 'Court', -'Match': 'Match', -'Players': 'Players', -'Umpire': 'Umpire', -'State': 'State', -'Umpire:': 'Umpire:', -'Service judge:': 'Service judge:', -'Finished Matches': 'Finished Matches', -'Time:': 'Time:', -'(Singles)': '(Singles)', -'e.g. MX O55': 'e.g. MX O55', -'e.g. semi-finals': 'e.g. semi-finals', -'Number:': 'Number:', -'Cancel': 'Cancel', -'Change': 'Change', -'No umpire': 'No umpire', -'No service judge': 'No service judge', -'Edit match': 'Edit match', -'Back': 'Back', -'PDF': 'PDF', -'Print': 'Print', -'Add Match': 'Add Match', -' Ready to start ': ' Ready to start ', -'Ready': 'Ready', -'Not assigned': 'Not assigned', -'team competition': 'team competition', -'nation competition': 'international competition', -'update from BTP': 'update from BTP', -'update ticker': 'update ticker', -'Tournaments': 'Tournaments', -'referee view': 'Referee view', -'Connecting ...': 'Connecting ...', -'Connected': 'Connected', -'Connection lost': 'Connection lost', -'Create tournament': 'Create tournament', -'create:id:label': 'tournament ID (all lowercase, no spaces):', -'experimental': '(experimental)', -'Winner': 'Winner', -'Loser': 'Loser', + 'Unassigned Matches': 'Unassigned Matches', + 'Next Matches': 'Next Matches', + 'edit tournament': 'edit tournament', + 'Court': 'Court', + 'Match': 'Match', + 'Players': 'Players', + 'Umpire': 'Umpire', + 'State': 'State', + 'Umpire:': 'Umpire:', + 'Service judge:': 'Service judge:', + 'Finished Matches': 'Finished Matches', + 'Time:': 'Time:', + '(Singles)': '(Singles)', + 'e.g. MX O55': 'e.g. MX O55', + 'e.g. semi-finals': 'e.g. semi-finals', + 'Number:': 'Number:', + 'Cancel': 'Cancel', + 'Change': 'Change', + 'No umpire': 'No umpire', + 'No service judge': 'No service judge', + 'Edit match': 'Edit match', + 'Back': 'Back', + 'PDF': 'PDF', + 'Print': 'Print', + 'Add Match': 'Add Match', + ' Ready to start ': ' Ready to start ', + 'Ready': 'Ready', + 'Not assigned': 'Not assigned', + 'team competition': 'team competition', + 'nation competition': 'international competition', + 'update from BTP': 'update from BTP', + 'update ticker': 'update ticker', + 'Tournaments': 'Tournaments', + 'referee view': 'Referee view', + 'Connecting ...': 'Connecting ...', + 'Connected': 'Connected', + 'Connection lost': 'Connection lost', + 'Create tournament': 'Create tournament', + 'create:id:label': 'tournament ID (all lowercase, no spaces):', + 'experimental': '(experimental)', + 'Winner': 'Winner', + 'Loser': 'Loser', -'tournament:edit:id': 'Tournament id:', -'tournament:edit:language': 'Language:', -'tournament:edit:language:auto': 'Not set (browser default)', -'tournament:edit:name': 'Name:', -'tournament:edit:courts': 'Courts:', -'tournament:edit:dm_style': 'Default display style:', -'tournament:edit:only_now_on_court': 'Matches have to be dragged onto court', -'tournament:edit:warmup_timer_behavior': 'Behaviour of the preparation countdown:', -'tournament:edit:warmup_timer_behavior:bwf-2016': 'BWF 2016+ (after choice of side)', -'tournament:edit:warmup_timer_behavior:legacy': 'legacy (after choice of side)', -'tournament:edit:warmup_timer_behavior:choise': 'after choice of side (individual time)', -'tournament:edit:warmup_timer_behavior:call-down': 'from call (countdown)', -'tournament:edit:warmup_timer_behavior:call-up': 'from call (timer)', -'tournament:edit:warmup_timer_behavior:none': 'none', -'tournament:edit:warmup_ready': 'Ready in seconds:', -'tournament:edit:warmup_start': 'Game starts after seconds:', -'tournament:edit:btp:enabled': 'Enable BTP synchronization', -'tournament:edit:btp:autofetch_enabled': 'Automatic synchronization', -'tournament:edit:btp:readonly': 'Read only', -'tournament:edit:btp:ip': 'IP address:', -'tournament:edit:btp:password': 'BTP password:', -'tournament:edit:btp:timezone': 'BTP timezone:', -'tournament:edit:btp:system timezone': 'System default ({tz})', + 'announcements:begin_to_play': 'Start to play!', + 'announcements:second_call': '"Second call for:"', + 'announcements:vs': ' vs ', + 'announcements:counting_board_service': 'Countingboard service:', + 'announcements:table_service': 'Tablet service:', + 'announcements:umpire': 'Umpire:', + 'announcements:and': ' and ', + 'announcements:preparation': 'In preparation:', + 'announcements:on_court': 'On Court ', + 'announcements:match_number': 'Match number ', + 'announcements:boys_singles': 'Boys singles', + 'announcements:boys_doubles': 'Boys double', + 'announcements:girls_singles': 'Girls singles', + 'announcements:girls_doubles': 'Girls doubles', + 'announcements:mixed_doubles': 'Mixed doubles', + 'announcements:men_singles': 'Men singles', + 'announcements:men_doubles': 'Men doubles', + 'announcements:women_singles': 'Women singles', + 'announcements:women_doubles': 'Women doubles', + 'announcements:round_16': 'Round of 16', + 'announcements:quaterfinal': 'Quarterfinal', + 'announcements:semifinal': 'Semifinal', + 'announcements:final': 'Final', + 'announcements:intermediate_round': 'Intermediate round', + 'announcements:game_for_place': 'Game for place ', + 'announcements:voice': 'Google UK English Male', + 'announcements:lang': 'en-EN', -'tournament:edit:ticker_enabled': 'Activate online ticker', -'tournament:edit:ticker_url': 'Ticker URL:', -'tournament:edit:ticker_password': 'Ticker password:', -'tournament:edit:tabletoperator_enabled': 'Use Tabletoperators', -'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Winner of Quarterfinals hav to do tabletoperatorservice', -'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', -'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', -'to_stats:header': 'Technical Officials Statistics', -'to_stats:name': 'Name', -'to_stats:umpire': 'U', -'to_stats:service_judge': 'SJ', -'to_stats:total': 'Total', -'match:rawinfo': 'Technical information', -'match:preparationcall': 'Announcement: Match in preparation', -'match:begintoplay': 'Announcement: Beginn to play', -'match:secondcallteamone': 'Announcement: Second call Team 1', -'match:secondcallteamtwo': 'Announcement: Second call Team 2', -'match:secondcaltabletoperator': 'Announcement: Second call Tabletoperator', -'match:scoresheet': 'Scoresheet', -'match:edit': 'Edit', -'match:override_colors': 'Override colors', -'match:incomplete': '[incomplete!] ', -'match:edit:scheduled_date': 'Date:', -'match:edit:delete': 'Delete match', -'match:edit:now_on_court': 'Now on court', -'match:delete:really': 'Really delete match {match_id}?', -'tournament:edit:logo': 'Logo', -'tournament:edit:logo:nologo': 'No logo', -'tournament:edit:logo:upload': 'Upload logo', -'tournament:edit:logo:background': 'Logo background:', -'tournament:edit:logo:foreground': 'Logo foreground:', + 'tournament:edit:id': 'Tournament id:', + 'tournament:edit:language': 'Language:', + 'tournament:edit:language:auto': 'Not set (browser default)', + 'tournament:edit:name': 'Name:', + 'tournament:edit:courts': 'Courts:', + 'tournament:edit:dm_style': 'Default display style:', + 'tournament:edit:only_now_on_court': 'Matches have to be dragged onto court', + 'tournament:edit:warmup_timer_behavior': 'Behaviour of the preparation countdown:', + 'tournament:edit:warmup_timer_behavior:bwf-2016': 'BWF 2016+ (after choice of side)', + 'tournament:edit:warmup_timer_behavior:legacy': 'legacy (after choice of side)', + 'tournament:edit:warmup_timer_behavior:choise': 'after choice of side (individual time)', + 'tournament:edit:warmup_timer_behavior:call-down': 'from call (countdown)', + 'tournament:edit:warmup_timer_behavior:call-up': 'from call (timer)', + 'tournament:edit:warmup_timer_behavior:none': 'none', + 'tournament:edit:warmup_ready': 'Ready in seconds:', + 'tournament:edit:warmup_start': 'Game starts after seconds:', + 'tournament:edit:btp:enabled': 'Enable BTP synchronization', + 'tournament:edit:btp:autofetch_enabled': 'Automatic synchronization', + 'tournament:edit:btp:readonly': 'Read only', + 'tournament:edit:btp:ip': 'IP address:', + 'tournament:edit:btp:password': 'BTP password:', + 'tournament:edit:btp:timezone': 'BTP timezone:', + 'tournament:edit:btp:system timezone': 'System default ({tz})', -'nationstats': 'Nation stats', -'nationstats:summary': '{player_count} players from {nation_count} nations', -'nationstats:summary:umpires': '{umpire_count} umpires from {nation_count} nations', + 'tournament:edit:ticker_enabled': 'Activate online ticker', + 'tournament:edit:ticker_url': 'Ticker URL:', + 'tournament:edit:ticker_password': 'Ticker password:', + 'tournament:edit:tabletoperator_enabled': 'Use Tabletoperators', + 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Winner of Quarterfinals hav to do tabletoperatorservice', + 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', + 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', + 'to_stats:header': 'Technical Officials Statistics', + 'to_stats:name': 'Name', + 'to_stats:umpire': 'U', + 'to_stats:service_judge': 'SJ', + 'to_stats:total': 'Total', -'umpires:status:heading': 'Umpires', -'umpires:status:ready': 'Ready', -'umpires:status:paused': 'Reserve', -'umpires:paused_since': 'since {time}', -'umpires:btp_id': 'BTP ID {btp_id}', -'umpires:last_on_court': 'previous match ended at {time}', + 'match:rawinfo': 'Technical information', + 'match:preparationcall': 'Announcement: Match in preparation', + 'match:begintoplay': 'Announcement: Beginn to play', + 'match:secondcallteamone': 'Announcement: Second call Team 1', + 'match:secondcallteamtwo': 'Announcement: Second call Team 2', + 'match:secondcaltabletoperator': 'Announcement: Second call Tabletoperator', + 'match:scoresheet': 'Scoresheet', + 'match:edit': 'Edit', + 'match:override_colors': 'Override colors', + 'match:incomplete': '[incomplete!] ', + 'match:edit:scheduled_date': 'Date:', + 'match:edit:delete': 'Delete match', + 'match:edit:now_on_court': 'Now on court', + 'match:delete:really': 'Really delete match {match_id}?', + 'tournament:edit:logo': 'Logo', + 'tournament:edit:logo:nologo': 'No logo', + 'tournament:edit:logo:upload': 'Upload logo', + 'tournament:edit:logo:background': 'Logo background:', + 'tournament:edit:logo:foreground': 'Logo foreground:', -'tabletoperator:unassigned': 'Next Tabletoperators', -'tabletoperator:name': 'Name', -'tabletoperator:add': 'Schedule as Tabletoperator', -'tabletoperator:remove': 'Remove from list', -'csvexport:winners': 'CSV export (winner\'s certificates)', + 'nationstats': 'Nation stats', + 'nationstats:summary': '{player_count} players from {nation_count} nations', + 'nationstats:summary:umpires': '{umpire_count} umpires from {nation_count} nations', + + 'umpires:status:heading': 'Umpires', + 'umpires:status:ready': 'Ready', + 'umpires:status:paused': 'Reserve', + 'umpires:paused_since': 'since {time}', + 'umpires:btp_id': 'BTP ID {btp_id}', + 'umpires:last_on_court': 'previous match ended at {time}', + + 'tabletoperator:unassigned': 'Next Tabletoperators', + 'tabletoperator:name': 'Name', + 'tabletoperator:add': 'Schedule as Tabletoperator', + 'tabletoperator:remove': 'Remove from list', + 'csvexport:winners': 'CSV export (winner\'s certificates)', }; /*@DEV*/ From 352fbecd73f568f49696c17cf69d6a32da969ee5 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 13 Apr 2024 19:53:25 +0200 Subject: [PATCH 060/224] New: Function it is now possible to normalize the name of the players and umpieres deopenden of the choosen language. The configuration si managed via database. --- bts/admin.js | 5 +++++ bts/database.js | 2 ++ bts/stournament.js | 8 ++++++++ static/js/announcements.js | 18 +++++++++++++++--- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 19ed6ea..46bf8f0 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -151,6 +151,11 @@ function handle_tournament_get(app, ws, msg) { tournament.displays = displays; cb(err); }); + }, function (cb) { + stournament.get_normalizations(app.db, tournament.key, function (err, normalizations) { + tournament.normalizations = normalizations; + cb(err); + }); }], function(err) { tournament.btp_status = btp_manager.get_status(tournament.key); tournament.ticker_status = ticker_manager.get_status(tournament.key); diff --git a/bts/database.js b/bts/database.js index 92d28d7..62927c1 100644 --- a/bts/database.js +++ b/bts/database.js @@ -19,6 +19,8 @@ const TABLES = [ 'tabletoperators', 'displaysettings', 'display_court_displaysettings', + 'normalizations' + ]; diff --git a/bts/stournament.js b/bts/stournament.js index c90680d..5eec575 100644 --- a/bts/stournament.js +++ b/bts/stournament.js @@ -48,6 +48,13 @@ function get_displays(db, tournament_key, callback) { }); } +function get_normalizations(db, tournament_key, callback) { + db.normalizations.find({}, function (err, normalizations) { + if (err) return callback(err); + return callback(err, normalizations); + }); +} + module.exports = { get_courts, @@ -55,4 +62,5 @@ module.exports = { get_umpires, get_tabletoperators, get_displays, + get_normalizations, }; diff --git a/static/js/announcements.js b/static/js/announcements.js index 8e63bea..477ee47 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -89,19 +89,31 @@ function createTabletOperator(matchSetup) { function createUmpire(matchSetup) { if (matchSetup.umpire_name && matchSetup.umpire_name != null) { - return ci18n('announcements:umpire') + matchSetup.umpire_name; + return ci18n('announcements:umpire') + normalizeNames(matchSetup.umpire_name); } return null; } function createSingleTeam(playersSetup) { - var team = playersSetup[0].name; + var team = normalizeNames(playersSetup[0].name); if (playersSetup.length == 2) { - team = team + ci18n('announcements:and')+ playersSetup[1].name + team = team + ci18n('announcements:and') + normalizeNames(playersSetup[1].name) } return team; } + +function normalizeNames(name) { + if (curt.normalizations && curt.normalizations.length > 0) { + for (const norm of curt.normalizations) { + if (ci18n('announcements:lang') == norm.language) { + name = name.replaceAll(norm.origin, norm.replace); + } + } + } + return name; +} + function createRoundAnnouncement(matchSetup) { var round = matchSetup.match_name; if (round == "R16") { From 3a99a4f9aface01af3d90efe647cec0f360e242d Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sun, 14 Apr 2024 02:32:49 +0200 Subject: [PATCH 061/224] Fix error the players of a match are unknown in btp, but they was added before to the db (delete draw) --- bts/btp_sync.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 574eb34..4f44545 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -96,6 +96,7 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, }; app.db.tournaments.findOne({key: tkey}, (err, tournament) => { + if(err) { console.log("reject"); reject(err); @@ -185,7 +186,6 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, match.match_order = bm.DisplayOrder[0]; } match._id = 'btp_' + btp_id; - resolve(match); }); }); @@ -356,8 +356,8 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { match.setup.tabletoperators = cur_match.setup.tabletoperators; } - for(let team_index = 0; team_index < cur_match.setup.teams.length; team_index++) { - for (let player_index = 0; player_index < cur_match.setup.teams[team_index].players.length; player_index++){ + for(let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { + for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++){ if (cur_match.setup.teams[team_index].players[player_index].now_playing_on_court != undefined) { match.setup.teams[team_index].players[player_index].now_playing_on_court = cur_match.setup.teams[team_index].players[player_index].now_playing_on_court; @@ -416,8 +416,8 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { let only_change_check_in = false; let result_enterd_in_btp = false; - for(let team_index = 0; team_index < cur_match.setup.teams.length; team_index++) { - for (let player_index = 0; player_index < cur_match.setup.teams[team_index].players.length; player_index++){ + for(let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { + for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++){ cur_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; } } From 92ec45e452db4b7d5edb646221be440a97e14452 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sun, 14 Apr 2024 02:33:48 +0200 Subject: [PATCH 062/224] fix error message in admin screen --- static/js/ctournament.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 6df278a..cc116fa 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -190,8 +190,9 @@ function update_upcoming_match(c){ console.log(new_section); if(old_section != new_section || new_section == 'unassigned') { - const upcoming_container = uiu.qs('.upcoming_container'); - cmatch.render_upcoming_matches(upcoming_container); + uiu.qsEach('.upcoming_container', (upcoming_container) => { + cmatch.render_upcoming_matches(upcoming_container); + }); } } From 8fe1084a82f6e1039421396eefee67f34d7765e2 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sun, 14 Apr 2024 02:34:46 +0200 Subject: [PATCH 063/224] remove warning (for debugging) if a row of a view is rerendered --- static/js/cmatch.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 8274a7a..7746dfa 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -42,8 +42,6 @@ function render_match_table_header(table, include_courts) { } function render_match_row(tr, match, court, style, show_player_status, show_add_tabletoperator) { - console.warn("rerender_row"); - if(!match.setup.is_match) { return; } From fe37068350efdfef0f80bc7ee9390c5da6147708 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sun, 14 Apr 2024 02:35:26 +0200 Subject: [PATCH 064/224] show the court where the tabletoperator (in unassigned list) played last --- bts/admin.js | 3 ++- bts/http_api.js | 4 +++- static/js/ctabletoperator.js | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 46bf8f0..f813045 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -289,7 +289,8 @@ function handle_tabletoperator_add(app, ws, msg) { 'match_id': 'manually_added', 'start_ts': Date.now(), 'end_ts': null, - 'court': null + 'court': null, + 'played_on_court': null }; app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_tabletoperator) { if (err) { diff --git a/bts/http_api.js b/bts/http_api.js index 05d3bad..ab9e218 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -424,8 +424,10 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ 'match_id': cur_match._id, 'start_ts': end_ts, 'end_ts': null, - 'court': null + 'court': null, + 'played_on_court': (cur_match.setup.court_id ? cur_match.setup.court_id : null) }; + app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { if (err) { ws.respond(msg, err); diff --git a/static/js/ctabletoperator.js b/static/js/ctabletoperator.js index 5e79453..8b0c253 100644 --- a/static/js/ctabletoperator.js +++ b/static/js/ctabletoperator.js @@ -36,6 +36,10 @@ function render_tabletoperator_row(tr, tabletoperator) { uiu.el(to_td, 'span', 'match_no_umpire', ' \u200B/ '); uiu.el(to_td, 'span', 'match_no_umpire', to[1].name); } + + const court = curt.courts_by_id[tabletoperator.played_on_court]; + uiu.el(tr, 'td', court ? 'court_history' : '', court ? court.num : ''); + if (tabletoperator.court == null) { const buttonbar = uiu.el(tr, 'td'); create_tabletoperator_button(buttonbar, 'vlink tabletoperator_remove_button', 'tabletoperator:remove', on_remove_from_list_button_click, tabletoperator._id); From 03ba9ca70faf3adfba64e5423f5061a23afae9d7 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Tue, 16 Apr 2024 21:00:29 +0200 Subject: [PATCH 065/224] New Function: Add a section in "Edit Tournament"-View for Managing the Displays which are responsible to visualize the Scores of the different Courts. First you can see if the Display ist "online". Also there is the possibility to restart the different displays --- bts/admin.js | 12 ++++++- bts/bupws.js | 77 +++++++++++++++++++++++++++++++++------- bts/stournament.js | 6 ++-- static/js/ci18n_de.js | 5 +++ static/js/ci18n_en.js | 5 +++ static/js/ctournament.js | 39 ++++++++++++++++++++ 6 files changed, 128 insertions(+), 16 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index f813045..25774ac 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -147,7 +147,7 @@ function handle_tournament_get(app, ws, msg) { cb(err); }); }, function (cb) { - stournament.get_displays(app.db, tournament.key, function (err, displays) { + stournament.get_displays(app, tournament.key, function (err, displays) { tournament.displays = displays; cb(err); }); @@ -420,6 +420,15 @@ function handle_second_call_team_one(app, ws, msg) { ws.respond(msg); } +function handle_reset_display(app, ws, msg) { + const tournament_key = msg.tournament_key; + const client_id = msg.display_setting_id; + const bupws = require('./bupws'); + bupws.restart_panel(app, tournament_key, client_id); + ws.respond("Angekommen: " + client_id); +} + + function handle_second_call_team_two(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { return; @@ -596,6 +605,7 @@ module.exports = { handle_tournament_get, handle_tournament_list, handle_tournament_edit_props, + handle_reset_display, notify_change, on_close, on_connect, diff --git a/bts/bupws.js b/bts/bupws.js index 6f8e67e..23ad50a 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -99,24 +99,30 @@ async function handle_init(app, ws, msg) { court_id = undefined; } if (msg.initialize_display) { - const client_id = determine_client_id(ws); - if (client_id) { - let display_setting = await get_display_setting(app, tournament_key, client_id, court_id) - if (display_setting != null) { - ws.court_id = display_setting.court_id; - court_id = display_setting.court_id; - notify_change_ws(ws, tournament_key, court_id, "settings-update", display_setting); - } - } + initialize_client(ws, app, tournament_key, court_id); } matches_handler(app, ws, tournament_key, court_id); } -function determine_client_id(ws) { - const remote_adress_seqments = ws._socket.remoteAddress.split('.'); - return remote_adress_seqments[remote_adress_seqments.length - 1]; +async function initialize_client(ws, app, tournament_key, court_id) { + const client_id = determine_client_id(ws); + if (client_id) { + let display_setting = await get_display_setting(app, tournament_key, client_id, court_id) + if (display_setting != null) { + ws.court_id = display_setting.court_id; + court_id = display_setting.court_id; + notify_change_ws(ws, tournament_key, court_id, "settings-update", display_setting); + } + } } +function determine_client_id(ws) { + if (!ws.client_id) { + const remote_adress_seqments = ws._socket.remoteAddress.split('.'); + ws.client_id = remote_adress_seqments[remote_adress_seqments.length - 1]; + } + return ws.client_id; +} function persist_client_court_displaysetting(app, client_court_displaysetting) { return new Promise((resolve, reject) => { @@ -155,7 +161,7 @@ function persist_displaysetting(app, setting) { } -function get_display_court_displaysettings(app, tkey, client_id) { +function client_id(app, tkey, client_id) { return new Promise((resolve, reject) => { const display_court_query = { 'client_id': client_id }; app.db.display_court_displaysettings.find(display_court_query).limit(1).exec((err, display_court_displaysetting) => { @@ -362,6 +368,49 @@ function create_event_representation(tournament) { return res; } +function restart_panel(app, tournament_key, client_id) { + for (const panel_ws of all_panels) { + + const ws_client_id = determine_client_id(panel_ws); + if (client_id == ws_client_id) { + initialize_client(panel_ws, app, tournament_key, panel_ws.court_id); + // Two time to make it work, no idea why + matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); + matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); + } + + } +} +function add_display_status(app, tournament_key, displays) { + for (const d of displays) { + d.online = false; + for (const panel_ws of all_panels) { + const ws_client_id = determine_client_id(panel_ws); + if (d.client_id == ws_client_id) { + d.online = true; + } + } + } + for (const panel_ws of all_panels) { + var found = false; + const ws_client_id = determine_client_id(panel_ws); + for (const d of displays) { + + if (d.client_id == ws_client_id) { + found = true; + } + } + if (!found) { + client_court_displaysetting = { + client_id: ws_client_id, + court_id: panel_ws.court_id, + displaysetting_id: "default", + online: true + } + displays[displays.length] = client_court_displaysetting; + } + } +} module.exports = { on_close, @@ -371,4 +420,6 @@ module.exports = { handle_score_change, handle_persist_display_settings, handle_reset_display_settings, + restart_panel, + add_display_status, }; \ No newline at end of file diff --git a/bts/stournament.js b/bts/stournament.js index 5eec575..1de559e 100644 --- a/bts/stournament.js +++ b/bts/stournament.js @@ -38,12 +38,14 @@ function get_tabletoperators(db, tournament_key, callback) { }); } -function get_displays(db, tournament_key, callback) { - db.display_court_displaysettings.find({}, function (err, display_court_displaysettings) { +function get_displays(app, tournament_key, callback) { + app.db.display_court_displaysettings.find({}, function (err, display_court_displaysettings) { if (err) return callback(err); // TODO: Append not registered Displays and set status online/offline of registered displays by using ite registered ws in bubws + const bupws = require('./bupws'); + bupws.add_display_status(app, tournament_key, display_court_displaysettings); return callback(err, display_court_displaysettings); }); } diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 4bc4078..a18fdce 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -105,6 +105,11 @@ var ci18n_de = { 'tournament:edit:ticker_enabled': 'Ticker aktivieren', 'tournament:edit:ticker_url': 'Ticker-Adresse:', 'tournament:edit:ticker_password': 'Ticker-Passwort:', + 'tournament:edit:displays': 'Anzeigen administrieren:', + 'tournament:edit:displays:num': 'Monitor', + 'tournament:edit:displays:court': 'Feld', + 'tournament:edit:displays:setting': 'Einstellung', + 'tournament:edit:displays:onlinestatus': 'Status', 'to_stats:header': 'Statistik der Technischen Offiziellen', 'to_stats:name': 'Name', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 13fe0ab..36a7d14 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -106,6 +106,11 @@ var ci18n_en = { 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Winner of Quarterfinals hav to do tabletoperatorservice', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', + 'tournament:edit:displays': 'Manage Displays:', + 'tournament:edit:displays:num': 'Displaynumber', + 'tournament:edit:displays:court': 'Ar Court', + 'tournament:edit:displays:setting': 'Setting', + 'tournament:edit:displays:onlinestatus': 'Status', 'to_stats:header': 'Technical Officials Statistics', 'to_stats:name': 'Name', 'to_stats:umpire': 'U', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index cc116fa..67bffe9 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -903,6 +903,45 @@ function ui_edit() { ui_edit(); }); }); + + uiu.el(main, 'h2', {}, ci18n('tournament:edit:displays')); + + const display_table = uiu.el(main, 'table'); + const display_tbody = uiu.el(display_table, 'tbody'); + const tr = uiu.el(display_tbody, 'tr'); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:num')); + uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:court')); + uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:setting')); + uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:onlinestatus')); + + for (const c of curt.displays) { + const tr = uiu.el(display_tbody, 'tr'); + uiu.el(tr, 'th', {}, c.client_id); + uiu.el(tr, 'td', {}, c.court_id || ''); + uiu.el(tr, 'td', {}, c.displaysetting_id || ''); + uiu.el(tr, 'td', {}, (!c.online) ? 'offline' : 'online'); + const actions_td = uiu.el(tr, 'td', {}); + const reset_btn = uiu.el(actions_td, 'button', { + 'data-display-setting-id': c.client_id, + }, 'Restart'); + + if (!c.online) { + reset_btn.setAttribute('disabled', 'disabled'); + } + reset_btn.addEventListener('click', function (e) { + const del_btn = e.target; + const display_setting_id = del_btn.getAttribute('data-display-setting-id'); + send({ + type: 'reset_display', + tournament_key: curt.key, + display_setting_id: display_setting_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + } } _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit); From da608fc923f5321d1d513de7cd9fae43bb754ff0 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Tue, 16 Apr 2024 23:32:12 +0200 Subject: [PATCH 066/224] New Function: Now it ist possible to Switch the courts on the displays using the adminpanel --- bts/admin.js | 13 +++++++++++++ bts/bupws.js | 35 +++++++++++++++++++++++++++++------ bts/stournament.js | 4 ++++ static/js/ctournament.js | 38 +++++++++++++++++++++++++++++++++++++- 4 files changed, 83 insertions(+), 7 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 25774ac..7edf072 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -428,6 +428,18 @@ function handle_reset_display(app, ws, msg) { ws.respond("Angekommen: " + client_id); } +function handle_relocate_display(app, ws, msg) { + const tournament_key = msg.tournament_key; + const client_id = msg.display_setting_id; + const new_court_id = msg.new_court_id; + const bupws = require('./bupws'); + bupws.restart_panel(app, tournament_key, client_id, new_court_id); + ws.respond("Angekommen: " + client_id); +} + + + + function handle_second_call_team_two(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { @@ -606,6 +618,7 @@ module.exports = { handle_tournament_list, handle_tournament_edit_props, handle_reset_display, + handle_relocate_display, notify_change, on_close, on_connect, diff --git a/bts/bupws.js b/bts/bupws.js index 23ad50a..f7c875b 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -56,7 +56,7 @@ async function handle_reset_display_settings(app, ws, msg) { const updatevalues = { client_id: 'deleted' } - client_court_displaysetting = await update_client_court_displaysetting(app, client_court_displaysetting, updatevalues); + client_court_displaysetting = await update_client_court_displaysetting(app, client_court_displaysetting.client_id, updatevalues); } } @@ -85,7 +85,7 @@ async function handle_persist_display_settings(app, ws, msg) { court_id: court_id, displaysetting_id: setting.id, } - client_court_displaysetting = await update_client_court_displaysetting(app, client_court_displaysetting, updatevalues); + client_court_displaysetting = await update_client_court_displaysetting(app, client_court_displaysetting.client_id, updatevalues); } } @@ -135,9 +135,9 @@ function persist_client_court_displaysetting(app, client_court_displaysetting) { }); } -function update_client_court_displaysetting(app, client_court_displaysetting, updatevalues) { +function update_client_court_displaysetting(app, client_court_displaysetting_id, updatevalues) { return new Promise((resolve, reject) => { - app.db.display_court_displaysettings.update({ _id: client_court_displaysetting._id }, { $set: updatevalues }, { returnUpdatedDocs: true }, function (err, numAffected, changed_objects) { + app.db.display_court_displaysettings.update({ client_id: client_court_displaysetting_id }, { $set: updatevalues }, { returnUpdatedDocs: true }, function (err, numAffected, changed_objects) { if (err) { reject(err) } @@ -177,6 +177,20 @@ function client_id(app, tkey, client_id) { }); } +function get_display_court_displaysettings(app, tkey, client_id) { + return new Promise((resolve, reject) => { + const display_court_query = { 'client_id': client_id }; + app.db.display_court_displaysettings.find(display_court_query).limit(1).exec((err, display_court_displaysetting) => { + if (err) { + return reject(err); + } + if (display_court_displaysetting.length == 1) { + resolve(display_court_displaysetting[0]); + } + resolve(null); + }); + }); +} function get_display_setting(app, tkey, client_id, court_id) { return new Promise((resolve, reject) => { const display_court_query = { 'client_id': client_id }; @@ -368,10 +382,19 @@ function create_event_representation(tournament) { return res; } -function restart_panel(app, tournament_key, client_id) { +async function restart_panel(app, tournament_key, client_id, new_court_id) { for (const panel_ws of all_panels) { const ws_client_id = determine_client_id(panel_ws); + if (new_court_id) { + panel_ws.court_id = new_court_id; + + const updatevalues = { + court_id: new_court_id + } + const client_court_displaysetting = await update_client_court_displaysetting(app, client_id, updatevalues); + + } if (client_id == ws_client_id) { initialize_client(panel_ws, app, tournament_key, panel_ws.court_id); // Two time to make it work, no idea why @@ -401,7 +424,7 @@ function add_display_status(app, tournament_key, displays) { } } if (!found) { - client_court_displaysetting = { + const client_court_displaysetting = { client_id: ws_client_id, court_id: panel_ws.court_id, displaysetting_id: "default", diff --git a/bts/stournament.js b/bts/stournament.js index 1de559e..0937503 100644 --- a/bts/stournament.js +++ b/bts/stournament.js @@ -43,9 +43,13 @@ function get_displays(app, tournament_key, callback) { if (err) return callback(err); // TODO: Append not registered Displays and set status online/offline of registered displays by using ite registered ws in bubws + display_court_displaysettings = display_court_displaysettings.filter(function (obj) { + return obj.client_id !== 'deleted'; + }); const bupws = require('./bupws'); bupws.add_display_status(app, tournament_key, display_court_displaysettings); + display_court_displaysettings = display_court_displaysettings.sort(utils.cmp_key('client_id')); return callback(err, display_court_displaysettings); }); } diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 67bffe9..56e9982 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -917,7 +917,7 @@ function ui_edit() { for (const c of curt.displays) { const tr = uiu.el(display_tbody, 'tr'); uiu.el(tr, 'th', {}, c.client_id); - uiu.el(tr, 'td', {}, c.court_id || ''); + createCourtSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.court_id); uiu.el(tr, 'td', {}, c.displaysetting_id || ''); uiu.el(tr, 'td', {}, (!c.online) ? 'offline' : 'online'); const actions_td = uiu.el(tr, 'td', {}); @@ -959,6 +959,42 @@ function create_checkbox(curt, parent_el, filed_id) { uiu.el(label, 'span', {}, ci18n('tournament:edit:' + filed_id)); } + +function createCourtSelectBox(parentEl,parent_id, court_id) { + const court_select_box = uiu.el(parentEl, 'select', { + name: 'court_'+ parent_id, + }); + + for (const court of curt.courts) { + const attrs = { + 'data-display-setting-id': court_id, + value: court._id, + } + + if ((court_id === court._id)) { + attrs.selected = 'selected'; + } + + uiu.el(court_select_box, 'option', attrs, court.name); + } + + court_select_box.addEventListener('change', (e) => { + const select_box = e.target; + const display_setting_id = select_box.name.split("_")[1]; + send({ + type: 'relocate_display', + tournament_key: curt.key, + new_court_id: e.srcElement.value, + display_setting_id: display_setting_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); +} + + function render_upcoming(container) { cmatch.prepare_render(curt); const courts_container = uiu.el(container, 'div', 'courts_container'); From d86f12746b5a99f5c236def9dc6ebd43686877b4 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Wed, 17 Apr 2024 20:35:01 +0200 Subject: [PATCH 067/224] Bugfix: Court.Name ist not always initialized --- static/js/ctournament.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 56e9982..876e0e5 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -975,7 +975,7 @@ function createCourtSelectBox(parentEl,parent_id, court_id) { attrs.selected = 'selected'; } - uiu.el(court_select_box, 'option', attrs, court.name); + uiu.el(court_select_box, 'option', attrs, court.num); } court_select_box.addEventListener('change', (e) => { From e84b3ad07c8d97b4e769c188cc294bc4191d34ab Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Wed, 17 Apr 2024 22:27:18 +0200 Subject: [PATCH 068/224] New: Function: Now it is possible to switch the mode of on display centralized using the adminpanel --- bts/admin.js | 28 ++++++++++++++++++++++------ bts/bupws.js | 39 +++++++++++++++++++++++++++++++-------- bts/stournament.js | 8 ++++++++ static/js/ctournament.js | 36 +++++++++++++++++++++++++++++++++++- 4 files changed, 96 insertions(+), 15 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 7edf072..d1039d1 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -147,15 +147,20 @@ function handle_tournament_get(app, ws, msg) { cb(err); }); }, function (cb) { - stournament.get_displays(app, tournament.key, function (err, displays) { - tournament.displays = displays; - cb(err); - }); + stournament.get_displays(app, tournament.key, function (err, displays) { + tournament.displays = displays; + cb(err); + }); }, function (cb) { - stournament.get_normalizations(app.db, tournament.key, function (err, normalizations) { - tournament.normalizations = normalizations; + stournament.get_normalizations(app.db, tournament.key, function (err, normalizations) { + tournament.normalizations = normalizations; cb(err); }); + }, function (cb) { + stournament.get_displaysettings(app.db, tournament.key, function (err, displaysettings) { + tournament.displaysettings = displaysettings; + cb(err); + }); }], function(err) { tournament.btp_status = btp_manager.get_status(tournament.key); tournament.ticker_status = ticker_manager.get_status(tournament.key); @@ -436,6 +441,16 @@ function handle_relocate_display(app, ws, msg) { bupws.restart_panel(app, tournament_key, client_id, new_court_id); ws.respond("Angekommen: " + client_id); } +function handle_change_display_mode(app, ws, msg) { + const tournament_key = msg.tournament_key; + const client_id = msg.display_setting_id; + const new_displaysettings_id = msg.new_displaysettings_id; + const bupws = require('./bupws'); + bupws.change_display_mode(app, tournament_key, client_id, new_displaysettings_id); + ws.respond("Angekommen: " + client_id); +} + + @@ -619,6 +634,7 @@ module.exports = { handle_tournament_edit_props, handle_reset_display, handle_relocate_display, + handle_change_display_mode, notify_change, on_close, on_connect, diff --git a/bts/bupws.js b/bts/bupws.js index f7c875b..0672c01 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -386,16 +386,17 @@ async function restart_panel(app, tournament_key, client_id, new_court_id) { for (const panel_ws of all_panels) { const ws_client_id = determine_client_id(panel_ws); - if (new_court_id) { - panel_ws.court_id = new_court_id; + if (client_id == ws_client_id) { + if (new_court_id) { + panel_ws.court_id = new_court_id; - const updatevalues = { - court_id: new_court_id - } - const client_court_displaysetting = await update_client_court_displaysetting(app, client_id, updatevalues); + const updatevalues = { + court_id: new_court_id + } + const client_court_displaysetting = await update_client_court_displaysetting(app, client_id, updatevalues); - } - if (client_id == ws_client_id) { + } + initialize_client(panel_ws, app, tournament_key, panel_ws.court_id); // Two time to make it work, no idea why matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); @@ -404,6 +405,27 @@ async function restart_panel(app, tournament_key, client_id, new_court_id) { } } + +async function change_display_mode(app, tournament_key, client_id, new_displaysettings_id) { + if (new_displaysettings_id) { + for (const panel_ws of all_panels) { + + const ws_client_id = determine_client_id(panel_ws); + if (client_id == ws_client_id) { + const updatevalues = { + displaysetting_id: new_displaysettings_id + } + const client_court_displaysetting = await update_client_court_displaysetting(app, client_id, updatevalues); + + initialize_client(panel_ws, app, tournament_key, panel_ws.court_id); + // Two time to make it work, no idea why + matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); + matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); + } + } + } +} + function add_display_status(app, tournament_key, displays) { for (const d of displays) { d.online = false; @@ -444,5 +466,6 @@ module.exports = { handle_persist_display_settings, handle_reset_display_settings, restart_panel, + change_display_mode, add_display_status, }; \ No newline at end of file diff --git a/bts/stournament.js b/bts/stournament.js index 0937503..ef8a079 100644 --- a/bts/stournament.js +++ b/bts/stournament.js @@ -61,6 +61,13 @@ function get_normalizations(db, tournament_key, callback) { }); } +function get_displaysettings(db, tournament_key, callback) { + db.displaysettings.find({}, function (err, displaysettings) { + if (err) return callback(err); + return callback(err, displaysettings); + }); +} + module.exports = { get_courts, @@ -69,4 +76,5 @@ module.exports = { get_tabletoperators, get_displays, get_normalizations, + get_displaysettings, }; diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 876e0e5..6fdda95 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -918,7 +918,7 @@ function ui_edit() { const tr = uiu.el(display_tbody, 'tr'); uiu.el(tr, 'th', {}, c.client_id); createCourtSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.court_id); - uiu.el(tr, 'td', {}, c.displaysetting_id || ''); + createDisplaySettingsSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.displaysetting_id); uiu.el(tr, 'td', {}, (!c.online) ? 'offline' : 'online'); const actions_td = uiu.el(tr, 'td', {}); const reset_btn = uiu.el(actions_td, 'button', { @@ -994,6 +994,40 @@ function createCourtSelectBox(parentEl,parent_id, court_id) { }); } +function createDisplaySettingsSelectBox(parentEl, parent_id, displaysettings_id) { + const displaysettings_select_box = uiu.el(parentEl, 'select', { + name: 'displaysettings_' + parent_id, + }); + + for (const ds of curt.displaysettings) { + const attrs = { + 'data-display-setting-id': displaysettings_id, + value: ds.id, + } + + if ((displaysettings_id === ds.id)) { + attrs.selected = 'selected'; + } + + uiu.el(displaysettings_select_box, 'option', attrs, ds.id); + } + + displaysettings_select_box.addEventListener('change', (e) => { + const select_box = e.target; + const display_setting_id = select_box.name.split("_")[1]; + send({ + type: 'change_display_mode', + tournament_key: curt.key, + new_displaysettings_id: e.srcElement.value, + display_setting_id: display_setting_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); +} + function render_upcoming(container) { cmatch.prepare_render(curt); From b6e5a0541eede854b538918b789ae4083086f40b Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Fri, 19 Apr 2024 20:12:08 +0200 Subject: [PATCH 069/224] Enhancement: Displays are now registered and unregistered at Adminpanel if they are available oder unavailable. Also all changes mad to the displays on adminpanel will be resend to all other adminpanles including itself --- bts/bupws.js | 112 ++++++++++++++++++++++++------------------- static/css/admin.css | 4 +- static/js/change.js | 18 +++++++ 3 files changed, 84 insertions(+), 50 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index 0672c01..b360b1d 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -3,6 +3,7 @@ const { forEach } = require('async'); const serror = require('./serror'); const utils = require('./utils'); +const admin = require('./admin'); const all_panels = []; @@ -10,10 +11,22 @@ function on_close(app, ws) { if (!utils.remove(all_panels, ws)) { serror.silent('Removing Scoreboard ws, but it was not connected!?'); } + notify_admin_display_status_changed(app, ws, false); } -function on_connect(app, ws) { +async function on_connect(app, ws) { all_panels.push(ws); + notify_admin_display_status_changed(app, ws, true); +} + +async function notify_admin_display_status_changed(app, ws, ws_online) { + const client_id = determine_client_id(ws); + var display_court_displaysetting = await get_display_court_displaysettings(app, client_id); + if (display_court_displaysetting == null) { + display_court_displaysetting = create_display_court_displaysettings(client_id, null, "default"); + } + display_court_displaysetting.online = ws_online; + admin.notify_change(app, 'default', 'display_status_changed', { 'display_court_displaysetting': display_court_displaysetting }); } function notify_change(tournament_key, court_id, ctype, val) { @@ -51,13 +64,12 @@ async function handle_reset_display_settings(app, ws, msg) { var setting = msg.panel_settings; const client_id = determine_client_id(ws); - var client_court_displaysetting = await get_display_court_displaysettings(app, tournament_key, client_id); + var client_court_displaysetting = await get_display_court_displaysettings(app, client_id); if (client_court_displaysetting != null) { const updatevalues = { client_id: 'deleted' } client_court_displaysetting = await update_client_court_displaysetting(app, client_court_displaysetting.client_id, updatevalues); - } } @@ -67,16 +79,11 @@ async function handle_persist_display_settings(app, ws, msg) { var setting = msg.panel_settings; const client_id = determine_client_id(ws); - var client_court_displaysetting = await get_display_court_displaysettings(app, tournament_key, client_id); + var client_court_displaysetting = await get_display_court_displaysettings(app, client_id); if (client_court_displaysetting == null) { setting.id = tournament_key + "_" + court_id + " _" + Date.now(); setting = await persist_displaysetting(app, setting); - - client_court_displaysetting = { - client_id: client_id, - court_id: court_id, - displaysetting_id: setting.id, - } + client_court_displaysetting = create_display_court_displaysettings(client_id, court_id, setting.id); client_court_displaysetting = await persist_client_court_displaysetting(app, client_court_displaysetting); } else { setting.id = tournament_key + "_" + court_id + " _" + Date.now(); @@ -89,6 +96,14 @@ async function handle_persist_display_settings(app, ws, msg) { } } +function create_display_court_displaysettings(client_id, court_id, displaysetting_id) { + return { + client_id: client_id, + court_id: court_id, + displaysetting_id: displaysetting_id, + } +} + async function handle_init(app, ws, msg) { const tournament_key = msg.tournament_key; var court_id = msg.panel_settings.court_id; @@ -100,8 +115,9 @@ async function handle_init(app, ws, msg) { } if (msg.initialize_display) { initialize_client(ws, app, tournament_key, court_id); + } else { + matches_handler(app, ws, tournament_key, ws.court_id); } - matches_handler(app, ws, tournament_key, court_id); } async function initialize_client(ws, app, tournament_key, court_id) { @@ -114,6 +130,7 @@ async function initialize_client(ws, app, tournament_key, court_id) { notify_change_ws(ws, tournament_key, court_id, "settings-update", display_setting); } } + matches_handler(app, ws, tournament_key, ws.court_id); } function determine_client_id(ws) { @@ -177,7 +194,7 @@ function client_id(app, tkey, client_id) { }); } -function get_display_court_displaysettings(app, tkey, client_id) { +function get_display_court_displaysettings(app, client_id) { return new Promise((resolve, reject) => { const display_court_query = { 'client_id': client_id }; app.db.display_court_displaysettings.find(display_court_query).limit(1).exec((err, display_court_displaysetting) => { @@ -383,47 +400,50 @@ function create_event_representation(tournament) { } async function restart_panel(app, tournament_key, client_id, new_court_id) { - for (const panel_ws of all_panels) { - - const ws_client_id = determine_client_id(panel_ws); - if (client_id == ws_client_id) { - if (new_court_id) { - panel_ws.court_id = new_court_id; - - const updatevalues = { - court_id: new_court_id - } - const client_court_displaysetting = await update_client_court_displaysetting(app, client_id, updatevalues); - - } + var client_court_displaysetting = null; + if (new_court_id) { - initialize_client(panel_ws, app, tournament_key, panel_ws.court_id); - // Two time to make it work, no idea why - matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); - matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); + const updatevalues = { + court_id: new_court_id } - + client_court_displaysetting = await update_client_court_displaysetting(app, client_id, updatevalues); + } + var display_online = reinitialize_panel(app, tournament_key,client_id, new_court_id); + if (client_court_displaysetting != null) { + client_court_displaysetting.online = display_online; + admin.notify_change(app, tournament_key, 'display_status_changed', { 'display_court_displaysetting': client_court_displaysetting }); } + } async function change_display_mode(app, tournament_key, client_id, new_displaysettings_id) { if (new_displaysettings_id) { - for (const panel_ws of all_panels) { + const updatevalues = { + displaysetting_id: new_displaysettings_id + } + const client_court_displaysetting = await update_client_court_displaysetting(app, client_id, updatevalues); + var display_online = reinitialize_panel(app, tournament_key, client_id,null); + client_court_displaysetting.online = display_online; + admin.notify_change(app, tournament_key, 'display_status_changed', { 'display_court_displaysetting': client_court_displaysetting }); + } +} - const ws_client_id = determine_client_id(panel_ws); - if (client_id == ws_client_id) { - const updatevalues = { - displaysetting_id: new_displaysettings_id - } - const client_court_displaysetting = await update_client_court_displaysetting(app, client_id, updatevalues); - - initialize_client(panel_ws, app, tournament_key, panel_ws.court_id); - // Two time to make it work, no idea why - matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); - matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); +function reinitialize_panel(app, tournament_key, client_id, new_court_id) { + for (const panel_ws of all_panels) { + const ws_client_id = determine_client_id(panel_ws); + if (client_id == ws_client_id) { + if (new_court_id != null) { + panel_ws.court_id = new_court_id; } + + initialize_client(panel_ws, app, tournament_key, panel_ws.court_id); + // Two times to make it work, no idea why + //matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); + //matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); + return true; } } + return false;; } function add_display_status(app, tournament_key, displays) { @@ -446,12 +466,8 @@ function add_display_status(app, tournament_key, displays) { } } if (!found) { - const client_court_displaysetting = { - client_id: ws_client_id, - court_id: panel_ws.court_id, - displaysetting_id: "default", - online: true - } + const client_court_displaysetting = display_court_displaysettings = create_display_court_displaysettings(ws_client_id, panel_ws.court_id, setting.id, "default"); + client_court_displaysetting.online = true; displays[displays.length] = client_court_displaysetting; } } diff --git a/static/css/admin.css b/static/css/admin.css index 6be16da..17f3164 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -59,8 +59,8 @@ h3 { .errors { position: fixed; - bottom: 0; - left: 0; + bottom: 4.5em; + right: 0.5em; color: #f00; background: rgba(255, 255, 255, 0.8); white-space: pre-wrap; diff --git a/static/js/change.js b/static/js/change.js index 78c8ef9..ed6d8dc 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -157,6 +157,24 @@ function default_handler_func(rerender, special_funcs, c) { case 'update_player_status': //nothing todo here break; + case 'display_status_changed': + const display_setting = c.val.display_court_displaysetting; + const d = utils.find(curt.displays, m => m.client_id === display_setting.client_id); + var laststatus = false; + if (!d) { + curt.displays[curt.displays.length] = display_setting; + curt.displays.sort(utils.cmp_key('client_id')); + return; + } else { + laststatus = d.online; + d.court_id = display_setting.court_id; + d.displaysetting_id = display_setting.displaysetting_id; + d.online = display_setting.online; + } + if (laststatus != d.online) { + cerror.silent('Display ' + display_setting.client_id + ' is ' + (display_setting.online ? 'online' : 'offline')); + } + break; default: cerror.silent('Unsupported change type ' + c.ctype); } From 5ad5e73bc23c9c8d5d3f4c7df7acfdebf1b21daa Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 20 Apr 2024 15:59:36 +0200 Subject: [PATCH 070/224] Bugfix: removed unnessesary and undefined variable --- bts/btp_conn.js | 4 ++-- bts/bupws.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 0b236db..53bd3fa 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -270,7 +270,7 @@ class BTPConn { } if (! this.key_unicode) { - serror.silent('Trying to send match data, but never logged in. Must retry later'); + //serror.silent('Trying to send match data, but never logged in. Must retry later'); return; } @@ -307,7 +307,7 @@ class BTPConn { } if (! this.key_unicode) { - serror.silent('Trying to update player data, but never logged in. Must retry later'); + //serror.silent('Trying to update player data, but never logged in. Must retry later'); return; } diff --git a/bts/bupws.js b/bts/bupws.js index b360b1d..bff2412 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -466,7 +466,7 @@ function add_display_status(app, tournament_key, displays) { } } if (!found) { - const client_court_displaysetting = display_court_displaysettings = create_display_court_displaysettings(ws_client_id, panel_ws.court_id, setting.id, "default"); + const client_court_displaysetting = display_court_displaysettings = create_display_court_displaysettings(ws_client_id, panel_ws.court_id, "default"); client_court_displaysetting.online = true; displays[displays.length] = client_court_displaysetting; } From 1bc1121f15edb7ccf23ff9d15ad4df049ad3ed2c Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 20 Apr 2024 16:06:13 +0200 Subject: [PATCH 071/224] Bugfix: another unnessesary Variable has to be remove --- bts/bupws.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bts/bupws.js b/bts/bupws.js index bff2412..35f3aa8 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -466,7 +466,7 @@ function add_display_status(app, tournament_key, displays) { } } if (!found) { - const client_court_displaysetting = display_court_displaysettings = create_display_court_displaysettings(ws_client_id, panel_ws.court_id, "default"); + const client_court_displaysetting = create_display_court_displaysettings(ws_client_id, panel_ws.court_id, "default"); client_court_displaysetting.online = true; displays[displays.length] = client_court_displaysetting; } From 46d4c9ccec2d6fa2e8ed3836c61905f33d358289 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 28 Apr 2024 15:14:37 +0200 Subject: [PATCH 072/224] Enhancement: Now it is possible to split doubles for tablet service automatically. Also it is possible to administer this behaviour in tournamnet settings --- bts/admin.js | 3 +- bts/http_api.js | 60 +++++++++++++++++++++++++--------------- static/js/change.js | 1 + static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/ctournament.js | 2 ++ 6 files changed, 44 insertions(+), 24 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index d1039d1..5e2ff7f 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -57,7 +57,8 @@ function handle_tournament_edit_props(app, ws, msg) { 'is_team', 'is_nation_competition', 'only_now_on_court', 'warmup', 'warmup_ready', 'warmup_start', 'ticker_enabled', 'ticker_url', 'ticker_password', - 'language', 'dm_style', 'tabletoperator_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', + 'language', 'dm_style', 'tabletoperator_enabled', + 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', 'logo_background_color', 'logo_foreground_color']); diff --git a/bts/http_api.js b/bts/http_api.js index ab9e218..e8c0176 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -412,30 +412,44 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ team = cur_match.setup.teams[index]; } if (team && typeof team.players !== 'undefined') { - var tabletoperator = []; - - team.players.forEach((player) => { - tabletoperator.push(player); - }); - - const new_tabletoperator = { - tournament_key, - tabletoperator, - 'match_id': cur_match._id, - 'start_ts': end_ts, - 'end_ts': null, - 'court': null, - 'played_on_court': (cur_match.setup.court_id ? cur_match.setup.court_id : null) - }; - - app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { - if (err) { - ws.respond(msg, err); - return; + var teams = []; + if (tournament.tabletoperator_split_doubles && team.players.length > 1) { + for (const player of team.players) { + var newTeam = { + players: [player] + }; + + teams.push(newTeam); } - const admin = require('./admin'); // avoid dependency cycle - admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); - }); + } else { + teams.push(team); + } + + for (const t of teams) { + var tabletoperator = []; + t.players.forEach((player) => { + tabletoperator.push(player); + }); + + const new_tabletoperator = { + tournament_key, + tabletoperator, + 'match_id': cur_match._id, + 'start_ts': end_ts, + 'end_ts': null, + 'court': null, + 'played_on_court': (cur_match.setup.court_id ? cur_match.setup.court_id : null) + }; + + app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { + if (err) { + ws.respond(msg, err); + return; + } + const admin = require('./admin'); // avoid dependency cycle + admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); + }); + } } } diff --git a/static/js/change.js b/static/js/change.js index ed6d8dc..1f609f2 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -70,6 +70,7 @@ function default_handler_func(rerender, special_funcs, c) { 'is_team', 'is_nation_competition', 'only_now_on_court', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', + 'tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled']; for (const cb_name of CHECKBOXES) { uiu.qsEach('input[name="' + cb_name + '"]', function(el) { diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index a18fdce..51bd968 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -100,6 +100,7 @@ var ci18n_de = { 'tournament:edit:btp:system timezone': 'Systemeinstellung ({tz})', 'tournament:edit:tabletoperator_enabled': 'Tabletbediener einsetzen', 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Gewinner der Viertelfinals bedienen das Tablet', + 'tournament:edit:tabletoperator_split_doubles': 'Doppelpaarungen für Tabletbdienung teilen', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', 'tournament:edit:ticker_enabled': 'Ticker aktivieren', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 36a7d14..769784c 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -104,6 +104,7 @@ var ci18n_en = { 'tournament:edit:ticker_password': 'Ticker password:', 'tournament:edit:tabletoperator_enabled': 'Use Tabletoperators', 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Winner of Quarterfinals hav to do tabletoperatorservice', + 'tournament:edit:tabletoperator_split_doubles': 'Split Doubles for Tablet Service.', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', 'tournament:edit:displays': 'Manage Displays:', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 6fdda95..66c8a67 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -742,6 +742,7 @@ function ui_edit() { create_checkbox(curt, ticker_fieldset, 'tabletoperator_with_umpire_enabled') create_checkbox(curt, ticker_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled') create_checkbox(curt, ticker_fieldset, 'tabletoperator_use_manual_counting_boards_enabled') + create_checkbox(curt, ticker_fieldset, 'tabletoperator_split_doubles') uiu.el(form, 'button', { @@ -769,6 +770,7 @@ function ui_edit() { ticker_password: data.ticker_password, tabletoperator_with_umpire_enabled: (!!data.tabletoperator_with_umpire_enabled), tabletoperator_winner_of_quaterfinals_enabled: (!!data.tabletoperator_winner_of_quaterfinals_enabled), + tabletoperator_split_doubles: (!!data.tabletoperator_split_doubles), tabletoperator_use_manual_counting_boards_enabled: (!!data.tabletoperator_use_manual_counting_boards_enabled), tabletoperator_enabled: (!!data.tabletoperator_enabled), }; From aa78c4bc3aa0587f0ba464f508e75befa2117b02 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 28 Apr 2024 15:57:27 +0200 Subject: [PATCH 073/224] Enhancement: Now it is possible to administer if the tabletoperator should have a break after his service or not --- bts/admin.js | 2 +- bts/http_api.js | 136 ++++++++++++++++++++------------------- static/js/change.js | 2 +- static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 3 +- static/js/ctournament.js | 2 + 6 files changed, 77 insertions(+), 69 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 5e2ff7f..b4b81de 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -57,7 +57,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'is_team', 'is_nation_competition', 'only_now_on_court', 'warmup', 'warmup_ready', 'warmup_start', 'ticker_enabled', 'ticker_url', 'ticker_password', - 'language', 'dm_style', 'tabletoperator_enabled', + 'language', 'dm_style', 'tabletoperator_enabled','tabletoperator_set_break_after_tabletservice', 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', 'logo_background_color', 'logo_foreground_color']); diff --git a/bts/http_api.js b/bts/http_api.js index e8c0176..61c4df1 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -547,85 +547,89 @@ function remove_player_on_court (app, tkey, cur_match_id, end_ts, callback) { function remove_tablet_on_court (app, tkey, cur_match_id, end_ts, callback) { - const admin = require('./admin'); // avoid dependency cycle - app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { - if (err) return callback(err); + + app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { + if (err) { + return callback(err); + } + app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { + if (err) return callback(err); - app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { - if (err) { - console.error(err); - return callback(err); - } + app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { + if (err) { + console.error(err); + return callback(err); + } - async.each(matches, (match, cb) => { + async.each(matches, (match, cb) => { - if(match.setup.now_on_court == true) { - return cb(null); - } + if(match.setup.now_on_court == true) { + return cb(null); + } - if(!cur_match.setup.tabletoperators || cur_match.setup.tabletoperators == 0) { - return cb(null); - } - - const match_id = match._id; - let remove_btp_ids = [ cur_match.setup.tabletoperators[0].btp_id]; + if(!cur_match.setup.tabletoperators || cur_match.setup.tabletoperators == 0) { + return cb(null); + } - if(cur_match.setup.tabletoperators.length > 1) { - remove_btp_ids.push(cur_match.setup.tabletoperators[1].btp_id); - } + const match_id = match._id; + let remove_btp_ids = [ cur_match.setup.tabletoperators[0].btp_id]; + if(cur_match.setup.tabletoperators.length > 1) { + remove_btp_ids.push(cur_match.setup.tabletoperators[1].btp_id); + } - let change = false; + let change = false; - if (match.setup.teams[0].players.length > 0 && - remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { - match.setup.teams[0].players[0].now_tablet_on_court = false; - match.setup.teams[0].players[0].checked_in = false; - match.setup.teams[0].players[0].last_time_on_court_ts = end_ts; - change = true; - } - - if (match.setup.teams[0].players.length > 1 && - remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { - match.setup.teams[0].players[1].now_tablet_on_court = false; - match.setup.teams[0].players[1].checked_in = false; - match.setup.teams[0].players[1].last_time_on_court_ts = end_ts; - change = true; - } + if (match.setup.teams[0].players.length > 0 && + remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + reset_tabletoperator_settings_at_player(tournament, match.setup.teams[0].players[0]); + change = true; + } - if (match.setup.teams[1].players.length > 0 && - remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { - match.setup.teams[1].players[0].now_tablet_on_court = false; - match.setup.teams[1].players[0].checked_in = false; - match.setup.teams[1].players[0].last_time_on_court_ts = end_ts; - change = true; - } + if (match.setup.teams[0].players.length > 1 && + remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + reset_tabletoperator_settings_at_player(tournament, match.setup.teams[0].players[1]); + change = true; + } - if (match.setup.teams[1].players.length > 1 && - remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { - match.setup.teams[1].players[1].now_tablet_on_court = false; - match.setup.teams[1].players[1].checked_in = false; - match.setup.teams[1].players[1].last_time_on_court_ts = end_ts; - change = true; - } + if (match.setup.teams[1].players.length > 0 && + remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + reset_tabletoperator_settings_at_player(tournament, match.setup.teams[1].players[0]); + change = true; + } - if (change) { - const setup = match.setup; - const match_q = {_id: match_id}; - app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { - if (err) return cb(err); + if (match.setup.teams[1].players.length > 1 && + remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + reset_tabletoperator_settings_at_player(tournament, match.setup.teams[1].players[1]); + change = true; + } - admin.notify_change(app, match.tournament_key, 'update_player_status', { match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); + if (change) { + const setup = match.setup; + const match_q = {_id: match_id}; + app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + if (err) return cb(err); + const admin = require('./admin'); // avoid dependency cycle + admin.notify_change(app, match.tournament_key, 'update_player_status', { match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup}); + return cb(null); + }); + } else { return cb(null); - }); - } else { - return cb(null); - } - }, callback); - }); - }); + } + }, callback); + }); + }); + }); +} + +function reset_tabletoperator_settings_at_player(tournament, player) { + player.now_tablet_on_court = false; + player.checked_in = false; + if (tournament.tabletoperator_set_break_after_tabletservice) { + player.last_time_on_court_ts = end_ts; + } } diff --git a/static/js/change.js b/static/js/change.js index 1f609f2..b4585bd 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -70,7 +70,7 @@ function default_handler_func(rerender, special_funcs, c) { 'is_team', 'is_nation_competition', 'only_now_on_court', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', - 'tabletoperator_split_doubles', + 'tabletoperator_split_doubles','tabletoperator_set_break_after_tabletservice', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled']; for (const cb_name of CHECKBOXES) { uiu.qsEach('input[name="' + cb_name + '"]', function(el) { diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 51bd968..69646ab 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -101,6 +101,7 @@ var ci18n_de = { 'tournament:edit:tabletoperator_enabled': 'Tabletbediener einsetzen', 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Gewinner der Viertelfinals bedienen das Tablet', 'tournament:edit:tabletoperator_split_doubles': 'Doppelpaarungen für Tabletbdienung teilen', + 'tournament:edit:tabletoperator_set_break_after_tabletservice': 'Pausenzeit für Tabletbedienung setzen', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', 'tournament:edit:ticker_enabled': 'Ticker aktivieren', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 769784c..3058249 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -103,7 +103,8 @@ var ci18n_en = { 'tournament:edit:ticker_url': 'Ticker URL:', 'tournament:edit:ticker_password': 'Ticker password:', 'tournament:edit:tabletoperator_enabled': 'Use Tabletoperators', - 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Winner of Quarterfinals hav to do tabletoperatorservice', + 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Winner of Quarterfinals have to do tabletoperatorservice', + 'tournament:edit:tabletoperator_set_break_after_tabletservice': 'Set break after tabletservice', 'tournament:edit:tabletoperator_split_doubles': 'Split Doubles for Tablet Service.', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 66c8a67..3a1d8a2 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -743,6 +743,7 @@ function ui_edit() { create_checkbox(curt, ticker_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled') create_checkbox(curt, ticker_fieldset, 'tabletoperator_use_manual_counting_boards_enabled') create_checkbox(curt, ticker_fieldset, 'tabletoperator_split_doubles') + create_checkbox(curt, ticker_fieldset, 'tabletoperator_set_break_after_tabletservice') uiu.el(form, 'button', { @@ -771,6 +772,7 @@ function ui_edit() { tabletoperator_with_umpire_enabled: (!!data.tabletoperator_with_umpire_enabled), tabletoperator_winner_of_quaterfinals_enabled: (!!data.tabletoperator_winner_of_quaterfinals_enabled), tabletoperator_split_doubles: (!!data.tabletoperator_split_doubles), + tabletoperator_set_break_after_tabletservice: (!!data.tabletoperator_set_break_after_tabletservice), tabletoperator_use_manual_counting_boards_enabled: (!!data.tabletoperator_use_manual_counting_boards_enabled), tabletoperator_enabled: (!!data.tabletoperator_enabled), }; From 8df01a7f820d66f357249e2f8299018932c9db85 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 28 Apr 2024 19:27:13 +0200 Subject: [PATCH 074/224] Bugfix: Tabletoperator was not restet correctly --- bts/http_api.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/bts/http_api.js b/bts/http_api.js index 61c4df1..36215b1 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -582,25 +582,25 @@ function remove_tablet_on_court (app, tkey, cur_match_id, end_ts, callback) { if (match.setup.teams[0].players.length > 0 && remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { - reset_tabletoperator_settings_at_player(tournament, match.setup.teams[0].players[0]); + reset_tabletoperator_settings_at_player(tournament, match.setup.teams[0].players[0], end_ts); change = true; } if (match.setup.teams[0].players.length > 1 && remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { - reset_tabletoperator_settings_at_player(tournament, match.setup.teams[0].players[1]); + reset_tabletoperator_settings_at_player(tournament, match.setup.teams[0].players[1], end_ts); change = true; } if (match.setup.teams[1].players.length > 0 && remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { - reset_tabletoperator_settings_at_player(tournament, match.setup.teams[1].players[0]); + reset_tabletoperator_settings_at_player(tournament, match.setup.teams[1].players[0], end_ts); change = true; } if (match.setup.teams[1].players.length > 1 && remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { - reset_tabletoperator_settings_at_player(tournament, match.setup.teams[1].players[1]); + reset_tabletoperator_settings_at_player(tournament, match.setup.teams[1].players[1], end_ts); change = true; } @@ -624,11 +624,13 @@ function remove_tablet_on_court (app, tkey, cur_match_id, end_ts, callback) { }); } -function reset_tabletoperator_settings_at_player(tournament, player) { +function reset_tabletoperator_settings_at_player(tournament, player, end_ts) { player.now_tablet_on_court = false; - player.checked_in = false; if (tournament.tabletoperator_set_break_after_tabletservice) { player.last_time_on_court_ts = end_ts; + player.checked_in = false; + } else { + player.checked_in = true; } } From a80bce995f78bcc8193202aa12654945540640d5 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 28 Apr 2024 21:28:43 +0200 Subject: [PATCH 075/224] Enhancement: abletoperators now will also be checkedin on btp after service --- bts/http_api.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bts/http_api.js b/bts/http_api.js index 36215b1..651fd78 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -582,25 +582,25 @@ function remove_tablet_on_court (app, tkey, cur_match_id, end_ts, callback) { if (match.setup.teams[0].players.length > 0 && remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { - reset_tabletoperator_settings_at_player(tournament, match.setup.teams[0].players[0], end_ts); + reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[0].players[0], end_ts); change = true; } if (match.setup.teams[0].players.length > 1 && remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { - reset_tabletoperator_settings_at_player(tournament, match.setup.teams[0].players[1], end_ts); + reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[0].players[1], end_ts); change = true; } if (match.setup.teams[1].players.length > 0 && remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { - reset_tabletoperator_settings_at_player(tournament, match.setup.teams[1].players[0], end_ts); + reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[1].players[0], end_ts); change = true; } if (match.setup.teams[1].players.length > 1 && remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { - reset_tabletoperator_settings_at_player(tournament, match.setup.teams[1].players[1], end_ts); + reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[1].players[1], end_ts); change = true; } @@ -624,13 +624,14 @@ function remove_tablet_on_court (app, tkey, cur_match_id, end_ts, callback) { }); } -function reset_tabletoperator_settings_at_player(tournament, player, end_ts) { +function reset_tabletoperator_settings_at_player(app, tkey, tournament, player, end_ts) { player.now_tablet_on_court = false; if (tournament.tabletoperator_set_break_after_tabletservice) { player.last_time_on_court_ts = end_ts; player.checked_in = false; } else { player.checked_in = true; + btp_manager.update_players(app, tkey, [player]); } } From e59f4d4944fa4856a3780b4e2e3f4bda5c753f1f Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 4 May 2024 17:54:07 +0200 Subject: [PATCH 076/224] Enhancement: break times of teabletoperators will be displaied in blue --- bts/btp_sync.js | 9 ++++++++- bts/http_api.js | 6 ++++-- static/css/cmatch.css | 1 - static/js/cmatch.js | 10 +++++++++- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 4f44545..837f070 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -393,6 +393,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { match.duration_ms = cur_match.duration_ms; match.end_ts = cur_match.end_ts; + if(match.setup.now_on_court === false) { if(cur_match.setup.warmup) { match.setup.warmup = cur_match.setup.warmup; @@ -418,7 +419,8 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { for(let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++){ - cur_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; + cur_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; + match.setup.teams[team_index].players[player_index].tablet_break_active = cur_match.setup.teams[team_index].players[player_index].tablet_break_active; } } @@ -603,6 +605,7 @@ async function integrate_player_state(app, tkey, btp_state, callback) { const player = cur_match.setup.teams[team_nr].players[player_nr]; if(ids_to_change.indexOf(id) == -1) { player.checked_in = true; + player.tablet_break_active = false; ids_to_change.push(id); players_to_change.push(player); } @@ -979,21 +982,25 @@ function set_player_on_court (app, tkey, match_on_court_setup, callback) { if (match.setup.teams[0].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { match.setup.teams[0].players[0].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[0].tablet_break_active = false; change = true; } if (match.setup.teams[0].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { match.setup.teams[0].players[1].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[1].tablet_break_active = false; change = true; } if (match.setup.teams[1].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { match.setup.teams[1].players[0].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[0].tablet_break_active = false; change = true; } if (match.setup.teams[1].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { match.setup.teams[1].players[1].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[1].tablet_break_active = false; change = true; } if (change) { diff --git a/bts/http_api.js b/bts/http_api.js index 651fd78..9bf135e 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -547,7 +547,7 @@ function remove_player_on_court (app, tkey, cur_match_id, end_ts, callback) { function remove_tablet_on_court (app, tkey, cur_match_id, end_ts, callback) { - + const admin = require('./admin'); // avoid dependency cycle app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { if (err) { return callback(err); @@ -609,7 +609,6 @@ function remove_tablet_on_court (app, tkey, cur_match_id, end_ts, callback) { const match_q = {_id: match_id}; app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { if (err) return cb(err); - const admin = require('./admin'); // avoid dependency cycle admin.notify_change(app, match.tournament_key, 'update_player_status', { match__id: match._id, btp_winner: match.btp_winner, setup: match.setup}); @@ -629,8 +628,11 @@ function reset_tabletoperator_settings_at_player(app, tkey, tournament, player, if (tournament.tabletoperator_set_break_after_tabletservice) { player.last_time_on_court_ts = end_ts; player.checked_in = false; + player.tablet_break_active = true; + } else { player.checked_in = true; + player.tablet_break_active = false; btp_manager.update_players(app, tkey, [player]); } } diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 14b47e4..1d27dbd 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -253,7 +253,6 @@ match_second_call_button, .player > .timer { display: inline-block; - background-color: #cc0000; vertical-align: middle; font-size: 0.7em; padding: 0.2em 0.3em 0.0em 0.3em; diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 7746dfa..c165f7f 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -555,7 +555,9 @@ function create_timer(timer_state, parent, default_color, exigent_color) { return; } - let el = uiu.el(parent, 'div', {class: 'timer', style: ('color:' + default_color +';')}, tv.str); + + var bgColor = timer_state.bgColor; + let el = uiu.el(parent, 'div', { class: 'timer', style: ('background-color:' + bgColor +'; color:' + default_color +';')}, tv.str); var tobj = {} @@ -592,6 +594,12 @@ function _extract_player_timer_state(player) { s.timer.start = (player.last_time_on_court_ts ? player.last_time_on_court_ts : false); s.timer.upwards = false; s.timer.exigent = false; + + if (player.tablet_break_active) { + s.bgColor = "#0000ff"; + } else { + s.bgColor = "#ff0000"; + } return s; } From 5e40d3066d670d26a3461409b2979dcde9bd1b21 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 4 May 2024 20:42:27 +0200 Subject: [PATCH 077/224] Enhancement: Tabletoperator can get a different break as the players will get --- bts/admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bts/admin.js b/bts/admin.js index b4b81de..e873292 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -56,7 +56,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'btp_ip', 'btp_password', 'is_team', 'is_nation_competition', 'only_now_on_court', 'warmup', 'warmup_ready', 'warmup_start', - 'ticker_enabled', 'ticker_url', 'ticker_password', + 'ticker_enabled', 'ticker_url', 'ticker_password','tabletoperator_break_seconds', 'language', 'dm_style', 'tabletoperator_enabled','tabletoperator_set_break_after_tabletservice', 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', From 6665bf476ca3d3af08454c8942c0cdef0ab32222 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 4 May 2024 20:43:06 +0200 Subject: [PATCH 078/224] Revert "Enhancement: Tabletoperator can get a different break as the players will get" This reverts commit 5e40d3066d670d26a3461409b2979dcde9bd1b21. --- bts/admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bts/admin.js b/bts/admin.js index e873292..b4b81de 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -56,7 +56,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'btp_ip', 'btp_password', 'is_team', 'is_nation_competition', 'only_now_on_court', 'warmup', 'warmup_ready', 'warmup_start', - 'ticker_enabled', 'ticker_url', 'ticker_password','tabletoperator_break_seconds', + 'ticker_enabled', 'ticker_url', 'ticker_password', 'language', 'dm_style', 'tabletoperator_enabled','tabletoperator_set_break_after_tabletservice', 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', From 685c4ab09354e5196c80d3f2b121c5df091d75e2 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 4 May 2024 20:43:19 +0200 Subject: [PATCH 079/224] Enhancement: Tabletoperator can get a different break as the players will get --- bts/btp_sync.js | 9 +++++++ bts/http_api.js | 8 ++++++- static/js/change.js | 2 +- static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/ctournament.js | 52 ++++++++++++++++++++++++++-------------- 6 files changed, 53 insertions(+), 20 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 837f070..e973c75 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -437,6 +437,15 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { const http_api = require('./http_api'); http_api.add_player_to_tabletoperator_list_by_match(app, tournament, tkey, match, match.end_ts); + + async.waterfall([ + cb => http_api.remove_tablet_on_court(app, tkey, match._id, match.end_ts, cb) + ], function (err) { + if (err) { + return; + } + }); + } }); } diff --git a/bts/http_api.js b/bts/http_api.js index 9bf135e..8eac097 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -626,9 +626,14 @@ function remove_tablet_on_court (app, tkey, cur_match_id, end_ts, callback) { function reset_tabletoperator_settings_at_player(app, tkey, tournament, player, end_ts) { player.now_tablet_on_court = false; if (tournament.tabletoperator_set_break_after_tabletservice) { - player.last_time_on_court_ts = end_ts; + var offset = 0; + if (tournament.tabletoperator_break_seconds) { + offset = (parseInt(tournament.tabletoperator_break_seconds) * 1000) - tournament.btp_settings.pause_duration_ms; + } + player.last_time_on_court_ts = end_ts + offset; player.checked_in = false; player.tablet_break_active = true; + btp_manager.update_players(app, tkey, [player]); } else { player.checked_in = true; @@ -667,4 +672,5 @@ module.exports = { matchinfo_handler, score_handler, add_player_to_tabletoperator_list_by_match, + remove_tablet_on_court }; diff --git a/static/js/change.js b/static/js/change.js index b4585bd..dada6c3 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -70,7 +70,7 @@ function default_handler_func(rerender, special_funcs, c) { 'is_team', 'is_nation_competition', 'only_now_on_court', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', - 'tabletoperator_split_doubles','tabletoperator_set_break_after_tabletservice', + 'tabletoperator_split_doubles', 'tabletoperator_set_break_after_tabletservice','tabletoperator_break_seconds', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled']; for (const cb_name of CHECKBOXES) { uiu.qsEach('input[name="' + cb_name + '"]', function(el) { diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 69646ab..baf8105 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -102,6 +102,7 @@ var ci18n_de = { 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Gewinner der Viertelfinals bedienen das Tablet', 'tournament:edit:tabletoperator_split_doubles': 'Doppelpaarungen für Tabletbdienung teilen', 'tournament:edit:tabletoperator_set_break_after_tabletservice': 'Pausenzeit für Tabletbedienung setzen', + 'tournament:edit:tabletoperator_break_seconds': 'Pausenzeit nach Tabletbedienung (sec)', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', 'tournament:edit:ticker_enabled': 'Ticker aktivieren', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 3058249..dd015f8 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -105,6 +105,7 @@ var ci18n_en = { 'tournament:edit:tabletoperator_enabled': 'Use Tabletoperators', 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Winner of Quarterfinals have to do tabletoperatorservice', 'tournament:edit:tabletoperator_set_break_after_tabletservice': 'Set break after tabletservice', + 'tournament:edit:tabletoperator_break_seconds': 'Pausenzeit nach Tabletbedienung (sec)', 'tournament:edit:tabletoperator_split_doubles': 'Split Doubles for Tablet Service.', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 3a1d8a2..153491b 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -738,13 +738,16 @@ function ui_edit() { }); - create_checkbox(curt, ticker_fieldset,'tabletoperator_enabled') - create_checkbox(curt, ticker_fieldset, 'tabletoperator_with_umpire_enabled') - create_checkbox(curt, ticker_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled') - create_checkbox(curt, ticker_fieldset, 'tabletoperator_use_manual_counting_boards_enabled') - create_checkbox(curt, ticker_fieldset, 'tabletoperator_split_doubles') - create_checkbox(curt, ticker_fieldset, 'tabletoperator_set_break_after_tabletservice') - + create_checkbox(curt, ticker_fieldset, 'tabletoperator_enabled'); + create_checkbox(curt, ticker_fieldset, 'tabletoperator_with_umpire_enabled'); + create_checkbox(curt, ticker_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled'); + create_checkbox(curt, ticker_fieldset, 'tabletoperator_use_manual_counting_boards_enabled'); + create_checkbox(curt, ticker_fieldset, 'tabletoperator_split_doubles'); + create_checkbox(curt, ticker_fieldset, 'tabletoperator_set_break_after_tabletservice'); + if (!curt.tabletoperator_break_seconds) { + curt.tabletoperator_break_seconds = 300; + } + create_input(curt, "number", ticker_fieldset, 'tabletoperator_break_seconds') uiu.el(form, 'button', { role: 'submit', @@ -775,6 +778,7 @@ function ui_edit() { tabletoperator_set_break_after_tabletservice: (!!data.tabletoperator_set_break_after_tabletservice), tabletoperator_use_manual_counting_boards_enabled: (!!data.tabletoperator_use_manual_counting_boards_enabled), tabletoperator_enabled: (!!data.tabletoperator_enabled), + tabletoperator_break_seconds: data.tabletoperator_break_seconds, }; send({ type: 'tournament_edit_props', @@ -950,18 +954,30 @@ function ui_edit() { _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit); -function create_checkbox(curt, parent_el, filed_id) { - const label = uiu.el(parent_el, 'label'); - const attrs = { - type: 'checkbox', - name: filed_id, - }; - if (curt[filed_id]) { - attrs.checked = 'checked'; + function create_checkbox(curt, parent_el, filed_id) { + const label = uiu.el(parent_el, 'label'); + const attrs = { + type: 'checkbox', + name: filed_id, + }; + if (curt[filed_id]) { + attrs.checked = 'checked'; + } + uiu.el(label, 'input', attrs); + uiu.el(label, 'span', {}, ci18n('tournament:edit:' + filed_id)); } - uiu.el(label, 'input', attrs); - uiu.el(label, 'span', {}, ci18n('tournament:edit:' + filed_id)); -} + + function create_input(curt, type, parent_el, filed_id) { + const text_input = uiu.el(parent_el, 'label'); + uiu.el(text_input, 'span', {}, ci18n('tournament:edit:' + filed_id)); + uiu.el(text_input, 'input', { + type: type, + name: filed_id, + value: curt[filed_id] || '', + }); + } + + function createCourtSelectBox(parentEl,parent_id, court_id) { From 7920a8287c238e8a1e2b02a75a1cdb1ae2583f32 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 5 May 2024 14:14:44 +0200 Subject: [PATCH 080/224] Ne Featute: Now it is possible to handle the "national associations" as tabletoperators instead of the players --- bts/admin.js | 101 +++++++++++++++++++++++---------------- bts/http_api.js | 29 +++++++++-- static/js/change.js | 5 +- static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/ctournament.js | 2 + 6 files changed, 92 insertions(+), 47 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index b4b81de..695a915 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -57,7 +57,8 @@ function handle_tournament_edit_props(app, ws, msg) { 'is_team', 'is_nation_competition', 'only_now_on_court', 'warmup', 'warmup_ready', 'warmup_start', 'ticker_enabled', 'ticker_url', 'ticker_password', - 'language', 'dm_style', 'tabletoperator_enabled','tabletoperator_set_break_after_tabletservice', + 'language', 'dm_style', 'tabletoperator_enabled','tabletoperator_break_seconds', + 'tabletoperator_set_break_after_tabletservice','tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', 'logo_background_color', 'logo_foreground_color']); @@ -265,50 +266,66 @@ function handle_tabletoperator_add(app, ws, msg) { return ws.respond(msg, { message: 'Missing tournament_key' }); } const tournament_key = msg.tournament_key; - var team = null; - if (msg.match) { - const team_id = msg.team_id; - const match = msg.match - team = match.setup.teams[team_id]; - } else if (msg.tabletoperator_name) { - team = { - "players": [ - { - "asian_name": false, - "name": msg.tabletoperator_name, - "firstname": "", - "lastname": "", - "btp_id": -1 - } - ], - "name": "N/N" - }; - - } - if (team != null) { - team.players.forEach((player) => { - var tabletoperator = []; - tabletoperator.push(player); - const new_tabletoperator = { - tournament_key, - tabletoperator, - 'match_id': 'manually_added', - 'start_ts': Date.now(), - 'end_ts': null, - 'court': null, - 'played_on_court': null + app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { + if (err) { + return ws.respond(err); + } + + var team = null; + if (msg.match) { + const team_id = msg.team_id; + const match = msg.match + team = match.setup.teams[team_id]; + } else if (msg.tabletoperator_name) { + team = { + "players": [ + { + "asian_name": false, + "name": msg.tabletoperator_name, + "firstname": "", + "lastname": "", + "btp_id": -1 + } + ], + "name": "N/N" }; - app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_tabletoperator) { - if (err) { - ws.respond(msg, err); - return; + + } + if (team != null) { + team.players.forEach((player) => { + var tabletoperator = []; + if (tournament.tabletoperator_with_state_enabled && player.state) { + tabletoperator.push({ + "asian_name": false, + "name": player.state, + "firstname": "", + "lastname": "", + "btp_id": -1 + }); + } else { + tabletoperator.push(player); } - notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_tabletoperator }); + const new_tabletoperator = { + tournament_key, + tabletoperator, + 'match_id': 'manually_added', + 'start_ts': Date.now(), + 'end_ts': null, + 'court': null, + 'played_on_court': null + }; + app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_tabletoperator) { + if (err) { + ws.respond(msg, err); + return; + } + notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_tabletoperator }); + }); }); - }); - } else { - return ws.respond(msg, { message: 'Not enough Information to add a tabletoperator to list' }); - } + } else { + return ws.respond(msg, { message: 'Not enough Information to add a tabletoperator to list' }); + } + }); } function handle_match_edit(app, ws, msg) { diff --git a/bts/http_api.js b/bts/http_api.js index 8eac097..c08fc7e 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -411,18 +411,30 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ const index = cur_match.btp_winner % 2; team = cur_match.setup.teams[index]; } + + // TODO: 'tabletoperator_with_state_enabled' + if (team && typeof team.players !== 'undefined') { var teams = []; if (tournament.tabletoperator_split_doubles && team.players.length > 1) { for (const player of team.players) { + var toinsert = player + if (tournament.tabletoperator_with_state_enabled && player.state) { + toinsert = create_team_from_player_state(player); + } var newTeam = { - players: [player] + players: [toinsert] }; - teams.push(newTeam); } } else { - teams.push(team); + var toinsert = team; + if (tournament.tabletoperator_with_state_enabled && team.players[0].state) { + toinsert = { + players: [create_team_from_player_state(team.players[0])] + }; + } + teams.push(toinsert); } for (const t of teams) { @@ -456,6 +468,17 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ }); } } + + +function create_team_from_player_state(player) { + return { + "asian_name": false, + "name": player.state, + "firstname": "", + "lastname": "", + "btp_id": -1 + }; +} function remove_player_on_court (app, tkey, cur_match_id, end_ts, callback) { const admin = require('./admin'); // avoid dependency cycle app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { diff --git a/static/js/change.js b/static/js/change.js index dada6c3..204d4ed 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -69,8 +69,9 @@ function default_handler_func(rerender, special_funcs, c) { const CHECKBOXES = [ 'is_team', 'is_nation_competition', 'only_now_on_court', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', - 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', - 'tabletoperator_split_doubles', 'tabletoperator_set_break_after_tabletservice','tabletoperator_break_seconds', + 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_with_state_enabled', + 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', + 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled']; for (const cb_name of CHECKBOXES) { uiu.qsEach('input[name="' + cb_name + '"]', function(el) { diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index baf8105..562deda 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -101,6 +101,7 @@ var ci18n_de = { 'tournament:edit:tabletoperator_enabled': 'Tabletbediener einsetzen', 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Gewinner der Viertelfinals bedienen das Tablet', 'tournament:edit:tabletoperator_split_doubles': 'Doppelpaarungen für Tabletbdienung teilen', + 'tournament:edit:tabletoperator_with_state_enabled': 'Landesverband anstelle Spieler aufrufen', 'tournament:edit:tabletoperator_set_break_after_tabletservice': 'Pausenzeit für Tabletbedienung setzen', 'tournament:edit:tabletoperator_break_seconds': 'Pausenzeit nach Tabletbedienung (sec)', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index dd015f8..34b14f2 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -107,6 +107,7 @@ var ci18n_en = { 'tournament:edit:tabletoperator_set_break_after_tabletservice': 'Set break after tabletservice', 'tournament:edit:tabletoperator_break_seconds': 'Pausenzeit nach Tabletbedienung (sec)', 'tournament:edit:tabletoperator_split_doubles': 'Split Doubles for Tablet Service.', + 'tournament:edit:tabletoperator_with_state_enabled': 'Call up national association instead of player', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', 'tournament:edit:displays': 'Manage Displays:', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 153491b..00ad9ba 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -743,6 +743,7 @@ function ui_edit() { create_checkbox(curt, ticker_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled'); create_checkbox(curt, ticker_fieldset, 'tabletoperator_use_manual_counting_boards_enabled'); create_checkbox(curt, ticker_fieldset, 'tabletoperator_split_doubles'); + create_checkbox(curt, ticker_fieldset, 'tabletoperator_with_state_enabled'); create_checkbox(curt, ticker_fieldset, 'tabletoperator_set_break_after_tabletservice'); if (!curt.tabletoperator_break_seconds) { curt.tabletoperator_break_seconds = 300; @@ -775,6 +776,7 @@ function ui_edit() { tabletoperator_with_umpire_enabled: (!!data.tabletoperator_with_umpire_enabled), tabletoperator_winner_of_quaterfinals_enabled: (!!data.tabletoperator_winner_of_quaterfinals_enabled), tabletoperator_split_doubles: (!!data.tabletoperator_split_doubles), + tabletoperator_with_state_enabled: (!!data.tabletoperator_with_state_enabled), tabletoperator_set_break_after_tabletservice: (!!data.tabletoperator_set_break_after_tabletservice), tabletoperator_use_manual_counting_boards_enabled: (!!data.tabletoperator_use_manual_counting_boards_enabled), tabletoperator_enabled: (!!data.tabletoperator_enabled), From 9c679a2c11a47c40692c1302bddb6437799f5555 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sun, 5 May 2024 14:43:01 +0200 Subject: [PATCH 081/224] Bugfix: Players will now also be removed from court if the result was entered using BTP --- bts/btp_sync.js | 11 +---------- bts/http_api.js | 31 +++++++++++++++---------------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index e973c75..58b0eab 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -436,16 +436,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { } if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { const http_api = require('./http_api'); - http_api.add_player_to_tabletoperator_list_by_match(app, tournament, tkey, match, match.end_ts); - - async.waterfall([ - cb => http_api.remove_tablet_on_court(app, tkey, match._id, match.end_ts, cb) - ], function (err) { - if (err) { - return; - } - }); - + http_api.reset_player_tabletoperator(app, tkey, match._id, match.end_ts); } }); } diff --git a/bts/http_api.js b/bts/http_api.js index c08fc7e..13d3513 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -273,20 +273,7 @@ function score_handler(req, res) { } if (update.team1_won != undefined && update.team1_won != null) { - async.waterfall([ - cb => remove_player_on_court(req.app, tournament_key, match_id, update.end_ts, cb), - cb => remove_tablet_on_court(req.app, tournament_key, match_id, update.end_ts, cb), - cb => add_player_to_tabletoperator_list(req.app, tournament_key, match_id, update.end_ts) - - ], function(err) { - if (err) { - res.json({ - status: 'error', - message: err.message, - }); - return; - } - }); + reset_player_tabletoperator(req.app, tournament_key, match_id, update.end_ts); } if (req.body.shuttle_count) { @@ -378,6 +365,19 @@ function score_handler(req, res) { }); } +function reset_player_tabletoperator(app, tournament_key, match_id, end_ts) { + async.waterfall([ + cb => remove_player_on_court(app, tournament_key, match_id, end_ts, cb), + cb => remove_tablet_on_court(app, tournament_key, match_id, end_ts, cb), + cb => add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts) + + ], function (err) { + if (err) { + return; + } + }); +} + function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, end_ts) { app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { if (err) { @@ -694,6 +694,5 @@ module.exports = { matches_handler, matchinfo_handler, score_handler, - add_player_to_tabletoperator_list_by_match, - remove_tablet_on_court + reset_player_tabletoperator }; From 446c5c1ab10a4fb253067fce5379ed5a05b30e1e Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 8 May 2024 23:42:09 +0200 Subject: [PATCH 082/224] Add Headlines to the Edit-Tournament page for better Overview --- static/js/ci18n_de.js | 3 +++ static/js/ci18n_en.js | 4 ++++ static/js/ctournament.js | 10 +++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 562deda..4e28ad7 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -114,6 +114,9 @@ var ci18n_de = { 'tournament:edit:displays:court': 'Feld', 'tournament:edit:displays:setting': 'Einstellung', 'tournament:edit:displays:onlinestatus': 'Status', + 'tournament:edit:tablets': 'Tablets Einstellungen:', + 'tournament:edit:ticker': 'Ticker Einstellungen:', + 'tournament:edit:btp': 'Badminton Turnier Planer Einstellungen:', 'to_stats:header': 'Statistik der Technischen Offiziellen', 'to_stats:name': 'Name', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 34b14f2..0bc9b05 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -115,6 +115,10 @@ var ci18n_en = { 'tournament:edit:displays:court': 'Ar Court', 'tournament:edit:displays:setting': 'Setting', 'tournament:edit:displays:onlinestatus': 'Status', + 'tournament:edit:tablets': 'Tablets Settings:', + 'tournament:edit:ticker': 'Ticker Settings:', + 'tournament:edit:btp': 'Badminton Tournament Planer Settings:', + 'to_stats:header': 'Technical Officials Statistics', 'to_stats:name': 'Name', 'to_stats:umpire': 'U', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 00ad9ba..02fac1f 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -636,6 +636,7 @@ function ui_edit() { // BTP const btp_fieldset = uiu.el(form, 'fieldset'); + uiu.el(btp_fieldset, 'h2', {}, ci18n('tournament:edit:btp')); const btp_enabled_label = uiu.el(btp_fieldset, 'label'); const ba_attrs = { type: 'checkbox', @@ -710,6 +711,7 @@ function ui_edit() { // Ticker const ticker_fieldset = uiu.el(form, 'fieldset'); + uiu.el(ticker_fieldset, 'h2', {}, ci18n('tournament:edit:ticker')); const ticker_enabled_label = uiu.el(ticker_fieldset, 'label'); const te_attrs = { type: 'checkbox', @@ -738,6 +740,9 @@ function ui_edit() { }); + + const tablet_fieldset = uiu.el(form, 'fieldset'); + uiu.el(tablet_fieldset, 'h2', {}, ci18n('tournament:edit:tablets')); create_checkbox(curt, ticker_fieldset, 'tabletoperator_enabled'); create_checkbox(curt, ticker_fieldset, 'tabletoperator_with_umpire_enabled'); create_checkbox(curt, ticker_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled'); @@ -748,7 +753,10 @@ function ui_edit() { if (!curt.tabletoperator_break_seconds) { curt.tabletoperator_break_seconds = 300; } - create_input(curt, "number", ticker_fieldset, 'tabletoperator_break_seconds') + create_input(curt, "number", tablet_fieldset, 'tabletoperator_break_seconds') + + + uiu.el(form, 'button', { role: 'submit', From 8748fa6b39cd387f83a0bd961f5d913d0081bbaf Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 8 May 2024 23:44:37 +0200 Subject: [PATCH 083/224] Bug fix: Fix btp-sync if the btp property checkin is set on check in per match --- bts/btp_sync.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 58b0eab..fb99d98 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -578,7 +578,6 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { async function integrate_player_state(app, tkey, btp_state, callback) { const btp_manager = require('./btp_manager'); - app.db.tournaments.findOne({key: tkey}, (err, tournament) => { if(err) return callback(err); @@ -620,6 +619,10 @@ async function integrate_player_state(app, tkey, btp_state, callback) { return callback(null); }); } + else + { + return callback(null); + } }); } From fa152d4223a9ebfbc47befc08a0af0ed2a01bfbc Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 11 May 2024 13:13:51 +0200 Subject: [PATCH 084/224] Enhancement:I Umpires also will be called in Preparation. Integrated to use a Meetingpoint for preparation --- bts/admin.js | 1 + static/js/announcements.js | 11 ++++++++++- static/js/change.js | 3 ++- static/js/ci18n_de.js | 2 ++ static/js/ci18n_en.js | 2 ++ static/js/ctournament.js | 2 ++ 6 files changed, 19 insertions(+), 2 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 695a915..1602ded 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -61,6 +61,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'tabletoperator_set_break_after_tabletservice','tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', + 'preparation_meetingpoint_enabled', 'logo_background_color', 'logo_foreground_color']); if (msg.props.btp_timezone) { diff --git a/static/js/announcements.js b/static/js/announcements.js index 477ee47..269660d 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -23,7 +23,12 @@ function announcePreparationMatch(matchSetup) { var eventName = createEventAnnouncement(matchSetup); var round = createRoundAnnouncement(matchSetup); var teams = createTeamAnnouncement(matchSetup); - announce([preparation, matchNumber, eventName, round, teams, preparation]); + const umpire = createUmpire(matchSetup); + var lastPart = preparation; + if (curt.preparation_meetingpoint_enabled) { + lastPart = createMeetingPointAnnouncement(); + } + announce([preparation, matchNumber, eventName, round, teams, umpire, lastPart]); } function announceSecondCallTeamOne(matchSetup) { if(!(window.localStorage.getItem('enable_announcements') === 'true')) { @@ -211,6 +216,10 @@ function createPreparationAnnouncement() { return ci18n('announcements:preparation'); } +function createMeetingPointAnnouncement() { + return ci18n('announcements:meetingpoint'); +} + function announce(callArray) { if(!(window.localStorage.getItem('enable_announcements') === 'true')) { return; diff --git a/static/js/change.js b/static/js/change.js index 204d4ed..95c7a22 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -72,7 +72,8 @@ function default_handler_func(rerender, special_funcs, c) { 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds', - 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled']; + 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', + 'preparation_meetingpoint_enabled']; for (const cb_name of CHECKBOXES) { uiu.qsEach('input[name="' + cb_name + '"]', function(el) { el.checked = curt[cb_name]; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 562deda..ad7e7eb 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -54,6 +54,7 @@ var ci18n_de = { 'announcements:umpire': 'Schiedsrichter:', 'announcements:and': ' und ', 'announcements:preparation': 'In Vorbereitung:', + 'announcements:meetingpoint': 'Treffen am Meetingpoint!', 'announcements:on_court': 'Auf Spielfeld ', 'announcements:match_number': 'Spiel Nummer ', 'announcements:boys_singles': 'Jungeneinzel', @@ -106,6 +107,7 @@ var ci18n_de = { 'tournament:edit:tabletoperator_break_seconds': 'Pausenzeit nach Tabletbedienung (sec)', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', + 'tournament:edit:preparation_meetingpoint_enabled': 'Meetingpoint für Vorbereitung nutzen', 'tournament:edit:ticker_enabled': 'Ticker aktivieren', 'tournament:edit:ticker_url': 'Ticker-Adresse:', 'tournament:edit:ticker_password': 'Ticker-Passwort:', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 34b14f2..d89db1b 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -54,6 +54,7 @@ var ci18n_en = { 'announcements:umpire': 'Umpire:', 'announcements:and': ' and ', 'announcements:preparation': 'In preparation:', + 'announcements:meetingpoint': 'Come to the meetingpoint!', 'announcements:on_court': 'On Court ', 'announcements:match_number': 'Match number ', 'announcements:boys_singles': 'Boys singles', @@ -110,6 +111,7 @@ var ci18n_en = { 'tournament:edit:tabletoperator_with_state_enabled': 'Call up national association instead of player', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', + 'tournament:edit:preparation_meetingpoint_enabled': 'Use Meetingpoint for preparation', 'tournament:edit:displays': 'Manage Displays:', 'tournament:edit:displays:num': 'Displaynumber', 'tournament:edit:displays:court': 'Ar Court', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 00ad9ba..075dcb8 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -745,6 +745,7 @@ function ui_edit() { create_checkbox(curt, ticker_fieldset, 'tabletoperator_split_doubles'); create_checkbox(curt, ticker_fieldset, 'tabletoperator_with_state_enabled'); create_checkbox(curt, ticker_fieldset, 'tabletoperator_set_break_after_tabletservice'); + create_checkbox(curt, ticker_fieldset, 'preparation_meetingpoint_enabled'); if (!curt.tabletoperator_break_seconds) { curt.tabletoperator_break_seconds = 300; } @@ -781,6 +782,7 @@ function ui_edit() { tabletoperator_use_manual_counting_boards_enabled: (!!data.tabletoperator_use_manual_counting_boards_enabled), tabletoperator_enabled: (!!data.tabletoperator_enabled), tabletoperator_break_seconds: data.tabletoperator_break_seconds, + preparation_meetingpoint_enabled: (!!data.preparation_meetingpoint_enabled), }; send({ type: 'tournament_edit_props', From bb098af8abc38e13bd321df248533d28f3223837 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 11 May 2024 13:22:59 +0200 Subject: [PATCH 085/224] Enhancement: Some GUI improvement in Tournamnet Settings Page --- static/js/ctournament.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/static/js/ctournament.js b/static/js/ctournament.js index fc55e26..e0c9f0c 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -743,14 +743,14 @@ function ui_edit() { const tablet_fieldset = uiu.el(form, 'fieldset'); uiu.el(tablet_fieldset, 'h2', {}, ci18n('tournament:edit:tablets')); - create_checkbox(curt, ticker_fieldset, 'tabletoperator_enabled'); - create_checkbox(curt, ticker_fieldset, 'tabletoperator_with_umpire_enabled'); - create_checkbox(curt, ticker_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled'); - create_checkbox(curt, ticker_fieldset, 'tabletoperator_use_manual_counting_boards_enabled'); - create_checkbox(curt, ticker_fieldset, 'tabletoperator_split_doubles'); - create_checkbox(curt, ticker_fieldset, 'tabletoperator_with_state_enabled'); - create_checkbox(curt, ticker_fieldset, 'tabletoperator_set_break_after_tabletservice'); - create_checkbox(curt, ticker_fieldset, 'preparation_meetingpoint_enabled'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_enabled'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_umpire_enabled'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_use_manual_counting_boards_enabled'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_split_doubles'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_state_enabled'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_set_break_after_tabletservice'); + create_checkbox(curt, tablet_fieldset, 'preparation_meetingpoint_enabled'); if (!curt.tabletoperator_break_seconds) { curt.tabletoperator_break_seconds = 300; } From d62c776cb4884ad24c8f9aa85cc2c0d9fb7763ea Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 11 May 2024 21:06:26 +0200 Subject: [PATCH 086/224] Bugfix: Matches are removed from Displays if option only_now_on_court is set to true --- bts/bupws.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index 35f3aa8..33f1d2f 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -308,8 +308,8 @@ function matches_handler(app, ws, tournament_key, court_id) { if(db_matches){ let matches = db_matches.map(dbm => create_match_representation(tournament, dbm)); - if (tournament.only_now_on_court) { - matches = matches.filter(m => m.setup.now_on_court); + if (!court_id && tournament.only_now_on_court) { + matches = matches.filter(m => m.setup.now_on_court); } db_courts.sort(utils.cmp_key('num')); From 5bfa0b911418f695d794d5dea264da43699590a3 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Mon, 13 May 2024 19:54:54 +0200 Subject: [PATCH 087/224] New Feature: Noe it is potionally possible to call the tabletoperator also in preparation of an match --- bts/admin.js | 55 ++++++++++++++++++++++++-------------- bts/btp_sync.js | 11 +++++--- static/js/announcements.js | 3 ++- static/js/change.js | 2 +- static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/ctournament.js | 2 ++ 7 files changed, 49 insertions(+), 26 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 1602ded..5838e35 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -11,6 +11,7 @@ const serror = require('./serror'); const stournament = require('./stournament'); const ticker_manager = require('./ticker_manager'); const utils = require('./utils'); +const btp_sync = require('./btp_sync'); /** @@ -61,7 +62,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'tabletoperator_set_break_after_tabletservice','tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', - 'preparation_meetingpoint_enabled', + 'preparation_meetingpoint_enabled','preparation_tabletoperator_setup_enabled', 'logo_background_color', 'logo_foreground_color']); if (msg.props.btp_timezone) { @@ -367,30 +368,44 @@ function handle_match_preparation_call(app, ws, msg) { } const tournament_key = msg.tournament_key; - const setup = _extract_setup(msg.setup); - - app.db.matches.update({_id: msg.id, tournament_key}, {$set: {setup}}, {returnUpdatedDocs: true}, function(err, numAffected, changed_match) { + app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { if (err) { - ws.respond(msg, err); - return; - } - if (numAffected !== 1) { - ws.respond(msg, new Error('Cannot find match ' + msg.id + ' of tournament ' + tournament_key + ' in database')); - return; + return ws.respond(err); } - if (changed_match._id !== msg.id) { - const errmsg = 'Match ' + changed_match._id + ' changed by accident, intended to change ' + msg.id + ' (old nedb version?)'; - serror.silent(errmsg); - ws.respond(msg, new Error(errmsg)); - return; + const setup = _extract_setup(msg.setup); + if (tournament.preparation_tabletoperator_setup_enabled) { + if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { + if (!setup.tabletoperators || setup.tabletoperators == null) { + const admin = require('./admin'); + setup.tabletoperators = await btp_sync.fetch_tabletoperator(admin, app, tournament_key, "prep_call"); + } + } } - //notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, setup}); - notify_change(app, tournament_key, 'match_preparation_call', {match__id: msg.id, match: changed_match}); - btp_manager.update_highlight(app, changed_match); - - ws.respond(msg, err); + app.db.matches.update({ _id: msg.id, tournament_key }, { $set: { setup } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_match) { + if (err) { + ws.respond(msg, err); + return; + } + if (numAffected !== 1) { + ws.respond(msg, new Error('Cannot find match ' + msg.id + ' of tournament ' + tournament_key + ' in database')); + return; + } + if (changed_match._id !== msg.id) { + const errmsg = 'Match ' + changed_match._id + ' changed by accident, intended to change ' + msg.id + ' (old nedb version?)'; + serror.silent(errmsg); + ws.respond(msg, new Error(errmsg)); + return; + } + + //notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, setup}); + notify_change(app, tournament_key, 'match_preparation_call', { match__id: msg.id, match: changed_match }); + + btp_manager.update_highlight(app, changed_match); + + ws.respond(msg, err); + }); }); } function handle_begin_to_play_call(app, ws, msg) { diff --git a/bts/btp_sync.js b/bts/btp_sync.js index fb99d98..eafb2aa 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -790,9 +790,11 @@ async function integrate_now_on_court(app, tkey, callback) { setup.called_timestamp = called_timestamp; try { if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { - const value = await serializedAsyncTask(admin, app, tkey, court_id); - if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { - setup.tabletoperators = value; + if (!setup.tabletoperators || setup.tabletoperators == null) { + const value = await fetch_tabletoperator(admin, app, tkey, court_id); + if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { + setup.tabletoperators = value; + } } } } catch (err) { @@ -864,7 +866,7 @@ function serialized(fn) { return res; } } -const serializedAsyncTask = serialized(get_last_looser_on_court); +const fetch_tabletoperator = serialized(get_last_looser_on_court); function get_last_looser_on_court(admin, app, tkey, court_id) { return new Promise((resolve, reject) => { const tabletoperator_querry = { 'tournament_key': tkey, court: null }; @@ -1048,6 +1050,7 @@ module.exports = { date_str, fetch, time_str, + fetch_tabletoperator, // test only _integrate_umpires: integrate_umpires, }; diff --git a/static/js/announcements.js b/static/js/announcements.js index 269660d..d7b842b 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -24,11 +24,12 @@ function announcePreparationMatch(matchSetup) { var round = createRoundAnnouncement(matchSetup); var teams = createTeamAnnouncement(matchSetup); const umpire = createUmpire(matchSetup); + const tabletOperator = createTabletOperator(matchSetup); var lastPart = preparation; if (curt.preparation_meetingpoint_enabled) { lastPart = createMeetingPointAnnouncement(); } - announce([preparation, matchNumber, eventName, round, teams, umpire, lastPart]); + announce([preparation, matchNumber, eventName, round, teams, umpire, tabletOperator, lastPart]); } function announceSecondCallTeamOne(matchSetup) { if(!(window.localStorage.getItem('enable_announcements') === 'true')) { diff --git a/static/js/change.js b/static/js/change.js index 95c7a22..a452fc5 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -73,7 +73,7 @@ function default_handler_func(rerender, special_funcs, c) { 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', - 'preparation_meetingpoint_enabled']; + 'preparation_meetingpoint_enabled','preparation_tabletoperator_setup_enabled']; for (const cb_name of CHECKBOXES) { uiu.qsEach('input[name="' + cb_name + '"]', function(el) { el.checked = curt[cb_name]; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 6418703..337fddb 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -108,6 +108,7 @@ var ci18n_de = { 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', 'tournament:edit:preparation_meetingpoint_enabled': 'Meetingpoint für Vorbereitung nutzen', + 'tournament:edit:preparation_tabletoperator_setup_enabled': 'Tabletbediener in Vorbereitung aufrufen', 'tournament:edit:ticker_enabled': 'Ticker aktivieren', 'tournament:edit:ticker_url': 'Ticker-Adresse:', 'tournament:edit:ticker_password': 'Ticker-Passwort:', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index fa86a7d..aa434af 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -112,6 +112,7 @@ var ci18n_en = { 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', 'tournament:edit:preparation_meetingpoint_enabled': 'Use Meetingpoint for preparation', + 'tournament:edit:preparation_tabletoperator_setup_enabled': 'Call up tablet operator in preparation', 'tournament:edit:displays': 'Manage Displays:', 'tournament:edit:displays:num': 'Displaynumber', 'tournament:edit:displays:court': 'Ar Court', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index e0c9f0c..8f8b0f5 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -751,6 +751,7 @@ function ui_edit() { create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_state_enabled'); create_checkbox(curt, tablet_fieldset, 'tabletoperator_set_break_after_tabletservice'); create_checkbox(curt, tablet_fieldset, 'preparation_meetingpoint_enabled'); + create_checkbox(curt, tablet_fieldset, 'preparation_tabletoperator_setup_enabled'); if (!curt.tabletoperator_break_seconds) { curt.tabletoperator_break_seconds = 300; } @@ -791,6 +792,7 @@ function ui_edit() { tabletoperator_enabled: (!!data.tabletoperator_enabled), tabletoperator_break_seconds: data.tabletoperator_break_seconds, preparation_meetingpoint_enabled: (!!data.preparation_meetingpoint_enabled), + preparation_tabletoperator_setup_enabled: (!!data.preparation_tabletoperator_setup_enabled) }; send({ type: 'tournament_edit_props', From 67d3519ba3f962607596ebd7e66d43ffac29c6f1 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 25 May 2024 12:49:31 +0200 Subject: [PATCH 088/224] Bufgfix: Free announcements stops and crashes further announcements when more than 200 chars are used in text. --- static/js/ctournament.js | 1 + 1 file changed, 1 insertion(+) diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 8f8b0f5..72b5a4c 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -273,6 +273,7 @@ function render_announcement_formular(target) { name: 'custom_announcement', cols: '50', rows: '4', + maxlength: '175' }); const btp_fetch_btn = uiu.el(form, 'button', { 'class': 'match_save_button', From f1e8640239b6d435e8b11d12d6f14fb747b6e1ef Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 2 Jun 2024 19:36:36 +0200 Subject: [PATCH 089/224] Enhancement: Name of Tournament and URL ton onlineresults will now be generated as QR-Code on Upcoming Matches. Also Nam eof Tournament and Link will be published to Ticker on connect. Name of tournament will be read from BTP make deps is mandatory! --- bts/admin.js | 25 ++++++++++++++++---- bts/btp_sync.js | 51 ++++++++++++++++++++++++++++++++++------ bts/ticker_conn.js | 16 ++++++++++++- static/js/change.js | 1 + static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/cmatch.js | 7 ++++++ static/js/ctournament.js | 11 +++++++++ ticker/tupdate.js | 7 +++++- 9 files changed, 107 insertions(+), 13 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 5838e35..4101341 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -52,7 +52,7 @@ function handle_tournament_edit_props(app, ws, msg) { const key = msg.key; const props = utils.pluck(msg.props, [ - 'name', + 'name','tguid', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'btp_ip', 'btp_password', 'is_team', 'is_nation_competition', 'only_now_on_court', @@ -120,16 +120,33 @@ function handle_tournament_get(app, ws, msg) { return ws.respond(msg, {message: 'Missing key'}); } - app.db.tournaments.findOne({key: msg.key}, function(err, tournament) { + app.db.tournaments.findOne({ key: msg.key }, function (err, tournament) { if (!err && !tournament) { - err = {message: 'No tournament ' + msg.key}; + err = { message: 'No tournament ' + msg.key }; } if (err) { ws.respond(msg, err); return; } - async.parallel([ + function (cb) { + try { + const qrcode = require('qrcode'); + var url = ""; + if (tournament.ticker_enabled) { + url = "https://" + tournament.ticker_url.split("/")[2]; + } else { + url = "https://" + (tournament.btp_settings ? tournament.btp_settings.tournament_urn : "www.turnier.de") + "/tournament" + (tournament.tguid ? "/" + tournament.tguid + "/matches" : "s/"); + } + + qrcode.toDataURL(url, function (error, data) { + const qrCodeDataUrl = data; + tournament.mainQrCode = qrCodeDataUrl; + cb(error); + }); + } catch (error) + { } + }, function(cb) { stournament.get_courts(app.db, tournament.key, function(err, courts) { tournament.courts = courts; diff --git a/bts/btp_sync.js b/bts/btp_sync.js index eafb2aa..4ecc997 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -562,17 +562,54 @@ function integrate_courts(app, tournament_key, btp_state, callback) { } function integrate_btp_settings(app, tkey, btp_state, callback) { - let btp_settings = {}; - btp_settings.check_in_per_match = btp_state.btp_settings.get(1003).Value[0] ? false : true; - btp_settings.pause_duration_ms = btp_state.btp_settings.get(1303).Value[0] * 60 * 1000; + app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { + if (err) return callback(err); + var toChange = {}; + var changed = false; + if (!tournament.btpSettings) { + tournament.btpSettings = {}; + changed = true; + } + + const tournament_name = btp_state.btp_settings.get(1001).Value[0]; + const tournament_urn = btp_state.btp_settings.get(1008).Value[0]; + const check_in_per_match = btp_state.btp_settings.get(1003).Value[0] ? false : true; + const pause_duration_ms = btp_state.btp_settings.get(1303).Value[0] * 60 * 1000; - app.db.tournaments.update({key: tkey}, {$set: {btp_settings}}, {}, (err) => { - if (err) { - return callback(err); + + + if (tournament.btpSettings.tournament_name != tournament_name) { + tournament.btpSettings.tournament_name = tournament_name; + changed = true; + toChange.btpSettings = tournament.btpSettings; + toChange.name = tournament_name; + } + if (tournament.btpSettings.tournament_urn != tournament_urn) { + tournament.btpSettings.tournament_urn = tournament_urn; + changed = true; + toChange.btpSettings = tournament.btpSettings; } + if (tournament.btpSettings.check_in_per_match != check_in_per_match) { + tournament.btpSettings.check_in_per_match = check_in_per_match; + changed = true; + toChange.btpSettings = tournament.btpSettings; + } + if (tournament.btpSettings.pause_duration_ms != pause_duration_ms) { + tournament.btpSettings.pause_duration_ms = pause_duration_ms; + changed = true; + toChange.btpSettings = tournament.btpSettings; + } + + if (changed) { + app.db.tournaments.update({ key: tkey }, { $set: toChange }, {}, (err) => { + if (err) { + return callback(err); + } - return callback(null); + return callback(null); + }); + } }); } diff --git a/bts/ticker_conn.js b/bts/ticker_conn.js index 91b6079..449075b 100644 --- a/bts/ticker_conn.js +++ b/bts/ticker_conn.js @@ -160,7 +160,10 @@ class TickerConn { }, { collection: 'matches', query: {tournament_key}, - }], (err, db_courts, db_matches) => { + }, { + collection: 'tournaments', + query: { key: tournament_key}, + }], (err, db_courts, db_matches, db_tournaments) => { if (err) return cb(err); const interesting_ids = utils.filter_map(db_courts, c => c.match_id); @@ -183,9 +186,20 @@ class TickerConn { } } + + var tname = ""; + var turl = ""; + if (db_tournaments && db_tournaments.length == 1) { + + tname = db_tournaments[0].name; + turl = "https://" + (db_tournaments.btp_settings ? db_tournaments.btp_settings.tournament_urn : "www.turnier.de") +"/tournament" + (db_tournaments[0].tguid ? "/" + db_tournaments[0].tguid+"/matches" : "s/"); + } + return cb(null, { courts: db_courts.map(craft_court), matches: interesting_matches.map(craft_match), + tournament_name: tname, + tournament_url: turl }); }); } diff --git a/static/js/change.js b/static/js/change.js index a452fc5..aefeb4a 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -44,6 +44,7 @@ function default_handler_func(rerender, special_funcs, c) { case 'props': { curt.name = c.val.name; curt.is_team = c.val.is_team; + curt.tguid = c.val.tguid; curt.is_nation_competition = c.val.is_nation_competition; curt.only_now_on_court = c.val.only_now_on_court; curt.btp_timezone = c.val.btp_timezone; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 337fddb..da77f64 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -80,6 +80,7 @@ var ci18n_de = { 'tournament:edit:language': 'Sprache:', 'tournament:edit:language:auto': 'Nicht gesetzt (Browser-Einstellung)', 'tournament:edit:name': 'Name:', + 'tournament:edit:tguid': 'Turnier Guid:', 'tournament:edit:courts': 'Felder:', 'tournament:edit:dm_style': 'Standard-Ansicht:', 'tournament:edit:only_now_on_court': 'Spiele müssen auf Feld gezogen werden', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index aa434af..8314ccd 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -80,6 +80,7 @@ var ci18n_en = { 'tournament:edit:language': 'Language:', 'tournament:edit:language:auto': 'Not set (browser default)', 'tournament:edit:name': 'Name:', + 'tournament:edit:tguid': 'Tournament Guid:', 'tournament:edit:courts': 'Courts:', 'tournament:edit:dm_style': 'Default display style:', 'tournament:edit:only_now_on_court': 'Matches have to be dragged onto court', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index c165f7f..9c0ca41 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -1042,6 +1042,13 @@ function render_upcoming_matches(container) { }); render_match_row(tr, match, null, 'upcoming'); } + + const qr = uiu.el(container, 'img', { + type: 'img', + id: 'main_q_code_upcoming', + src: curt.mainQrCode, + style: 'position: absolute; right: 20px; bottom: 20px;' + }); } function render_finished(container) { diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 72b5a4c..2d5eb8d 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -470,6 +470,16 @@ function ui_edit() { 'class': 'ct_name', }); + + const name_tguid = uiu.el(form, 'label'); + uiu.el(name_tguid, 'span', {}, ci18n('tournament:edit:tguid')); + uiu.el(name_tguid, 'input', { + type: 'text', + name: 'tguid', + value: curt.tguid ? curt.tguid : "", + 'class': 'ct_tguid', + }); + // Tournament language selection const language_label = uiu.el(form, 'label'); uiu.el(language_label, 'span', {}, ci18n('tournament:edit:language')); @@ -767,6 +777,7 @@ function ui_edit() { form_utils.onsubmit(form, function(data) { const props = { name: data.name, + tguid: data.tguid, language: data.language, is_team: (!!data.is_team), is_nation_competition: (!!data.is_nation_competition), diff --git a/ticker/tupdate.js b/ticker/tupdate.js index 525c23a..3a71cc3 100644 --- a/ticker/tupdate.js +++ b/ticker/tupdate.js @@ -22,7 +22,12 @@ function handle_tset(app, ws, msg) { if (!_require_msg(ws, msg, ['event'])) { return; } - + if (msg.event.tournament_name) { + app.config.tournament_name = msg.event.tournament_name; + } + if (msg.event.tournament_url) { + app.config.note_html = "Alle Spiele auf
    Turnier.de"; + } tdata.set(app, msg.event, (err) => { if (err) { serror.silent('Failed tset: ' + err.message + ' ' + err.stack); From 7d5d97810d7c4506e19b00a478d802e8455f6816 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Mon, 3 Jun 2024 14:30:53 +0200 Subject: [PATCH 090/224] Bugfix: Wrong naming of a variable corrupts correct work of btp --- bts/btp_sync.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 4ecc997..7ca5203 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -567,8 +567,8 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { if (err) return callback(err); var toChange = {}; var changed = false; - if (!tournament.btpSettings) { - tournament.btpSettings = {}; + if (!tournament.btp_settings) { + tournament.btp_settings = {}; changed = true; } @@ -579,26 +579,26 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { - if (tournament.btpSettings.tournament_name != tournament_name) { - tournament.btpSettings.tournament_name = tournament_name; + if (tournament.btp_settings.tournament_name != tournament_name) { + tournament.btp_settings.tournament_name = tournament_name; changed = true; - toChange.btpSettings = tournament.btpSettings; + toChange.btp_settings = tournament.btp_settings; toChange.name = tournament_name; } - if (tournament.btpSettings.tournament_urn != tournament_urn) { - tournament.btpSettings.tournament_urn = tournament_urn; + if (tournament.btp_settings.tournament_urn != tournament_urn) { + tournament.btp_settings.tournament_urn = tournament_urn; changed = true; - toChange.btpSettings = tournament.btpSettings; + toChange.btp_settings = tournament.btp_settings; } - if (tournament.btpSettings.check_in_per_match != check_in_per_match) { - tournament.btpSettings.check_in_per_match = check_in_per_match; + if (tournament.btp_settings.check_in_per_match != check_in_per_match) { + tournament.btp_settings.check_in_per_match = check_in_per_match; changed = true; - toChange.btpSettings = tournament.btpSettings; + toChange.btp_settings = tournament.btp_settings; } - if (tournament.btpSettings.pause_duration_ms != pause_duration_ms) { - tournament.btpSettings.pause_duration_ms = pause_duration_ms; + if (tournament.btp_settings.pause_duration_ms != pause_duration_ms) { + tournament.btp_settings.pause_duration_ms = pause_duration_ms; changed = true; - toChange.btpSettings = tournament.btpSettings; + toChange.btp_settings = tournament.btp_settings; } if (changed) { @@ -609,6 +609,8 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { return callback(null); }); + } else { + return callback(null); } }); } From 1850004eb80bb440ebdfa37f3454e9b76ad8e2ad Mon Sep 17 00:00:00 2001 From: tengmel Date: Mon, 3 Jun 2024 20:52:31 +0200 Subject: [PATCH 091/224] Add missing dependencies for module qrcode --- package-lock.json | 535 ++++++++++++++++++++++++++++++++++++++++------ package.json | 1 + 2 files changed, 474 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index 02011ff..13e0a52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "nedb": "*", "node-xlsx": "^0.21.0", "pretty-data": "^0.40.0", + "qrcode": "^1.5.3", "request": "^2.88.0", "rimraf": "*", "serve-favicon": "*", @@ -867,6 +868,14 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", @@ -1016,6 +1025,104 @@ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "dev": true }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clone-regexp": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", @@ -1269,7 +1376,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -1413,6 +1519,11 @@ "node": ">=0.3.1" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "node_modules/dir-glob": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", @@ -1510,6 +1621,11 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -2335,7 +2451,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -4260,7 +4375,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", - "dev": true, "dependencies": { "p-try": "^2.0.0" }, @@ -4284,7 +4398,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "engines": { "node": ">=6" } @@ -4437,6 +4550,14 @@ "node": ">=6" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -4700,6 +4821,23 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -4999,7 +5137,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5007,8 +5144,7 @@ "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "node_modules/resolve": { "version": "1.11.1", @@ -5190,8 +5326,7 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "node_modules/set-value": { "version": "2.0.1", @@ -6385,8 +6520,7 @@ "node_modules/which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "node_modules/wide-align": { "version": "1.1.3", @@ -6545,8 +6679,28 @@ "node_modules/y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } }, "node_modules/yargs-parser": { "version": "13.0.0", @@ -6558,15 +6712,6 @@ "decamelize": "^1.2.0" } }, - "node_modules/yargs-parser/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/yargs-unparser": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", @@ -6581,15 +6726,6 @@ "node": ">=6" } }, - "node_modules/yargs-unparser/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/yargs-unparser/node_modules/cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", @@ -6643,6 +6779,105 @@ "decamelize": "^1.2.0" } }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/yauzl": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", @@ -7350,6 +7585,11 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, "camelcase-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", @@ -7481,6 +7721,82 @@ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", "dev": true }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "clone-regexp": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", @@ -7681,8 +7997,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decamelize-keys": { "version": "1.1.0", @@ -7791,6 +8106,11 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "dir-glob": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", @@ -7879,6 +8199,11 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, + "encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -8545,8 +8870,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-stdin": { "version": "7.0.0", @@ -10102,7 +10426,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", - "dev": true, "requires": { "p-try": "^2.0.0" } @@ -10119,8 +10442,7 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "parent-module": { "version": "1.0.1", @@ -10239,6 +10561,11 @@ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -10442,6 +10769,17 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "requires": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + } + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -10686,14 +11024,12 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, "resolve": { "version": "1.11.1", @@ -10848,8 +11184,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-value": { "version": "2.0.1", @@ -11838,8 +12173,7 @@ "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" }, "wide-align": { "version": "1.1.3", @@ -11963,8 +12297,99 @@ "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } }, "yargs-parser": { "version": "13.0.0", @@ -11974,14 +12399,6 @@ "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } } }, "yargs-unparser": { @@ -11995,12 +12412,6 @@ "yargs": "^12.0.5" }, "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", diff --git a/package.json b/package.json index 4a2a34f..882928a 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "nedb": "*", "node-xlsx": "^0.21.0", "pretty-data": "^0.40.0", + "qrcode": "^1.5.3", "request": "^2.88.0", "rimraf": "*", "serve-favicon": "*", From ae786a0bff2d41201282627f2cae4dde47f81933 Mon Sep 17 00:00:00 2001 From: tengmel Date: Mon, 3 Jun 2024 21:04:17 +0200 Subject: [PATCH 092/224] Enhancement: Linke also work with no connection to btp --- bts/admin.js | 2 +- bts/ticker_conn.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 4101341..44bffd2 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -136,7 +136,7 @@ function handle_tournament_get(app, ws, msg) { if (tournament.ticker_enabled) { url = "https://" + tournament.ticker_url.split("/")[2]; } else { - url = "https://" + (tournament.btp_settings ? tournament.btp_settings.tournament_urn : "www.turnier.de") + "/tournament" + (tournament.tguid ? "/" + tournament.tguid + "/matches" : "s/"); + url = "https://" + ((tournament.btp_settings && tournament.btp_settings.tournament_urn) ? tournament.btp_settings.tournament_urn : "www.turnier.de") + "/tournament" + (tournament.tguid ? "/" + tournament.tguid + "/matches" : "s/"); } qrcode.toDataURL(url, function (error, data) { diff --git a/bts/ticker_conn.js b/bts/ticker_conn.js index 449075b..8c9d94c 100644 --- a/bts/ticker_conn.js +++ b/bts/ticker_conn.js @@ -192,7 +192,7 @@ class TickerConn { if (db_tournaments && db_tournaments.length == 1) { tname = db_tournaments[0].name; - turl = "https://" + (db_tournaments.btp_settings ? db_tournaments.btp_settings.tournament_urn : "www.turnier.de") +"/tournament" + (db_tournaments[0].tguid ? "/" + db_tournaments[0].tguid+"/matches" : "s/"); + turl = "https://" + ((tournament.btp_settings && tournament.btp_settings.tournament_urn) ? db_tournaments.btp_settings.tournament_urn : "www.turnier.de") +"/tournament" + (db_tournaments[0].tguid ? "/" + db_tournaments[0].tguid+"/matches" : "s/"); } return cb(null, { From 3fd0803260aaeb8cef2044b7df7131768c248f20 Mon Sep 17 00:00:00 2001 From: tengmel Date: Tue, 4 Jun 2024 20:50:21 +0200 Subject: [PATCH 093/224] New Feature: If no Tournamentlogo is given a QR-Code will be dispaied on Displays --- bts/admin.js | 19 ++++++++++++------- bts/bupws.js | 18 ++++++++++++++++++ bts/http_api.js | 8 +++----- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 44bffd2..41a5446 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -115,6 +115,15 @@ function handle_courts_add(app, ws, msg) { }); } +function generate_tournament_web_url(tournament) { + var url = ""; + if (tournament.ticker_enabled) { + url = "https://" + tournament.ticker_url.split("/")[2]; + } else { + url = "https://" + ((tournament.btp_settings && tournament.btp_settings.tournament_urn) ? tournament.btp_settings.tournament_urn : "www.turnier.de") + "/tournament" + (tournament.tguid ? "/" + tournament.tguid + "/matches" : "s/"); + } + return url; +} function handle_tournament_get(app, ws, msg) { if (! msg.key) { return ws.respond(msg, {message: 'Missing key'}); @@ -132,13 +141,8 @@ function handle_tournament_get(app, ws, msg) { function (cb) { try { const qrcode = require('qrcode'); - var url = ""; - if (tournament.ticker_enabled) { - url = "https://" + tournament.ticker_url.split("/")[2]; - } else { - url = "https://" + ((tournament.btp_settings && tournament.btp_settings.tournament_urn) ? tournament.btp_settings.tournament_urn : "www.turnier.de") + "/tournament" + (tournament.tguid ? "/" + tournament.tguid + "/matches" : "s/"); - } - + + const url = generate_tournament_web_url(tournament); qrcode.toDataURL(url, function (error, data) { const qrCodeDataUrl = data; tournament.mainQrCode = qrCodeDataUrl; @@ -687,6 +691,7 @@ module.exports = { handle_relocate_display, handle_change_display_mode, notify_change, + generate_tournament_web_url, on_close, on_connect, }; \ No newline at end of file diff --git a/bts/bupws.js b/bts/bupws.js index 33f1d2f..bf8a74e 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -394,6 +394,24 @@ function create_event_representation(tournament) { if (tournament.logo_id) { res.tournament_logo_url = `/h/${encodeURIComponent(tournament.key)}/logo/${tournament.logo_id}`; } + else { + try { + const fs = require('fs'); + const path = require('path'); + const d = new Date(); + const datestring = d.toISOString().slice(0, 10); + const filename = "logo/" + datestring +"_"+tournament._id + ".png"; + const filepath = path.join(utils.root_dir(), 'data', 'logos', datestring +"_"+tournament._id + ".png"); + if (!fs.existsSync(filepath)) { + const qrcode = require('qrcode'); + const url = admin.generate_tournament_web_url(tournament); + qrcode.toFile(filepath, url, { scale: 7, errorCorrectionLevel: 'H' }, function (error) { }); + } + res.tournament_logo_url = `/h/${encodeURIComponent(tournament.key)}/${filename}`; + } catch (error) { + console.log("A error occured during generating QR-Code for displays"); + } + } res.tournament_logo_background_color = tournament.logo_background_color || '#000000'; res.tournament_logo_foreground_color = tournament.logo_foreground_color || '#aaaaaa'; return res; diff --git a/bts/http_api.js b/bts/http_api.js index 13d3513..a543d6c 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -670,8 +670,7 @@ function logo_handler(req, res) { const {tournament_key, logo_id} = req.params; assert(tournament_key); assert(logo_id); - const m = /^[-0-9a-f]+\.(gif|png|jpg|jpeg|svg|webp)$/.exec(logo_id); - assert(m, `Invalid logo ${logo_id}`); + const filetype = logo_id.split(".")[1]; const mime = { gif: 'image/gif', png: 'image/png', @@ -679,9 +678,8 @@ function logo_handler(req, res) { jpeg: 'image/jpeg', svg: 'image/svg+xml', webp: 'image/webp', - }[m[1]]; - assert(mime, `Unsupported ext ${JSON.stringify(m[1])}`); - + }[filetype]; + assert(mime, `Unsupported ext ${JSON.stringify(filetype)}`); const fn = path.join(utils.root_dir(), 'data', 'logos', path.basename(logo_id)); res.setHeader('Content-Type', mime); res.setHeader('Cache-Control', 'public, max-age=31536000'); From 59f94b7086b9e007b1002fcf53535421405f9c49 Mon Sep 17 00:00:00 2001 From: tengmel Date: Wed, 5 Jun 2024 22:01:42 +0200 Subject: [PATCH 094/224] Bugfix: Copy and Paste error crashes BTS when Ticker is active --- bts/ticker_conn.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bts/ticker_conn.js b/bts/ticker_conn.js index 8c9d94c..5ff353c 100644 --- a/bts/ticker_conn.js +++ b/bts/ticker_conn.js @@ -190,9 +190,9 @@ class TickerConn { var tname = ""; var turl = ""; if (db_tournaments && db_tournaments.length == 1) { - - tname = db_tournaments[0].name; - turl = "https://" + ((tournament.btp_settings && tournament.btp_settings.tournament_urn) ? db_tournaments.btp_settings.tournament_urn : "www.turnier.de") +"/tournament" + (db_tournaments[0].tguid ? "/" + db_tournaments[0].tguid+"/matches" : "s/"); + const tournament = db_tournaments[0]; + tname = tournament.name; + turl = "https://" + ((tournament.btp_settings && tournament.btp_settings.tournament_urn) ? tournament.btp_settings.tournament_urn : "www.turnier.de") + "/tournament" + (tournament.tguid ? "/" + tournament.tguid+"/matches" : "s/"); } return cb(null, { From 98a9fa8416b9619bf5855030afad5a37b723087f Mon Sep 17 00:00:00 2001 From: tengmel Date: Thu, 6 Jun 2024 21:40:44 +0200 Subject: [PATCH 095/224] Enhanecment: Speedup of the voice 12,5% --- static/js/announcements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/announcements.js b/static/js/announcements.js index d7b842b..7187532 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -253,7 +253,7 @@ function announce(callArray) { if (part && part != null) { var words = new SpeechSynthesisUtterance(part); words.lang = ci18n('announcements:lang'); - words.rate = 1; + words.rate = 1.125; words.pitch = 0; words.volume = 1; words.voice = voice; From 4942ed4b136166486136d24c6088c339f663273a Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 8 Jun 2024 21:32:18 +0200 Subject: [PATCH 096/224] Enhencement: Accept also "-" as delimiter for Events --- static/js/announcements.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/announcements.js b/static/js/announcements.js index 7187532..34cb0c0 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -146,7 +146,7 @@ function createRoundAnnouncement(matchSetup) { return round; } function createEventAnnouncement(matchSetup) { - var eventParts = matchSetup.event_name.split(" "); + var eventParts = matchSetup.event_name.replaceAll("-"," ").split(" "); var eventName = ""; if (eventParts[0] == 'JE') { eventName = ci18n('announcements:boys_singles'); @@ -253,7 +253,7 @@ function announce(callArray) { if (part && part != null) { var words = new SpeechSynthesisUtterance(part); words.lang = ci18n('announcements:lang'); - words.rate = 1.125; + words.rate = 1.05; words.pitch = 0; words.volume = 1; words.voice = voice; From f662eb98eff8c0a2d2f8745af45b761f364f9abc Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 9 Jun 2024 12:02:41 +0200 Subject: [PATCH 097/224] New Function: Now it is possibe to admister the default displaysettings for an tournament. When the default displasetting will change also all displays which uses this settings will be reinitialized automatically --- bts/admin.js | 38 +- bts/bupws.js | 110 +- bts/stournament.js | 4 +- static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/ctournament.js | 2183 +++++++++++++++++++------------------- 6 files changed, 1209 insertions(+), 1128 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 41a5446..487eba3 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -58,7 +58,8 @@ function handle_tournament_edit_props(app, ws, msg) { 'is_team', 'is_nation_competition', 'only_now_on_court', 'warmup', 'warmup_ready', 'warmup_start', 'ticker_enabled', 'ticker_url', 'ticker_password', - 'language', 'dm_style', 'tabletoperator_enabled','tabletoperator_break_seconds', + 'language', 'dm_style', 'displaysettings_general', + 'tabletoperator_enabled', 'tabletoperator_break_seconds', 'tabletoperator_set_break_after_tabletservice','tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', @@ -68,21 +69,32 @@ function handle_tournament_edit_props(app, ws, msg) { if (msg.props.btp_timezone) { props.btp_timezone = msg.props.btp_timezone === 'system' ? undefined : msg.props.btp_timezone; } - - app.db.tournaments.update({key}, {$set: props}, {returnUpdatedDocs: true}, function(err, num, t) { - if (err) { + app.db.tournaments.findOne({ key }, async (err, tournament) => { + if (err || !tournament) { ws.respond(msg, err); return; } - if (utils.has_key(props, k => /^btp_/.test(k))) { - btp_manager.reconfigure(app, t); - } - if (utils.has_key(props, k => /^ticker_/.test(k))) { - ticker_manager.reconfigure(app, t); - } - notify_change(app, key, 'props', t); + app.db.tournaments.update({ key }, { $set: props }, { returnUpdatedDocs: true }, function (err, num, t) { + if (err) { + ws.respond(msg, err); + return; + } + if (utils.has_key(props, k => /^btp_/.test(k))) { + btp_manager.reconfigure(app, t); + } + if (utils.has_key(props, k => /^ticker_/.test(k))) { + ticker_manager.reconfigure(app, t); + } + notify_change(app, key, 'props', t); - ws.respond(msg, err); + if (!tournament.displaysettings_general || (tournament.displaysettings_general != t.displaysettings_general)){ + + const bupws = require('./bupws'); + bupws.change_default_display_mode(app, t, tournament.displaysettings_general, t.displaysettings_general); + } + + ws.respond(msg, err); + }); }); } @@ -172,7 +184,7 @@ function handle_tournament_get(app, ws, msg) { cb(err); }); }, function (cb) { - stournament.get_displays(app, tournament.key, function (err, displays) { + stournament.get_displays(app, tournament, function (err, displays) { tournament.displays = displays; cb(err); }); diff --git a/bts/bupws.js b/bts/bupws.js index bf8a74e..7fcb58a 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -7,6 +7,8 @@ const admin = require('./admin'); const all_panels = []; +const default_tournament_key = 'default'; +const default_displaysettings_key = default_tournament_key; function on_close(app, ws) { if (!utils.remove(all_panels, ws)) { serror.silent('Removing Scoreboard ws, but it was not connected!?'); @@ -20,13 +22,22 @@ async function on_connect(app, ws) { } async function notify_admin_display_status_changed(app, ws, ws_online) { - const client_id = determine_client_id(ws); - var display_court_displaysetting = await get_display_court_displaysettings(app, client_id); - if (display_court_displaysetting == null) { - display_court_displaysetting = create_display_court_displaysettings(client_id, null, "default"); - } - display_court_displaysetting.online = ws_online; - admin.notify_change(app, 'default', 'display_status_changed', { 'display_court_displaysetting': display_court_displaysetting }); + app.db.tournaments.findOne({ key: default_tournament_key }, async (err, tournament) => { + if (!err && !tournament) { + err = { message: 'No tournament ' + default_tournament_key }; + } + const client_id = determine_client_id(ws); + var display_court_displaysetting = await get_display_court_displaysettings(app, client_id); + if (display_court_displaysetting == null) { + display_court_displaysetting = create_display_court_displaysettings(client_id, null, generate_default_displaysettings_id(tournament)); + } + display_court_displaysetting.online = ws_online; + admin.notify_change(app, default_tournament_key, 'display_status_changed', { 'display_court_displaysetting': display_court_displaysetting }); + }); +} + +function generate_default_displaysettings_id(tournament) { + return tournament.displaysettings_general ? tournament.displaysettings_general : default_displaysettings_key; } function notify_change(tournament_key, court_id, ctype, val) { @@ -120,10 +131,10 @@ async function handle_init(app, ws, msg) { } } -async function initialize_client(ws, app, tournament_key, court_id) { +async function initialize_client(ws, app, tournament_key, court_id, displaysetting) { const client_id = determine_client_id(ws); if (client_id) { - let display_setting = await get_display_setting(app, tournament_key, client_id, court_id) + let display_setting = await get_display_setting(app, tournament_key, client_id, court_id, displaysetting) if (display_setting != null) { ws.court_id = display_setting.court_id; court_id = display_setting.court_id; @@ -208,7 +219,7 @@ function get_display_court_displaysettings(app, client_id) { }); }); } -function get_display_setting(app, tkey, client_id, court_id) { +function get_display_setting(app, tkey, client_id, court_id, displaysetting) { return new Promise((resolve, reject) => { const display_court_query = { 'client_id': client_id }; app.db.display_court_displaysettings.find(display_court_query).limit(1).exec((err, display_court_displaysetting) => { @@ -230,17 +241,26 @@ function get_display_setting(app, tkey, client_id, court_id) { resolve(returnvalue); }); } else { - const display_query_default = { 'id': 'default' }; - app.db.displaysettings.find(display_query_default).limit(1).exec((err, display_setting_default) => { - if (err) { + app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { + if (err || !tournament) { return reject(err); } - if (display_setting_default.length == 1) { - returnvalue = display_setting_default[0]; - returnvalue.court_id = court_id; - returnvalue.displaymode_court_id = court_id; - } - resolve(returnvalue); + var displaysetting_id = generate_default_displaysettings_id(tournament); + if (displaysetting) { + displaysetting_id = displaysetting; + } + const display_query_default = { 'id': displaysetting_id }; + app.db.displaysettings.find(display_query_default).limit(1).exec((err, display_setting_default) => { + if (err) { + return reject(err); + } + if (display_setting_default.length == 1) { + returnvalue = display_setting_default[0]; + returnvalue.court_id = court_id; + returnvalue.displaymode_court_id = court_id; + } + resolve(returnvalue); + }); }); } }); @@ -420,13 +440,16 @@ function create_event_representation(tournament) { async function restart_panel(app, tournament_key, client_id, new_court_id) { var client_court_displaysetting = null; if (new_court_id) { - + if (new_court_id == "--") { + new_court_id = undefined; + } + const updatevalues = { court_id: new_court_id } client_court_displaysetting = await update_client_court_displaysetting(app, client_id, updatevalues); } - var display_online = reinitialize_panel(app, tournament_key,client_id, new_court_id); + var display_online = reinitialize_panel(app, tournament_key, client_id, new_court_id, undefined); if (client_court_displaysetting != null) { client_court_displaysetting.online = display_online; admin.notify_change(app, tournament_key, 'display_status_changed', { 'display_court_displaysetting': client_court_displaysetting }); @@ -440,31 +463,53 @@ async function change_display_mode(app, tournament_key, client_id, new_displayse displaysetting_id: new_displaysettings_id } const client_court_displaysetting = await update_client_court_displaysetting(app, client_id, updatevalues); - var display_online = reinitialize_panel(app, tournament_key, client_id,null); - client_court_displaysetting.online = display_online; - admin.notify_change(app, tournament_key, 'display_status_changed', { 'display_court_displaysetting': client_court_displaysetting }); + var display_online = reinitialize_panel(app, tournament_key, client_id, null, new_displaysettings_id); + if (client_court_displaysetting) { + client_court_displaysetting.online = display_online; + admin.notify_change(app, tournament_key, 'display_status_changed', { 'display_court_displaysetting': client_court_displaysetting }); + } } } +async function change_default_display_mode(app, tournament, old_displaysettings_id, new_displaysettings_id) { + if (new_displaysettings_id) { + app.db.display_court_displaysettings.find({ displaysetting_id: old_displaysettings_id }).exec( async (err, display_court_displaysettings) => { + if (err) { + return reject(err); + } + const updatevalues = { + displaysetting_id: new_displaysettings_id + } + for (const displaysettings of display_court_displaysettings) { + const client_court_displaysetting = await update_client_court_displaysetting(app, displaysettings.client_id, updatevalues); + if (client_court_displaysetting) { + var display_online = reinitialize_panel(app, tournament.key, displaysettings.client_id, null, undefined); -function reinitialize_panel(app, tournament_key, client_id, new_court_id) { + } + } + for (const panel_ws of all_panels) { + restart_panel(app, tournament.key, panel_ws.client_id); + } + }); + } +} + + + +function reinitialize_panel(app, tournament_key, client_id, new_court_id, displaysetting) { for (const panel_ws of all_panels) { const ws_client_id = determine_client_id(panel_ws); if (client_id == ws_client_id) { if (new_court_id != null) { panel_ws.court_id = new_court_id; } - - initialize_client(panel_ws, app, tournament_key, panel_ws.court_id); - // Two times to make it work, no idea why - //matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); - //matches_handler(app, panel_ws, tournament_key, panel_ws.court_id); + initialize_client(panel_ws, app, tournament_key, panel_ws.court_id, displaysetting); return true; } } return false;; } -function add_display_status(app, tournament_key, displays) { +function add_display_status(app, tournament, displays) { for (const d of displays) { d.online = false; for (const panel_ws of all_panels) { @@ -484,7 +529,7 @@ function add_display_status(app, tournament_key, displays) { } } if (!found) { - const client_court_displaysetting = create_display_court_displaysettings(ws_client_id, panel_ws.court_id, "default"); + const client_court_displaysetting = create_display_court_displaysettings(ws_client_id, panel_ws.court_id, generate_default_displaysettings_id(tournament)); client_court_displaysetting.online = true; displays[displays.length] = client_court_displaysetting; } @@ -501,5 +546,6 @@ module.exports = { handle_reset_display_settings, restart_panel, change_display_mode, + change_default_display_mode, add_display_status, }; \ No newline at end of file diff --git a/bts/stournament.js b/bts/stournament.js index ef8a079..3af1a45 100644 --- a/bts/stournament.js +++ b/bts/stournament.js @@ -38,7 +38,7 @@ function get_tabletoperators(db, tournament_key, callback) { }); } -function get_displays(app, tournament_key, callback) { +function get_displays(app, tournament, callback) { app.db.display_court_displaysettings.find({}, function (err, display_court_displaysettings) { if (err) return callback(err); @@ -48,7 +48,7 @@ function get_displays(app, tournament_key, callback) { }); const bupws = require('./bupws'); - bupws.add_display_status(app, tournament_key, display_court_displaysettings); + bupws.add_display_status(app, tournament, display_court_displaysettings); display_court_displaysettings = display_court_displaysettings.sort(utils.cmp_key('client_id')); return callback(err, display_court_displaysettings); }); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index da77f64..32b3a57 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -83,6 +83,7 @@ var ci18n_de = { 'tournament:edit:tguid': 'Turnier Guid:', 'tournament:edit:courts': 'Felder:', 'tournament:edit:dm_style': 'Standard-Ansicht:', + 'tournament:edit:displaysettings_general': 'Standard-Displayeinstellung:', 'tournament:edit:only_now_on_court': 'Spiele müssen auf Feld gezogen werden', 'tournament:edit:warmup_timer_behavior': 'Verhalten des Vorbereitungs-Countdowns:', 'tournament:edit:warmup_timer_behavior:bwf-2016': 'BWF ab 2016 (ab Auslosung)', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 8314ccd..944e988 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -83,6 +83,7 @@ var ci18n_en = { 'tournament:edit:tguid': 'Tournament Guid:', 'tournament:edit:courts': 'Courts:', 'tournament:edit:dm_style': 'Default display style:', + 'tournament:edit:displaysettings_general': 'Default displaysetting:', 'tournament:edit:only_now_on_court': 'Matches have to be dragged onto court', 'tournament:edit:warmup_timer_behavior': 'Behaviour of the preparation countdown:', 'tournament:edit:warmup_timer_behavior:bwf-2016': 'BWF 2016+ (after choice of side)', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 2d5eb8d..e99c028 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -3,638 +3,616 @@ var curt; // current tournament var ctournament = (function() { -function _route_single(rex, func, handler) { - if (!handler) { - handler = change.default_handler(func); - } - - crouting.register(rex, function(m) { - switch_tournament(m[1], func); - }, handler); -} - -function switch_tournament(tournament_key, success_cb) { - send({ - type: 'tournament_get', - key: tournament_key, - }, function(err, response) { - if (err) { - return cerror.net(err); + function _route_single(rex, func, handler) { + if (!handler) { + handler = change.default_handler(func); } - curt = response.tournament; - if (curt.language && curt.language !== 'auto') { - ci18n.switch_language(curt.language); - } - uiu.text_qs('.btp_status', 'BTP status: ' + curt.btp_status); - uiu.text_qs('.ticker_status', 'Ticker status: ' + curt.ticker_status); - success_cb(); - }); -} + crouting.register(rex, function (m) { + switch_tournament(m[1], func); + }, handler); + } -function ui_create() { - const main = uiu.qs('.main'); - - uiu.empty(main); - const form = uiu.el(main, 'form'); - uiu.el(form, 'h2', {}, ci18n('Create tournament')); - const id_label = uiu.el(form, 'label', {}, ci18n('create:id:label')); - const key_input = uiu.el(id_label, 'input', { - type: 'text', - name: 'key', - autofocus: 'autofocus', - required: 'required', - pattern: '^[a-z0-9]+$', - }); - uiu.el(form, 'button', { - role: 'submit', - }, ci18n('Create tournament')); - key_input.focus(); - - form_utils.onsubmit(form, function(data) { + function switch_tournament(tournament_key, success_cb) { send({ - type: 'create_tournament', - key: data.key, - }, function(err) { - if (err) return cerror.net(err); + type: 'tournament_get', + key: tournament_key, + }, function (err, response) { + if (err) { + return cerror.net(err); + } - uiu.remove(form); - switch_tournament(data.key, ui_show); + curt = response.tournament; + if (curt.language && curt.language !== 'auto') { + ci18n.switch_language(curt.language); + } + uiu.text_qs('.btp_status', 'BTP status: ' + curt.btp_status); + uiu.text_qs('.ticker_status', 'Ticker status: ' + curt.ticker_status); + success_cb(); }); - }); -} + } -function ui_list() { - crouting.set('t/'); - toprow.set([{ - label: ci18n('Tournaments'), - func: ui_list, - }]); - - send({ - type: 'tournament_list', - }, function(err, response) { - if (err) { - return cerror.net(err); - } - list_show(response.tournaments); - }); -} -crouting.register(/^t\/$/, ui_list, change.default_handler); - -function list_show(tournaments) { - const main = uiu.qs('.main'); - uiu.empty(main); - uiu.el(main, 'h1', {}, 'Tournaments'); - tournaments.forEach(function(t) { - const link = uiu.el(main, 'div', 'vlink', t.name || t.key); - link.addEventListener('click', function() { - switch_tournament(t.key, ui_show); + function ui_create() { + const main = uiu.qs('.main'); + + uiu.empty(main); + const form = uiu.el(main, 'form'); + uiu.el(form, 'h2', {}, ci18n('Create tournament')); + const id_label = uiu.el(form, 'label', {}, ci18n('create:id:label')); + const key_input = uiu.el(id_label, 'input', { + type: 'text', + name: 'key', + autofocus: 'autofocus', + required: 'required', + pattern: '^[a-z0-9]+$', }); - }); + uiu.el(form, 'button', { + role: 'submit', + }, ci18n('Create tournament')); + key_input.focus(); - const create_btn = uiu.el(main, 'button', { - role: 'button', - }, 'Create tournament ...'); - create_btn.addEventListener('click', ui_create); -} + form_utils.onsubmit(form, function (data) { + send({ + type: 'create_tournament', + key: data.key, + }, function (err) { + if (err) return cerror.net(err); -function update_score(c) { - const cval = c.val; - const match_id = cval.match_id; + uiu.remove(form); + switch_tournament(data.key, ui_show); + }); + }); + } + + function ui_list() { + crouting.set('t/'); + toprow.set([{ + label: ci18n('Tournaments'), + func: ui_list, + }]); + + send({ + type: 'tournament_list', + }, function (err, response) { + if (err) { + return cerror.net(err); + } + list_show(response.tournaments); + }); + } + crouting.register(/^t\/$/, ui_list, change.default_handler); + + function list_show(tournaments) { + const main = uiu.qs('.main'); + uiu.empty(main); + uiu.el(main, 'h1', {}, 'Tournaments'); + tournaments.forEach(function (t) { + const link = uiu.el(main, 'div', 'vlink', t.name || t.key); + link.addEventListener('click', function () { + switch_tournament(t.key, ui_show); + }); + }); - // Find the match - const m = utils.find(curt.matches, m => m._id === match_id); - if (!m) { - cerror.silent('Cannot find match to update score, ID: ' + JSON.stringify(match_id)); - return; + const create_btn = uiu.el(main, 'button', { + role: 'button', + }, 'Create tournament ...'); + create_btn.addEventListener('click', ui_create); } - const old_section = cmatch.calc_section(m); - m.network_score = cval.network_score; - m.presses = cval.presses; - m.team1_won = cval.team1_won; - m.shuttle_count = cval.shuttle_count; - const new_section = cmatch.calc_section(m); - - if (old_section === new_section) { - cmatch.update_match_score(m); - } else { - if(new_section == 'finished' || new_section == 'unassigned'){ - m.setup.now_on_court = false; - } - else{ - m.setup.now_on_court = true; + function update_score(c) { + const cval = c.val; + const match_id = cval.match_id; + + // Find the match + const m = utils.find(curt.matches, m => m._id === match_id); + if (!m) { + cerror.silent('Cannot find match to update score, ID: ' + JSON.stringify(match_id)); + return; } - cmatch.update_match(m, old_section, new_section); - } -} -function update_player_status(c){ - const cval = c.val; - const match_id = cval.match__id; + const old_section = cmatch.calc_section(m); + m.network_score = cval.network_score; + m.presses = cval.presses; + m.team1_won = cval.team1_won; + m.shuttle_count = cval.shuttle_count; + const new_section = cmatch.calc_section(m); - // Find the match - const m = utils.find(curt.matches, m => m._id === match_id); - if (!m) { - cerror.silent('Cannot find match to update player status, ID: ' + JSON.stringify(match_id)); - return; + if (old_section === new_section) { + cmatch.update_match_score(m); + } else { + if (new_section == 'finished' || new_section == 'unassigned') { + m.setup.now_on_court = false; + } + else { + m.setup.now_on_court = true; + } + cmatch.update_match(m, old_section, new_section); + } } - m.btp_winner = cval.btp_winner; - m.setup = cval.setup; - cmatch.update_players(m); -} + function update_player_status(c) { + const cval = c.val; + const match_id = cval.match__id; -function update_match(c){ - const cval = c.val; - const match_id = cval.match__id; + // Find the match + const m = utils.find(curt.matches, m => m._id === match_id); + if (!m) { + cerror.silent('Cannot find match to update player status, ID: ' + JSON.stringify(match_id)); + return; + } + m.btp_winner = cval.btp_winner; + m.setup = cval.setup; - // Find the match - const m = utils.find(curt.matches, m => m._id === match_id); - if (!m) { - cerror.silent('Cannot find match to update, ID: ' + JSON.stringify(match_id)); - return; + cmatch.update_players(m); } - const old_section = cmatch.calc_section(m); - m.network_score = cval.match.network_score; - m.presses = cval.match.presses; - m.team1_won = cval.match.team1_won; - m.shuttle_count = cval.match.shuttle_count; - m.setup = cval.match.setup; - m.btp_winner = cval.match.btp_winner; - const new_section = cmatch.calc_section(m); - cmatch.update_match(m, old_section, new_section); -} -function update_upcoming_match(c){ - const cval = c.val; - const match_id = cval.match__id; + function update_match(c) { + const cval = c.val; + const match_id = cval.match__id; - // Find the match - const m = utils.find(curt.matches, m => m._id === match_id); - if (!m) { - cerror.silent('Cannot find match to update, ID: ' + JSON.stringify(match_id)); - return; - } - const old_section = cmatch.calc_section(m); - m.network_score = cval.match.network_score; - m.presses = cval.match.presses; - m.team1_won = cval.match.team1_won; - m.shuttle_count = cval.match.shuttle_count; - m.setup = cval.match.setup; - m.btp_winner = cval.match.btp_winner; - const new_section = cmatch.calc_section(m); - cmatch.update_match(m, old_section, new_section); - - console.log(new_section); - - if(old_section != new_section || new_section == 'unassigned') { - uiu.qsEach('.upcoming_container', (upcoming_container) => { - cmatch.render_upcoming_matches(upcoming_container); - }); + // Find the match + const m = utils.find(curt.matches, m => m._id === match_id); + if (!m) { + cerror.silent('Cannot find match to update, ID: ' + JSON.stringify(match_id)); + return; + } + const old_section = cmatch.calc_section(m); + m.network_score = cval.match.network_score; + m.presses = cval.match.presses; + m.team1_won = cval.match.team1_won; + m.shuttle_count = cval.match.shuttle_count; + m.setup = cval.match.setup; + m.btp_winner = cval.match.btp_winner; + const new_section = cmatch.calc_section(m); + cmatch.update_match(m, old_section, new_section); } -} -function tabletoperator_add(c) { - curt.tabletoperators.push(c.val.tabletoperator); + function update_upcoming_match(c) { + const cval = c.val; + const match_id = cval.match__id; - _show_render_tabletoperators(); -} + // Find the match + const m = utils.find(curt.matches, m => m._id === match_id); + if (!m) { + cerror.silent('Cannot find match to update, ID: ' + JSON.stringify(match_id)); + return; + } + const old_section = cmatch.calc_section(m); + m.network_score = cval.match.network_score; + m.presses = cval.match.presses; + m.team1_won = cval.match.team1_won; + m.shuttle_count = cval.match.shuttle_count; + m.setup = cval.match.setup; + m.btp_winner = cval.match.btp_winner; + const new_section = cmatch.calc_section(m); + cmatch.update_match(m, old_section, new_section); + + console.log(new_section); -function tabletoperator_removed(c) { - const changed_t = utils.find(curt.tabletoperators, m => m._id === c.val.tabletoperator._id); - if (changed_t) { - changed_t.court = c.val.tabletoperator.court; + if (old_section != new_section || new_section == 'unassigned') { + uiu.qsEach('.upcoming_container', (upcoming_container) => { + cmatch.render_upcoming_matches(upcoming_container); + }); + } } - _show_render_tabletoperators(); -} + function tabletoperator_add(c) { + curt.tabletoperators.push(c.val.tabletoperator); -function update_current_match(c) { - //change.change_current_match(c.val); - update_match(c); -} + _show_render_tabletoperators(); + } -function update_upcoming_current_match(c) { - //change.change_current_match(c.val); - update_upcoming_match(c); -} + function tabletoperator_removed(c) { + const changed_t = utils.find(curt.tabletoperators, m => m._id === c.val.tabletoperator._id); + if (changed_t) { + changed_t.court = c.val.tabletoperator.court; + } -function _update_all_ui_elements() { - _show_render_matches(); - _show_render_tabletoperators(); + _show_render_tabletoperators(); + } -} -function _update_all_ui_elements_upcoming() { - cmatch.render_courts(uiu.qs('.courts_container'), 'public'); - cmatch.render_upcoming_matches(uiu.qs('.upcoming_container')); -} + function update_current_match(c) { + //change.change_current_match(c.val); + update_match(c); + } -function _show_render_matches() { - cmatch.render_courts(uiu.qs('.courts_container')); - cmatch.render_unassigned(uiu.qs('.unassigned_container')); - cmatch.render_finished(uiu.qs('.finished_container')); -} -function _show_render_tabletoperators() { - ctabletoperator.render_unassigned(uiu.qs('.unassigned_tableoperators_container')); -} + function update_upcoming_current_match(c) { + //change.change_current_match(c.val); + update_upcoming_match(c); + } + function _update_all_ui_elements() { + _show_render_matches(); + _show_render_tabletoperators(); -function ui_btp_fetch() { - send({ - type: 'btp_fetch', - tournament_key: curt.key, - }, err => { - if (err) { - return cerror.net(err); - } - }); -} + } + function _update_all_ui_elements_upcoming() { + cmatch.render_courts(uiu.qs('.courts_container'), 'public'); + cmatch.render_upcoming_matches(uiu.qs('.upcoming_container')); + } + + function _show_render_matches() { + cmatch.render_courts(uiu.qs('.courts_container')); + cmatch.render_unassigned(uiu.qs('.unassigned_container')); + cmatch.render_finished(uiu.qs('.finished_container')); + } + function _show_render_tabletoperators() { + ctabletoperator.render_unassigned(uiu.qs('.unassigned_tableoperators_container')); + } -function ui_ticker_push() { - send({ - type: 'ticker_reset', - tournament_key: curt.key, - }, err => { - if (err) { - return cerror.net(err); - } - }); -} -function render_announcement_formular(target) { - const announcements = uiu.el(target, 'div', 'announcements_container'); - const heading = uiu.el(announcements, 'h3', {}, 'Freie Ansage'); - const form = uiu.el(announcements, 'form'); - uiu.el(form, 'textarea', { - type: 'textarea', - id: 'custom_announcement', - name: 'custom_announcement', - cols: '50', - rows: '4', - maxlength: '175' - }); - const btp_fetch_btn = uiu.el(form, 'button', { - 'class': 'match_save_button', - role: 'submit', - }, 'Ansage abspielen'); - form_utils.onsubmit(form, function (d) { - //announce([d.custom_announcement]); + function ui_btp_fetch() { send({ - type: 'free_announce', + type: 'btp_fetch', tournament_key: curt.key, - text: d.custom_announcement, - }, function (err) { + }, err => { if (err) { return cerror.net(err); } }); - }); -} + } -function render_enable_announcement(target) { - const announcements = uiu.el(target, 'div', 'enable_announcements_container'); - const heading = uiu.el(announcements, 'h3', {}, 'Ansagen auf diesem Gerät'); - const form = uiu.el(announcements, 'form'); - const enable_announcements =uiu.el(form, 'input', { - type: 'checkbox', - id: 'enable_announcements', - name: 'enable_announcements' - }); - - enable_announcements.checked = (window.localStorage.getItem('enable_announcements') === 'true'); - uiu.el(form, 'label', {for: 'enable_announcements'}, 'Ansagen auf diesem Gerät aktivieren'); - enable_announcements.addEventListener('change', change_announcements); -} + function ui_ticker_push() { + send({ + type: 'ticker_reset', + tournament_key: curt.key, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } -function change_announcements(e) { - let enable_announcements = document.getElementById('enable_announcements'); - console.log(enable_announcements.checked); - window.localStorage.setItem('enable_announcements', enable_announcements.checked); + function render_announcement_formular(target) { + const announcements = uiu.el(target, 'div', 'announcements_container'); + const heading = uiu.el(announcements, 'h3', {}, 'Freie Ansage'); + const form = uiu.el(announcements, 'form'); + uiu.el(form, 'textarea', { + type: 'textarea', + id: 'custom_announcement', + name: 'custom_announcement', + cols: '50', + rows: '4', + maxlength: '175' + }); + const btp_fetch_btn = uiu.el(form, 'button', { + 'class': 'match_save_button', + role: 'submit', + }, 'Ansage abspielen'); + form_utils.onsubmit(form, function (d) { + //announce([d.custom_announcement]); + send({ + type: 'free_announce', + tournament_key: curt.key, + text: d.custom_announcement, + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + }); + } -} + function render_enable_announcement(target) { + const announcements = uiu.el(target, 'div', 'enable_announcements_container'); + const heading = uiu.el(announcements, 'h3', {}, 'Ansagen auf diesem Gerät'); + const form = uiu.el(announcements, 'form'); + const enable_announcements = uiu.el(form, 'input', { + type: 'checkbox', + id: 'enable_announcements', + name: 'enable_announcements' + }); -function ui_show() { - crouting.set('t/:key/', {key: curt.key}); - const bup_lang = ((curt.language && curt.language !== 'auto') ? '&lang=' + encodeURIComponent(curt.language) : ''); - const bup_dm_style = '&dm_style=' + encodeURIComponent(curt.dm_style || 'international'); - toprow.set([{ - label: ci18n('Tournaments'), - func: ui_list, - }, { - label: curt.name || curt.key, - func: ui_show, - 'class': 'ct_name', - }], [{ - label: 'Scoreboard', - href: '/bup/#btsh_e=' + encodeURIComponent(curt.key) + '&display' + bup_dm_style + bup_lang, - }, { - label: 'Umpire Panel', - href: '/bup/#btsh_e=' + encodeURIComponent(curt.key) + bup_lang, - }, { - label: ci18n('Next Matches'), - href: '/admin/t/' + encodeURIComponent(curt.key) + '/upcoming', - },]); + enable_announcements.checked = (window.localStorage.getItem('enable_announcements') === 'true'); + uiu.el(form, 'label', { for: 'enable_announcements' }, 'Ansagen auf diesem Gerät aktivieren'); + enable_announcements.addEventListener('change', change_announcements); + } - const main = uiu.qs('.main'); - uiu.empty(main); + function change_announcements(e) { + let enable_announcements = document.getElementById('enable_announcements'); + console.log(enable_announcements.checked); + window.localStorage.setItem('enable_announcements', enable_announcements.checked); + } - const meta_div = uiu.el(main, 'div', 'metadata_container'); + function ui_show() { + crouting.set('t/:key/', { key: curt.key }); + const bup_lang = ((curt.language && curt.language !== 'auto') ? '&lang=' + encodeURIComponent(curt.language) : ''); + const bup_dm_style = '&dm_style=' + encodeURIComponent(curt.dm_style || 'international'); + toprow.set([{ + label: ci18n('Tournaments'), + func: ui_list, + }, { + label: curt.name || curt.key, + func: ui_show, + 'class': 'ct_name', + }], [{ + label: 'Scoreboard', + href: '/bup/#btsh_e=' + encodeURIComponent(curt.key) + '&display' + bup_dm_style + bup_lang, + }, { + label: 'Umpire Panel', + href: '/bup/#btsh_e=' + encodeURIComponent(curt.key) + bup_lang, + }, { + label: ci18n('Next Matches'), + href: '/admin/t/' + encodeURIComponent(curt.key) + '/upcoming', + },]); - uiu.el(meta_div, 'div', 'unassigned_tableoperators_container'); - render_announcement_formular(meta_div); + const main = uiu.qs('.main'); + uiu.empty(main); - const meta_right_div = uiu.el(meta_div, 'div', 'metadata_right_container'); + const meta_div = uiu.el(main, 'div', 'metadata_container'); - render_enable_announcement(meta_right_div); + uiu.el(meta_div, 'div', 'unassigned_tableoperators_container'); + render_announcement_formular(meta_div); - render_settings(meta_right_div); + const meta_right_div = uiu.el(meta_div, 'div', 'metadata_right_container'); - uiu.el(main, 'h1', 'tournament_name ct_name', curt.name || curt.key); + render_enable_announcement(meta_right_div); - cmatch.prepare_render(curt); + render_settings(meta_right_div); - + uiu.el(main, 'h1', 'tournament_name ct_name', curt.name || curt.key); + cmatch.prepare_render(curt); - uiu.el(main, 'div', 'courts_container'); - uiu.el(main, 'div', 'unassigned_container'); - const match_create_container = uiu.el(main, 'div'); - cmatch.render_create(match_create_container); - uiu.el(main, 'div', 'finished_container'); + uiu.el(main, 'div', 'courts_container'); + uiu.el(main, 'div', 'unassigned_container'); + const match_create_container = uiu.el(main, 'div'); + cmatch.render_create(match_create_container); + uiu.el(main, 'div', 'finished_container'); - _show_render_matches(); + _show_render_matches(); - const footer_links = uiu.el(main, 'div', 'footer_links'); - const umpires_link = uiu.el(footer_links, 'span', 'vlink', ci18n('umpires:status:heading')); - umpires_link.addEventListener('click', cumpires.ui_status); + const footer_links = uiu.el(main, 'div', 'footer_links'); + const umpires_link = uiu.el(footer_links, 'span', 'vlink', ci18n('umpires:status:heading')); + umpires_link.addEventListener('click', cumpires.ui_status); - if (/^dmo35/.test(curt.key)) { - const csvexport_link = uiu.el(footer_links, 'span', 'vlink', ci18n('csvexport:winners')); - csvexport_link.addEventListener('click', ccsvexport.export_winners); - } + if (/^dmo35/.test(curt.key)) { + const csvexport_link = uiu.el(footer_links, 'span', 'vlink', ci18n('csvexport:winners')); + csvexport_link.addEventListener('click', ccsvexport.export_winners); + } - if (curt.is_nation_competition) { - crouting.render_link(footer_links, `t/${curt.key}/nationstats`, ci18n('nationstats')); + if (curt.is_nation_competition) { + crouting.render_link(footer_links, `t/${curt.key}/nationstats`, ci18n('nationstats')); + } + + _show_render_tabletoperators(); } - - _show_render_tabletoperators(); -} -_route_single(/t\/([a-z0-9]+)\/$/, ui_show, change.default_handler(_update_all_ui_elements, { - score: update_score, - court_current_match: update_current_match, - update_player_status: update_player_status, - match_edit: update_match, - tabletoperator_add: tabletoperator_add, - tabletoperator_removed: tabletoperator_removed, -})); - -function render_settings(target) { - const settings_div = uiu.el(target, 'div', 'metadata_right_container'); - uiu.el(settings_div, 'h3', {}, 'Turnier-Einstellungen'); - const settings_btn = uiu.el(settings_div, 'div', 'tournament_settings_link vlink', ci18n('edit tournament')); - settings_btn.addEventListener('click', ui_edit); - - if (curt.btp_enabled) { - const btp_fetch_btn = uiu.el(settings_div, 'button', 'tournament_btp_fetch', ci18n('update from BTP')); - btp_fetch_btn.addEventListener('click', ui_btp_fetch); + _route_single(/t\/([a-z0-9]+)\/$/, ui_show, change.default_handler(_update_all_ui_elements, { + score: update_score, + court_current_match: update_current_match, + update_player_status: update_player_status, + match_edit: update_match, + tabletoperator_add: tabletoperator_add, + tabletoperator_removed: tabletoperator_removed, + })); + + function render_settings(target) { + const settings_div = uiu.el(target, 'div', 'metadata_right_container'); + uiu.el(settings_div, 'h3', {}, 'Turnier-Einstellungen'); + const settings_btn = uiu.el(settings_div, 'div', 'tournament_settings_link vlink', ci18n('edit tournament')); + settings_btn.addEventListener('click', ui_edit); + + if (curt.btp_enabled) { + const btp_fetch_btn = uiu.el(settings_div, 'button', 'tournament_btp_fetch', ci18n('update from BTP')); + btp_fetch_btn.addEventListener('click', ui_btp_fetch); + } + if (curt.ticker_enabled) { + const ticker_push_btn = uiu.el(settings_div, 'button', 'tournament_ticker_push', ci18n('update ticker')); + ticker_push_btn.addEventListener('click', ui_ticker_push); + } } - if (curt.ticker_enabled) { - const ticker_push_btn = uiu.el(settings_div, 'button', 'tournament_ticker_push', ci18n('update ticker')); - ticker_push_btn.addEventListener('click', ui_ticker_push); + + function _upload_logo(e) { + const input = e.target; + if (!input.files.length) return; + + const reader = new FileReader(); + reader.readAsDataURL(input.files[0]); + reader.onload = () => { + send({ + type: 'tournament_upload_logo', + tournament_key: curt.key, + data_url: reader.result, + }, (err) => { + if (err) { + return cerror.net(err); + } + input.closest('form').reset(); + }); + }; + reader.onerror = (e) => { + alert('Failed to upload: ' + e); + }; } -} -function _upload_logo(e) { - const input = e.target; - if (!input.files.length) return; + function ui_edit() { + crouting.set('t/:key/edit', { key: curt.key }); + toprow.set([{ + label: ci18n('Tournaments'), + func: ui_list, + }, { + label: curt.name || curt.key, + func: ui_show, + 'class': 'ct_name', + }, { + label: ci18n('edit tournament'), + func: ui_edit, + }]); + + const main = uiu.qs('.main'); + uiu.empty(main); + + const form = uiu.el(main, 'form', 'tournament_settings'); + const key_label = uiu.el(form, 'label'); + uiu.el(key_label, 'span', {}, ci18n('tournament:edit:id')); + uiu.el(key_label, 'input', { + type: 'text', + name: 'key', + readonly: 'readonly', + disabled: 'disabled', + title: 'Can not be changed', + 'class': 'uneditable', + value: curt.key, + }); - const reader = new FileReader(); - reader.readAsDataURL(input.files[0]); - reader.onload = () => { - send({ - type: 'tournament_upload_logo', - tournament_key: curt.key, - data_url: reader.result, - }, (err) => { - if (err) { - return cerror.net(err); - } - input.closest('form').reset(); + const name_label = uiu.el(form, 'label'); + uiu.el(name_label, 'span', {}, ci18n('tournament:edit:name')); + uiu.el(name_label, 'input', { + type: 'text', + name: 'name', + required: 'required', + value: curt.name || curt.key, + 'class': 'ct_name', }); - }; - reader.onerror = (e) => { - alert('Failed to upload: ' + e); - }; -} -function ui_edit() { - crouting.set('t/:key/edit', {key: curt.key}); - toprow.set([{ - label: ci18n('Tournaments'), - func: ui_list, - }, { - label: curt.name || curt.key, - func: ui_show, - 'class': 'ct_name', - }, { - label: ci18n('edit tournament'), - func: ui_edit, - }]); - - const main = uiu.qs('.main'); - uiu.empty(main); - - const form = uiu.el(main, 'form', 'tournament_settings'); - const key_label = uiu.el(form, 'label'); - uiu.el(key_label, 'span', {}, ci18n('tournament:edit:id')); - uiu.el(key_label, 'input', { - type: 'text', - name: 'key', - readonly: 'readonly', - disabled: 'disabled', - title: 'Can not be changed', - 'class': 'uneditable', - value: curt.key, - }); - - const name_label = uiu.el(form, 'label'); - uiu.el(name_label, 'span', {}, ci18n('tournament:edit:name')); - uiu.el(name_label, 'input', { - type: 'text', - name: 'name', - required: 'required', - value: curt.name || curt.key, - 'class': 'ct_name', - }); - - - const name_tguid = uiu.el(form, 'label'); - uiu.el(name_tguid, 'span', {}, ci18n('tournament:edit:tguid')); - uiu.el(name_tguid, 'input', { - type: 'text', - name: 'tguid', - value: curt.tguid ? curt.tguid : "", - 'class': 'ct_tguid', - }); - - // Tournament language selection - const language_label = uiu.el(form, 'label'); - uiu.el(language_label, 'span', {}, ci18n('tournament:edit:language')); - const language_select = uiu.el(language_label, 'select', { - name: 'language', - required: 'required', - }); - const all_langs = ci18n.get_all_languages(); - uiu.el(language_select, 'option', {value: 'auto'}, ci18n('tournament:edit:language:auto')); - for (const l of all_langs) { - const l_attrs = { - value: l._code, + + const name_tguid = uiu.el(form, 'label'); + uiu.el(name_tguid, 'span', {}, ci18n('tournament:edit:tguid')); + uiu.el(name_tguid, 'input', { + type: 'text', + name: 'tguid', + value: curt.tguid ? curt.tguid : "", + 'class': 'ct_tguid', + }); + + // Tournament language selection + const language_label = uiu.el(form, 'label'); + uiu.el(language_label, 'span', {}, ci18n('tournament:edit:language')); + const language_select = uiu.el(language_label, 'select', { + name: 'language', + required: 'required', + }); + const all_langs = ci18n.get_all_languages(); + uiu.el(language_select, 'option', { value: 'auto' }, ci18n('tournament:edit:language:auto')); + for (const l of all_langs) { + const l_attrs = { + value: l._code, + }; + if (l._code === curt.language) { + l_attrs.selected = 'selected'; + } + uiu.el(language_select, 'option', l_attrs, l._name); + } + + // Team competition? + const is_team_label = uiu.el(form, 'label'); + const is_team_attrs = { + type: 'checkbox', + name: 'is_team', }; - if (l._code === curt.language) { - l_attrs.selected = 'selected'; + if (curt.is_team) { + is_team_attrs.checked = 'checked'; } - uiu.el(language_select, 'option', l_attrs, l._name); - } + uiu.el(is_team_label, 'input', is_team_attrs); + uiu.el(is_team_label, 'span', {}, ci18n('team competition')); - // Team competition? - const is_team_label = uiu.el(form, 'label'); - const is_team_attrs = { - type: 'checkbox', - name: 'is_team', - }; - if (curt.is_team) { - is_team_attrs.checked = 'checked'; - } - uiu.el(is_team_label, 'input', is_team_attrs); - uiu.el(is_team_label, 'span', {}, ci18n('team competition')); - - // Nation competition? - const is_nation_competition_label = uiu.el(form, 'label'); - const is_nation_competition_attrs = { - type: 'checkbox', - name: 'is_nation_competition', - }; - if (curt.is_nation_competition) { - is_nation_competition_attrs.checked = 'checked'; - } - uiu.el(is_nation_competition_label, 'input', is_nation_competition_attrs); - uiu.el(is_nation_competition_label, 'span', {}, ci18n('nation competition')); - - // Default display - const cur_dm_style = curt.dm_style || 'international'; - const dm_style_label = uiu.el(form, 'label'); - uiu.el(dm_style_label, 'span', {}, ci18n('tournament:edit:dm_style')); - const dm_style_select = uiu.el(dm_style_label, 'select', { - name: 'dm_style', - required: 'required', - }); - const all_dm_styles = displaymode.ALL_STYLES; - for (const s of all_dm_styles) { - const s_attrs = { - value: s, + // Nation competition? + const is_nation_competition_label = uiu.el(form, 'label'); + const is_nation_competition_attrs = { + type: 'checkbox', + name: 'is_nation_competition', }; - if (s === cur_dm_style) { - s_attrs.selected = 'selected'; + if (curt.is_nation_competition) { + is_nation_competition_attrs.checked = 'checked'; + } + uiu.el(is_nation_competition_label, 'input', is_nation_competition_attrs); + uiu.el(is_nation_competition_label, 'span', {}, ci18n('nation competition')); + + // Default display + const cur_dm_style = curt.dm_style || 'international'; + const dm_style_label = uiu.el(form, 'label'); + uiu.el(dm_style_label, 'span', {}, ci18n('tournament:edit:dm_style')); + const dm_style_select = uiu.el(dm_style_label, 'select', { + name: 'dm_style', + required: 'required', + }); + const all_dm_styles = displaymode.ALL_STYLES; + for (const s of all_dm_styles) { + const s_attrs = { + value: s, + }; + if (s === cur_dm_style) { + s_attrs.selected = 'selected'; + } + uiu.el(dm_style_select, 'option', s_attrs, s); } - uiu.el(dm_style_select, 'option', s_attrs, s); - } - // Placed on court required? - const only_now_on_court_label = uiu.el(form, 'label'); - const attrs = { - type: 'checkbox', - name: 'only_now_on_court', - }; - if (curt.only_now_on_court) { - attrs.checked = 'checked'; - } - uiu.el(only_now_on_court_label, 'input', attrs); - uiu.el(only_now_on_court_label, 'span', {}, ci18n('tournament:edit:only_now_on_court')); + const displaysettings_style_label = uiu.el(form, 'label'); + uiu.el(displaysettings_style_label, 'span', {}, ci18n('tournament:edit:displaysettings_general')); + createGeneralDisplaySettingsSelectBox(displaysettings_style_label, curt.displaysettings_general ? curt.displaysettings_general : "default"); - // Warmup Timer - if (!curt.warmup_ready) { - curt.warmup_ready = 150; - } - if (!curt.warmup_start) { - curt.warmup_start = 180; - } - var warmup_options = [ ['bwf-2016' , 90, 120, true], - ['legacy' , 120, 120, true], - ['choise' , curt.warmup_ready, curt.warmup_start, false], - ['call-down' , curt.warmup_ready, curt.warmup_start, false], - ['call-up' , 0, 0, true], - ['none' , 0, 0, true]]; - - var last_selected_warmup = warmup_options[0]; - - const warmup_timer_label = uiu.el(form, 'label'); - uiu.el(warmup_timer_label, 'span', {}, ci18n('tournament:edit:warmup_timer_behavior')); - const warmup_timer_select = uiu.el(warmup_timer_label, 'select', { - name: 'warmup', - }); - uiu.el(warmup_timer_select, 'option', {value: warmup_options[0][0]}, ci18n('tournament:edit:warmup_timer_behavior:' + warmup_options[0][0]), {wo: warmup_options[0][0]}); - let warmup_marked = false; - - const warmup_ready = uiu.el(form, 'label'); - uiu.el(warmup_ready, 'span', {}, ci18n('tournament:edit:warmup_ready')); - var warmup_ready_input = uiu.el(warmup_ready, 'input', { - type: 'number', - name: 'warmup_ready', - required: 'required', - disabled: warmup_options[0][3], - value: warmup_options[0][1], - 'class': 'ct_name', - }); - - const warmup_start = uiu.el(form, 'label'); - uiu.el(warmup_start, 'span', {}, ci18n('tournament:edit:warmup_start')); - var warmup_start_input = uiu.el(warmup_start, 'input', { - type: 'number', - name: 'warmup_start', - required: 'required', - disabled: warmup_options[0][3], - value: warmup_options[0][2], - 'class': 'ct_name', - }); - - for (const wo of warmup_options.slice(1)) { + // Placed on court required? + const only_now_on_court_label = uiu.el(form, 'label'); const attrs = { - value: wo[0], + type: 'checkbox', + name: 'only_now_on_court', + }; + if (curt.only_now_on_court) { + attrs.checked = 'checked'; } + uiu.el(only_now_on_court_label, 'input', attrs); + uiu.el(only_now_on_court_label, 'span', {}, ci18n('tournament:edit:only_now_on_court')); - if ((wo[0] === curt.warmup) && !warmup_marked) { - warmup_marked = true; - attrs.selected = 'selected'; - - warmup_ready_input.value = wo[1]; - warmup_ready_input.disabled = wo[3]; - warmup_start_input.value = wo[2]; - warmup_start_input.disabled = wo[3]; + // Warmup Timer + if (!curt.warmup_ready) { + curt.warmup_ready = 150; + } - last_selected_warmup = wo; + if (!curt.warmup_start) { + curt.warmup_start = 180; } - uiu.el(warmup_timer_select, 'option', attrs, ci18n('tournament:edit:warmup_timer_behavior:'+wo[0])); - } + var warmup_options = [['bwf-2016', 90, 120, true], + ['legacy', 120, 120, true], + ['choise', curt.warmup_ready, curt.warmup_start, false], + ['call-down', curt.warmup_ready, curt.warmup_start, false], + ['call-up', 0, 0, true], + ['none', 0, 0, true]]; - warmup_timer_select.onchange = function() { - if (!last_selected_warmup[3]) { - for (const wo of warmup_options) { - if (!wo[3]) - { - wo[1] = warmup_ready_input.value; - wo[2] = warmup_start_input.value; - } + var last_selected_warmup = warmup_options[0]; + + const warmup_timer_label = uiu.el(form, 'label'); + uiu.el(warmup_timer_label, 'span', {}, ci18n('tournament:edit:warmup_timer_behavior')); + const warmup_timer_select = uiu.el(warmup_timer_label, 'select', { + name: 'warmup', + }); + uiu.el(warmup_timer_select, 'option', { value: warmup_options[0][0] }, ci18n('tournament:edit:warmup_timer_behavior:' + warmup_options[0][0]), { wo: warmup_options[0][0] }); + let warmup_marked = false; + + const warmup_ready = uiu.el(form, 'label'); + uiu.el(warmup_ready, 'span', {}, ci18n('tournament:edit:warmup_ready')); + var warmup_ready_input = uiu.el(warmup_ready, 'input', { + type: 'number', + name: 'warmup_ready', + required: 'required', + disabled: warmup_options[0][3], + value: warmup_options[0][1], + 'class': 'ct_name', + }); + + const warmup_start = uiu.el(form, 'label'); + uiu.el(warmup_start, 'span', {}, ci18n('tournament:edit:warmup_start')); + var warmup_start_input = uiu.el(warmup_start, 'input', { + type: 'number', + name: 'warmup_start', + required: 'required', + disabled: warmup_options[0][3], + value: warmup_options[0][2], + 'class': 'ct_name', + }); + + for (const wo of warmup_options.slice(1)) { + const attrs = { + value: wo[0], } - } - for (const wo of warmup_options) { - if (warmup_timer_select.value == wo[0]) { + if ((wo[0] === curt.warmup) && !warmup_marked) { + warmup_marked = true; + attrs.selected = 'selected'; + warmup_ready_input.value = wo[1]; warmup_ready_input.disabled = wo[3]; warmup_start_input.value = wo[2]; @@ -642,342 +620,366 @@ function ui_edit() { last_selected_warmup = wo; } + + uiu.el(warmup_timer_select, 'option', attrs, ci18n('tournament:edit:warmup_timer_behavior:' + wo[0])); } - }; - // BTP - const btp_fieldset = uiu.el(form, 'fieldset'); - uiu.el(btp_fieldset, 'h2', {}, ci18n('tournament:edit:btp')); - const btp_enabled_label = uiu.el(btp_fieldset, 'label'); - const ba_attrs = { - type: 'checkbox', - name: 'btp_enabled', - }; - if (curt.btp_enabled) { - ba_attrs.checked = 'checked'; - } - uiu.el(btp_enabled_label, 'input', ba_attrs); - uiu.el(btp_enabled_label, 'span', {}, ci18n('tournament:edit:btp:enabled')); + warmup_timer_select.onchange = function () { + if (!last_selected_warmup[3]) { + for (const wo of warmup_options) { + if (!wo[3]) { + wo[1] = warmup_ready_input.value; + wo[2] = warmup_start_input.value; + } + } + } - const btp_autofetch_enabled_label = uiu.el(btp_fieldset, 'label'); - const bae_attrs = { - type: 'checkbox', - name: 'btp_autofetch_enabled', - }; - if (curt.btp_autofetch_enabled) { - bae_attrs.checked = 'checked'; - } - uiu.el(btp_autofetch_enabled_label, 'input', bae_attrs); - uiu.el(btp_autofetch_enabled_label, 'span', {}, ci18n('tournament:edit:btp:autofetch_enabled')); + for (const wo of warmup_options) { + if (warmup_timer_select.value == wo[0]) { + warmup_ready_input.value = wo[1]; + warmup_ready_input.disabled = wo[3]; + warmup_start_input.value = wo[2]; + warmup_start_input.disabled = wo[3]; - const btp_readonly_label = uiu.el(btp_fieldset, 'label'); - const bro_attrs = { - type: 'checkbox', - name: 'btp_readonly', - }; - if (curt.btp_readonly) { - bro_attrs.checked = 'checked'; - } - uiu.el(btp_readonly_label, 'input', bro_attrs); - uiu.el(btp_readonly_label, 'span', {}, ci18n('tournament:edit:btp:readonly')); - - const btp_ip_label = uiu.el(btp_fieldset, 'label'); - uiu.el(btp_ip_label, 'span', {}, ci18n('tournament:edit:btp:ip')); - uiu.el(btp_ip_label, 'input', { - type: 'text', - name: 'btp_ip', - value: (curt.btp_ip || ''), - }); - - const btp_password_label = uiu.el(btp_fieldset, 'label'); - uiu.el(btp_password_label, 'span', {}, ci18n('tournament:edit:btp:password')); - uiu.el(btp_password_label, 'input', { - type: 'text', - name: 'btp_password', - value: (curt.btp_password || ''), - }); - - // BTP timezone - const btp_timezone_label = uiu.el(btp_fieldset, 'label'); - uiu.el(btp_timezone_label, 'span', {}, ci18n('tournament:edit:btp:timezone')); - const btp_timezone_select = uiu.el(btp_timezone_label, 'select', { - name: 'btp_timezone', - }); - uiu.el( - btp_timezone_select, 'option', {value: 'system'}, - ci18n('tournament:edit:btp:system timezone', {tz: curt.system_timezone})); - let marked = false; - for (const tz of timezones.ALL_TIMEZONES) { - const attrs = { - value: tz, - } + last_selected_warmup = wo; + } + } + }; - if ((tz === curt.btp_timezone) && !marked) { - marked = true; - attrs.selected = 'selected'; + // BTP + const btp_fieldset = uiu.el(form, 'fieldset'); + uiu.el(btp_fieldset, 'h2', {}, ci18n('tournament:edit:btp')); + const btp_enabled_label = uiu.el(btp_fieldset, 'label'); + const ba_attrs = { + type: 'checkbox', + name: 'btp_enabled', + }; + if (curt.btp_enabled) { + ba_attrs.checked = 'checked'; } + uiu.el(btp_enabled_label, 'input', ba_attrs); + uiu.el(btp_enabled_label, 'span', {}, ci18n('tournament:edit:btp:enabled')); - uiu.el(btp_timezone_select, 'option', attrs, tz); - } + const btp_autofetch_enabled_label = uiu.el(btp_fieldset, 'label'); + const bae_attrs = { + type: 'checkbox', + name: 'btp_autofetch_enabled', + }; + if (curt.btp_autofetch_enabled) { + bae_attrs.checked = 'checked'; + } + uiu.el(btp_autofetch_enabled_label, 'input', bae_attrs); + uiu.el(btp_autofetch_enabled_label, 'span', {}, ci18n('tournament:edit:btp:autofetch_enabled')); - // Ticker - const ticker_fieldset = uiu.el(form, 'fieldset'); - uiu.el(ticker_fieldset, 'h2', {}, ci18n('tournament:edit:ticker')); - const ticker_enabled_label = uiu.el(ticker_fieldset, 'label'); - const te_attrs = { - type: 'checkbox', - name: 'ticker_enabled', - }; - if (curt.ticker_enabled) { - te_attrs.checked = 'checked'; - } - uiu.el(ticker_enabled_label, 'input', te_attrs); - uiu.el(ticker_enabled_label, 'span', {}, ci18n('tournament:edit:ticker_enabled')); - - const ticker_url_label = uiu.el(ticker_fieldset, 'label'); - uiu.el(ticker_url_label, 'span', {}, ci18n('tournament:edit:ticker_url')); - uiu.el(ticker_url_label, 'input', { - type: 'text', - name: 'ticker_url', - value: (curt.ticker_url || ''), - }); - - const ticker_password_label = uiu.el(ticker_fieldset, 'label'); - uiu.el(ticker_password_label, 'span', {}, ci18n('tournament:edit:ticker_password')); - uiu.el(ticker_password_label, 'input', { - type: 'text', - name: 'ticker_password', - value: (curt.ticker_password || ''), - }); - - - - const tablet_fieldset = uiu.el(form, 'fieldset'); - uiu.el(tablet_fieldset, 'h2', {}, ci18n('tournament:edit:tablets')); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_enabled'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_umpire_enabled'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_use_manual_counting_boards_enabled'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_split_doubles'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_state_enabled'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_set_break_after_tabletservice'); - create_checkbox(curt, tablet_fieldset, 'preparation_meetingpoint_enabled'); - create_checkbox(curt, tablet_fieldset, 'preparation_tabletoperator_setup_enabled'); - if (!curt.tabletoperator_break_seconds) { - curt.tabletoperator_break_seconds = 300; - } - create_input(curt, "number", tablet_fieldset, 'tabletoperator_break_seconds') - - - - - uiu.el(form, 'button', { - role: 'submit', - }, ci18n('Change')); - form_utils.onsubmit(form, function(data) { - const props = { - name: data.name, - tguid: data.tguid, - language: data.language, - is_team: (!!data.is_team), - is_nation_competition: (!!data.is_nation_competition), - only_now_on_court: (!!data.only_now_on_court), - btp_enabled: (!!data.btp_enabled), - btp_autofetch_enabled: (!!data.btp_autofetch_enabled), - btp_readonly: (!!data.btp_readonly), - btp_ip: data.btp_ip, - btp_password: data.btp_password, - btp_timezone: data.btp_timezone, - dm_style: data.dm_style, - warmup: data.warmup, - warmup_ready: data.warmup_ready, - warmup_start: data.warmup_start, - ticker_enabled: (!! data.ticker_enabled), - ticker_url: data.ticker_url, - ticker_password: data.ticker_password, - tabletoperator_with_umpire_enabled: (!!data.tabletoperator_with_umpire_enabled), - tabletoperator_winner_of_quaterfinals_enabled: (!!data.tabletoperator_winner_of_quaterfinals_enabled), - tabletoperator_split_doubles: (!!data.tabletoperator_split_doubles), - tabletoperator_with_state_enabled: (!!data.tabletoperator_with_state_enabled), - tabletoperator_set_break_after_tabletservice: (!!data.tabletoperator_set_break_after_tabletservice), - tabletoperator_use_manual_counting_boards_enabled: (!!data.tabletoperator_use_manual_counting_boards_enabled), - tabletoperator_enabled: (!!data.tabletoperator_enabled), - tabletoperator_break_seconds: data.tabletoperator_break_seconds, - preparation_meetingpoint_enabled: (!!data.preparation_meetingpoint_enabled), - preparation_tabletoperator_setup_enabled: (!!data.preparation_tabletoperator_setup_enabled) + const btp_readonly_label = uiu.el(btp_fieldset, 'label'); + const bro_attrs = { + type: 'checkbox', + name: 'btp_readonly', }; - send({ - type: 'tournament_edit_props', - key: curt.key, - props: props, - }, function(err) { - if (err) { - return cerror.net(err); - } - ui_show(); + if (curt.btp_readonly) { + bro_attrs.checked = 'checked'; + } + uiu.el(btp_readonly_label, 'input', bro_attrs); + uiu.el(btp_readonly_label, 'span', {}, ci18n('tournament:edit:btp:readonly')); + + const btp_ip_label = uiu.el(btp_fieldset, 'label'); + uiu.el(btp_ip_label, 'span', {}, ci18n('tournament:edit:btp:ip')); + uiu.el(btp_ip_label, 'input', { + type: 'text', + name: 'btp_ip', + value: (curt.btp_ip || ''), }); - }); - - const logo_preview_container = uiu.el(main, 'div', { - style: ( - 'float:right;position:relative;text-align:center;' + - 'height: 216px; width: 384px; font-size: 35px;' + - 'background:' + (curt.logo_background_color || '#000000') + ';' + - 'color:' + (curt.logo_foreground_color || '#aaaaaa') + ';' - ), - }); - if (curt.logo_id) { - uiu.el(logo_preview_container, 'img', { - style: 'height: 151px;', - src: '/h/' + encodeURIComponent(curt.key) + '/logo/' + curt.logo_id, + + const btp_password_label = uiu.el(btp_fieldset, 'label'); + uiu.el(btp_password_label, 'span', {}, ci18n('tournament:edit:btp:password')); + uiu.el(btp_password_label, 'input', { + type: 'text', + name: 'btp_password', + value: (curt.btp_password || ''), }); - uiu.el(logo_preview_container, 'div', {}, 'Court 42'); - } - uiu.el(main, 'h2', {}, ci18n('tournament:edit:logo')); - const logo_form = uiu.el(main, 'form'); - const logo_button = uiu.el(logo_form, 'input', { - type: 'file', - accept: 'image/*', - }); - logo_button.addEventListener('change', _upload_logo); - const logo_colors_container = uiu.el(logo_form, 'div', {style: 'display: block'}); - const bg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:background')); - const logo_background_color_input = uiu.el(bg_col_label, 'input', { - type: 'color', - name: 'logo_background_color', - value: curt.logo_background_color || '#000000', - }); - logo_background_color_input.addEventListener('change', (e) => { - send({ - type: 'tournament_edit_props', - key: curt.key, - props: { - logo_background_color: e.target.value, - }, - }, function(err) { - if (err) { - return cerror.net(err); - } + // BTP timezone + const btp_timezone_label = uiu.el(btp_fieldset, 'label'); + uiu.el(btp_timezone_label, 'span', {}, ci18n('tournament:edit:btp:timezone')); + const btp_timezone_select = uiu.el(btp_timezone_label, 'select', { + name: 'btp_timezone', }); - }); - const fg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:foreground')); - const fg_col_input = uiu.el(fg_col_label, 'input', { - type: 'color', - name: 'logo_foreground_color', - value: curt.logo_foreground_color || '#aaaaaa', - }); - fg_col_input.addEventListener('change', (e) => { - send({ - type: 'tournament_edit_props', - key: curt.key, - props: { - logo_foreground_color: e.target.value, - }, - }, function(err) { - if (err) { - return cerror.net(err); + uiu.el( + btp_timezone_select, 'option', { value: 'system' }, + ci18n('tournament:edit:btp:system timezone', { tz: curt.system_timezone })); + let marked = false; + for (const tz of timezones.ALL_TIMEZONES) { + const attrs = { + value: tz, } - }); - }); - - uiu.el(main, 'h2', {}, ci18n('tournament:edit:courts')); - - const courts_table = uiu.el(main, 'table'); - const courts_tbody = uiu.el(courts_table, 'tbody'); - for (const c of curt.courts) { - const tr = uiu.el(courts_tbody, 'tr'); - uiu.el(tr, 'th', {}, c.num); - uiu.el(tr, 'td', {}, c.name || ''); - const actions_td = uiu.el(tr, 'td', {}); - const del_btn = uiu.el(actions_td, 'button', { - 'data-court-id': c._id, - }, 'Delete'); - del_btn.addEventListener('click', function(e) { - const del_btn = e.target; - const court_id = del_btn.getAttribute('data-court-id'); - if (confirm('Do you really want to delete ' + court_id + '? (Will not do anything yet!)')) { - debug.log('TODO: would now delete court'); + + if ((tz === curt.btp_timezone) && !marked) { + marked = true; + attrs.selected = 'selected'; } - }); - } - const nums = curt.courts.map(c => parseInt(c.num)); - const maxnum = Math.max(0, Math.max.apply(null, nums)); - - const courts_add_form = uiu.el(main, 'form'); - uiu.el(courts_add_form, 'input', { - type: 'number', - name: 'count', - min: 1, - max: 99, - value: 1, - }); - const courts_add_button = uiu.el(courts_add_form, 'button', { - role: 'button', - }, 'Add Courts'); - form_utils.onsubmit(courts_add_form, function(data) { - courts_add_button.setAttribute('disabled', 'disabled'); - const court_count = parseInt(data.count); - const nums = []; - for (let court_num = maxnum + 1;court_num <= maxnum + court_count;court_num++) { - nums.push(court_num); + uiu.el(btp_timezone_select, 'option', attrs, tz); } - send({ - type: 'courts_add', - tournament_key: curt.key, - nums, - }, function(err, response) { - if (err) { - courts_add_button.removeAttribute('disabled'); - return cerror.net(err); - } - Array.prototype.push.apply(curt.courts, response.added_courts); - ui_edit(); + // Ticker + const ticker_fieldset = uiu.el(form, 'fieldset'); + uiu.el(ticker_fieldset, 'h2', {}, ci18n('tournament:edit:ticker')); + const ticker_enabled_label = uiu.el(ticker_fieldset, 'label'); + const te_attrs = { + type: 'checkbox', + name: 'ticker_enabled', + }; + if (curt.ticker_enabled) { + te_attrs.checked = 'checked'; + } + uiu.el(ticker_enabled_label, 'input', te_attrs); + uiu.el(ticker_enabled_label, 'span', {}, ci18n('tournament:edit:ticker_enabled')); + + const ticker_url_label = uiu.el(ticker_fieldset, 'label'); + uiu.el(ticker_url_label, 'span', {}, ci18n('tournament:edit:ticker_url')); + uiu.el(ticker_url_label, 'input', { + type: 'text', + name: 'ticker_url', + value: (curt.ticker_url || ''), }); - }); - uiu.el(main, 'h2', {}, ci18n('tournament:edit:displays')); + const ticker_password_label = uiu.el(ticker_fieldset, 'label'); + uiu.el(ticker_password_label, 'span', {}, ci18n('tournament:edit:ticker_password')); + uiu.el(ticker_password_label, 'input', { + type: 'text', + name: 'ticker_password', + value: (curt.ticker_password || ''), + }); - const display_table = uiu.el(main, 'table'); - const display_tbody = uiu.el(display_table, 'tbody'); - const tr = uiu.el(display_tbody, 'tr'); - uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:num')); - uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:court')); - uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:setting')); - uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:onlinestatus')); - for (const c of curt.displays) { - const tr = uiu.el(display_tbody, 'tr'); - uiu.el(tr, 'th', {}, c.client_id); - createCourtSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.court_id); - createDisplaySettingsSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.displaysetting_id); - uiu.el(tr, 'td', {}, (!c.online) ? 'offline' : 'online'); - const actions_td = uiu.el(tr, 'td', {}); - const reset_btn = uiu.el(actions_td, 'button', { - 'data-display-setting-id': c.client_id, - }, 'Restart'); - - if (!c.online) { - reset_btn.setAttribute('disabled', 'disabled'); + + const tablet_fieldset = uiu.el(form, 'fieldset'); + uiu.el(tablet_fieldset, 'h2', {}, ci18n('tournament:edit:tablets')); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_enabled'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_umpire_enabled'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_use_manual_counting_boards_enabled'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_split_doubles'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_state_enabled'); + create_checkbox(curt, tablet_fieldset, 'tabletoperator_set_break_after_tabletservice'); + create_checkbox(curt, tablet_fieldset, 'preparation_meetingpoint_enabled'); + create_checkbox(curt, tablet_fieldset, 'preparation_tabletoperator_setup_enabled'); + if (!curt.tabletoperator_break_seconds) { + curt.tabletoperator_break_seconds = 300; } - reset_btn.addEventListener('click', function (e) { - const del_btn = e.target; - const display_setting_id = del_btn.getAttribute('data-display-setting-id'); + create_input(curt, "number", tablet_fieldset, 'tabletoperator_break_seconds') + + + + + uiu.el(form, 'button', { + role: 'submit', + }, ci18n('Change')); + form_utils.onsubmit(form, function (data) { + const props = { + name: data.name, + tguid: data.tguid, + language: data.language, + is_team: (!!data.is_team), + is_nation_competition: (!!data.is_nation_competition), + only_now_on_court: (!!data.only_now_on_court), + btp_enabled: (!!data.btp_enabled), + btp_autofetch_enabled: (!!data.btp_autofetch_enabled), + btp_readonly: (!!data.btp_readonly), + btp_ip: data.btp_ip, + btp_password: data.btp_password, + btp_timezone: data.btp_timezone, + dm_style: data.dm_style, + displaysettings_general: data.displaysettings_general, + warmup: data.warmup, + warmup_ready: data.warmup_ready, + warmup_start: data.warmup_start, + ticker_enabled: (!!data.ticker_enabled), + ticker_url: data.ticker_url, + ticker_password: data.ticker_password, + tabletoperator_with_umpire_enabled: (!!data.tabletoperator_with_umpire_enabled), + tabletoperator_winner_of_quaterfinals_enabled: (!!data.tabletoperator_winner_of_quaterfinals_enabled), + tabletoperator_split_doubles: (!!data.tabletoperator_split_doubles), + tabletoperator_with_state_enabled: (!!data.tabletoperator_with_state_enabled), + tabletoperator_set_break_after_tabletservice: (!!data.tabletoperator_set_break_after_tabletservice), + tabletoperator_use_manual_counting_boards_enabled: (!!data.tabletoperator_use_manual_counting_boards_enabled), + tabletoperator_enabled: (!!data.tabletoperator_enabled), + tabletoperator_break_seconds: data.tabletoperator_break_seconds, + preparation_meetingpoint_enabled: (!!data.preparation_meetingpoint_enabled), + preparation_tabletoperator_setup_enabled: (!!data.preparation_tabletoperator_setup_enabled) + }; send({ - type: 'reset_display', + type: 'tournament_edit_props', + key: curt.key, + props: props, + }, function (err) { + if (err) { + return cerror.net(err); + } + ui_show(); + }); + }); + + const logo_preview_container = uiu.el(main, 'div', { + style: ( + 'float:right;position:relative;text-align:center;' + + 'height: 216px; width: 384px; font-size: 35px;' + + 'background:' + (curt.logo_background_color || '#000000') + ';' + + 'color:' + (curt.logo_foreground_color || '#aaaaaa') + ';' + ), + }); + if (curt.logo_id) { + uiu.el(logo_preview_container, 'img', { + style: 'height: 151px;', + src: '/h/' + encodeURIComponent(curt.key) + '/logo/' + curt.logo_id, + }); + uiu.el(logo_preview_container, 'div', {}, 'Court 42'); + } + + uiu.el(main, 'h2', {}, ci18n('tournament:edit:logo')); + const logo_form = uiu.el(main, 'form'); + const logo_button = uiu.el(logo_form, 'input', { + type: 'file', + accept: 'image/*', + }); + logo_button.addEventListener('change', _upload_logo); + const logo_colors_container = uiu.el(logo_form, 'div', { style: 'display: block' }); + const bg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:background')); + const logo_background_color_input = uiu.el(bg_col_label, 'input', { + type: 'color', + name: 'logo_background_color', + value: curt.logo_background_color || '#000000', + }); + logo_background_color_input.addEventListener('change', (e) => { + send({ + type: 'tournament_edit_props', + key: curt.key, + props: { + logo_background_color: e.target.value, + }, + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + }); + const fg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:foreground')); + const fg_col_input = uiu.el(fg_col_label, 'input', { + type: 'color', + name: 'logo_foreground_color', + value: curt.logo_foreground_color || '#aaaaaa', + }); + fg_col_input.addEventListener('change', (e) => { + send({ + type: 'tournament_edit_props', + key: curt.key, + props: { + logo_foreground_color: e.target.value, + }, + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + }); + + uiu.el(main, 'h2', {}, ci18n('tournament:edit:courts')); + + const courts_table = uiu.el(main, 'table'); + const courts_tbody = uiu.el(courts_table, 'tbody'); + for (const c of curt.courts) { + const tr = uiu.el(courts_tbody, 'tr'); + uiu.el(tr, 'th', {}, c.num); + uiu.el(tr, 'td', {}, c.name || ''); + const actions_td = uiu.el(tr, 'td', {}); + const del_btn = uiu.el(actions_td, 'button', { + 'data-court-id': c._id, + }, 'Delete'); + del_btn.addEventListener('click', function (e) { + const del_btn = e.target; + const court_id = del_btn.getAttribute('data-court-id'); + if (confirm('Do you really want to delete ' + court_id + '? (Will not do anything yet!)')) { + debug.log('TODO: would now delete court'); + } + }); + } + + const nums = curt.courts.map(c => parseInt(c.num)); + const maxnum = Math.max(0, Math.max.apply(null, nums)); + + const courts_add_form = uiu.el(main, 'form'); + uiu.el(courts_add_form, 'input', { + type: 'number', + name: 'count', + min: 1, + max: 99, + value: 1, + }); + const courts_add_button = uiu.el(courts_add_form, 'button', { + role: 'button', + }, 'Add Courts'); + form_utils.onsubmit(courts_add_form, function (data) { + courts_add_button.setAttribute('disabled', 'disabled'); + const court_count = parseInt(data.count); + const nums = []; + for (let court_num = maxnum + 1; court_num <= maxnum + court_count; court_num++) { + nums.push(court_num); + } + + send({ + type: 'courts_add', tournament_key: curt.key, - display_setting_id: display_setting_id, - }, err => { + nums, + }, function (err, response) { if (err) { + courts_add_button.removeAttribute('disabled'); return cerror.net(err); } + Array.prototype.push.apply(curt.courts, response.added_courts); + ui_edit(); }); }); + + uiu.el(main, 'h2', {}, ci18n('tournament:edit:displays')); + + const display_table = uiu.el(main, 'table'); + const display_tbody = uiu.el(display_table, 'tbody'); + const tr = uiu.el(display_tbody, 'tr'); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:num')); + uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:court')); + uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:setting')); + uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:onlinestatus')); + + for (const c of curt.displays) { + const tr = uiu.el(display_tbody, 'tr'); + uiu.el(tr, 'th', {}, c.client_id); + createCourtSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.court_id); + createDisplaySettingsSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.displaysetting_id); + uiu.el(tr, 'td', {}, (!c.online) ? 'offline' : 'online'); + const actions_td = uiu.el(tr, 'td', {}); + const reset_btn = uiu.el(actions_td, 'button', { + 'data-display-setting-id': c.client_id, + }, 'Restart'); + + if (!c.online) { + reset_btn.setAttribute('disabled', 'disabled'); + } + reset_btn.addEventListener('click', function (e) { + const del_btn = e.target; + const display_setting_id = del_btn.getAttribute('data-display-setting-id'); + send({ + type: 'reset_display', + tournament_key: curt.key, + display_setting_id: display_setting_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + } } -} -_route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit); + _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit); function create_checkbox(curt, parent_el, filed_id) { @@ -1006,287 +1008,306 @@ _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit); -function createCourtSelectBox(parentEl,parent_id, court_id) { - const court_select_box = uiu.el(parentEl, 'select', { - name: 'court_'+ parent_id, - }); + function createCourtSelectBox(parentEl, parent_id, court_id) { + const court_select_box = uiu.el(parentEl, 'select', { + name: 'court_' + parent_id, + }); - for (const court of curt.courts) { + const empty_id = "--"; const attrs = { 'data-display-setting-id': court_id, - value: court._id, + value: empty_id, } - if ((court_id === court._id)) { + if (!court_id || empty_id === court_id) { attrs.selected = 'selected'; } + uiu.el(court_select_box, 'option', attrs, empty_id); - uiu.el(court_select_box, 'option', attrs, court.num); - } - - court_select_box.addEventListener('change', (e) => { - const select_box = e.target; - const display_setting_id = select_box.name.split("_")[1]; - send({ - type: 'relocate_display', - tournament_key: curt.key, - new_court_id: e.srcElement.value, - display_setting_id: display_setting_id, - }, err => { - if (err) { - return cerror.net(err); + for (const court of curt.courts) { + const attrs = { + 'data-display-setting-id': court_id, + value: court._id, } - }); - }); -} - -function createDisplaySettingsSelectBox(parentEl, parent_id, displaysettings_id) { - const displaysettings_select_box = uiu.el(parentEl, 'select', { - name: 'displaysettings_' + parent_id, - }); - for (const ds of curt.displaysettings) { - const attrs = { - 'data-display-setting-id': displaysettings_id, - value: ds.id, + if ((court_id === court._id)) { + attrs.selected = 'selected'; + } + uiu.el(court_select_box, 'option', attrs, court.num); } - if ((displaysettings_id === ds.id)) { - attrs.selected = 'selected'; - } - uiu.el(displaysettings_select_box, 'option', attrs, ds.id); + court_select_box.addEventListener('change', (e) => { + const select_box = e.target; + const display_setting_id = select_box.name.split("_")[1]; + send({ + type: 'relocate_display', + tournament_key: curt.key, + new_court_id: e.srcElement.value, + display_setting_id: display_setting_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); } - displaysettings_select_box.addEventListener('change', (e) => { - const select_box = e.target; - const display_setting_id = select_box.name.split("_")[1]; - send({ - type: 'change_display_mode', - tournament_key: curt.key, - new_displaysettings_id: e.srcElement.value, - display_setting_id: display_setting_id, - }, err => { - if (err) { - return cerror.net(err); - } + function createDisplaySettingsSelectBox(parentEl, parent_id, displaysettings_id) { + const displaysettings_select_box = uiu.el(parentEl, 'select', { + name: 'displaysettings_' + parent_id, }); - }); -} + createSelectBoxContent(displaysettings_select_box, curt.displaysettings, displaysettings_id); -function render_upcoming(container) { - cmatch.prepare_render(curt); - const courts_container = uiu.el(container, 'div', 'courts_container'); - cmatch.render_courts(courts_container, 'public'); + displaysettings_select_box.addEventListener('change', (e) => { + const select_box = e.target; + const display_setting_id = select_box.name.split("_")[1]; + send({ + type: 'change_display_mode', + tournament_key: curt.key, + new_displaysettings_id: e.srcElement.value, + display_setting_id: display_setting_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + } - const upcoming_container = uiu.el(container, 'div', 'upcoming_container'); - cmatch.render_upcoming_matches(upcoming_container); -} + function createGeneralDisplaySettingsSelectBox(parentEl, displaysettings_id) { + const displaysettings_select_box = uiu.el(parentEl, 'select', { + name: 'displaysettings_general' + }); + createSelectBoxContent(displaysettings_select_box, curt.displaysettings, displaysettings_id); + + } + function createSelectBoxContent(select_box, content, selected_id) { + for (const item of content) { + const attrs = { + 'data-display-setting-id': selected_id, + value: item.id, + } + if ((selected_id === item.id)) { + attrs.selected = 'selected'; + } + uiu.el(select_box, 'option', attrs, item.id); + } + } -function ui_upcoming() { - crouting.set('t/:key/upcoming', {key: curt.key}); - toprow.hide(); + function render_upcoming(container) { + cmatch.prepare_render(curt); + const courts_container = uiu.el(container, 'div', 'courts_container'); + cmatch.render_courts(courts_container, 'public'); - const main = uiu.qs('.main'); - uiu.empty(main); - main.classList.add('main_upcoming'); + const upcoming_container = uiu.el(container, 'div', 'upcoming_container'); + cmatch.render_upcoming_matches(upcoming_container); + } - uiu.hide_qs('.btp_status'); - uiu.hide_qs('.ticker_status'); - uiu.hide_qs('.status'); + function ui_upcoming() { + crouting.set('t/:key/upcoming', { key: curt.key }); + toprow.hide(); - render_upcoming(main); - main.addEventListener('click', () => { - fullscreen.toggle(); - }); -} -_route_single(/t\/([a-z0-9]+)\/upcoming/, ui_upcoming, change.default_handler(_update_all_ui_elements_upcoming, { - score: update_score, - court_current_match: update_upcoming_current_match, - //update_player_status: update_player_status, - match_edit: update_upcoming_match, - //tabletoperator_add: tabletoperator_add, - //tabletoperator_removed: tabletoperator_removed, -})); - - -function init() { - send({ - type: 'tournament_list', - }, function(err, response) { - if (err) { - return cerror.net(err); - } + const main = uiu.qs('.main'); + uiu.empty(main); + main.classList.add('main_upcoming'); - const tournaments = response.tournaments; - if (tournaments.length === 1) { - switch_tournament(tournaments[0].key, ui_show); - } else { - list_show(tournaments); - } - }); -} -crouting.register(/^$/, init, change.default_handler); + uiu.hide_qs('.btp_status'); + uiu.hide_qs('.ticker_status'); + uiu.hide_qs('.status'); -function _cancel_ui_allscoresheets() { - const dlg = document.querySelector('.allscoresheets_dialog'); - if (!dlg) { - return; // Already cancelled + render_upcoming(main); + main.addEventListener('click', () => { + fullscreen.toggle(); + }); } - cbts_utils.esc_stack_pop(); - uiu.remove(dlg); - ui_show(); -} + _route_single(/t\/([a-z0-9]+)\/upcoming/, ui_upcoming, change.default_handler(_update_all_ui_elements_upcoming, { + score: update_score, + court_current_match: update_upcoming_current_match, + //update_player_status: update_player_status, + match_edit: update_upcoming_match, + //tabletoperator_add: tabletoperator_add, + //tabletoperator_removed: tabletoperator_removed, + })); -function _pad(n, width, z) { - z = z || '0'; - n = n + ''; - return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; -} + function init() { + send({ + type: 'tournament_list', + }, function (err, response) { + if (err) { + return cerror.net(err); + } -function _render_scoresheet(task, pos, cb) { - const { - container, - status, - progress, - matches, - pseudo_state, - tournament_name, - zip} = task; + const tournaments = response.tournaments; + if (tournaments.length === 1) { + switch_tournament(tournaments[0].key, ui_show); + } else { + list_show(tournaments); + } + }); + } + crouting.register(/^$/, init, change.default_handler); - if (pos >= matches.length) { - return cb(); + function _cancel_ui_allscoresheets() { + const dlg = document.querySelector('.allscoresheets_dialog'); + if (!dlg) { + return; // Already cancelled + } + cbts_utils.esc_stack_pop(); + uiu.remove(dlg); + ui_show(); } - progress.value = pos; - uiu.text(status, 'Rendere ' + (pos + 1) + ' / ' + (matches.length)); - - const match = matches[pos]; - const setup = utils.deep_copy(match.setup); - setup.tournament_name = curt.name; - const s = calc.remote_state(pseudo_state, setup, match.presses); - s.ui = {}; - - scoresheet.load_sheet(scoresheet.sheet_name(setup), function(xml) { - var svg = scoresheet.make_sheet_node(s, xml); - svg.setAttribute('class', 'scoresheet single_scoresheet'); - // Usually we'd call importNode here to import the document here, but IE/Edge then ignores the styles - container.appendChild(svg); - scoresheet.sheet_render(s, svg); - - const title = ( - tournament_name + ' ' + _pad(setup.match_num, 3, ' ') + ' ' + - setup.event_name + ' ' + setup.match_name + ' ' + - pronunciation.teamtext_internal(s, 0) + ' v ' + - pronunciation.teamtext_internal(s, 1)); - const props = { - title, - subject: 'Schiedsrichterzettel', - creator: 'bts with bup (https://github.com/phihag/bts/)', - }; - const pdf = svg2pdf.make([svg], props, 'landscape'); + function _pad(n, width, z) { + z = z || '0'; + n = n + ''; + return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; + } - const ab = pdf.output('arraybuffer'); - zip.file(title.replace(/\s*\/\s*/g, ', ') + '.pdf', ab); - uiu.empty(container); - progress.value = pos + 1; - setTimeout(function() { - _render_scoresheet(task, pos + 1, cb); - }, 0); - }, '/bupdev/'); -} + function _render_scoresheet(task, pos, cb) { + const { + container, + status, + progress, + matches, + pseudo_state, + tournament_name, + zip } = task; -function ui_allscoresheets() { - crouting.set('t/' + curt.key + '/allscoresheets', {}, _cancel_ui_allscoresheets); + if (pos >= matches.length) { + return cb(); + } - cbts_utils.esc_stack_push(_cancel_ui_allscoresheets); + progress.value = pos; + uiu.text(status, 'Rendere ' + (pos + 1) + ' / ' + (matches.length)); + + const match = matches[pos]; + const setup = utils.deep_copy(match.setup); + setup.tournament_name = curt.name; + const s = calc.remote_state(pseudo_state, setup, match.presses); + s.ui = {}; + + scoresheet.load_sheet(scoresheet.sheet_name(setup), function (xml) { + var svg = scoresheet.make_sheet_node(s, xml); + svg.setAttribute('class', 'scoresheet single_scoresheet'); + // Usually we'd call importNode here to import the document here, but IE/Edge then ignores the styles + container.appendChild(svg); + scoresheet.sheet_render(s, svg); + + const title = ( + tournament_name + ' ' + _pad(setup.match_num, 3, ' ') + ' ' + + setup.event_name + ' ' + setup.match_name + ' ' + + pronunciation.teamtext_internal(s, 0) + ' v ' + + pronunciation.teamtext_internal(s, 1)); + const props = { + title, + subject: 'Schiedsrichterzettel', + creator: 'bts with bup (https://github.com/phihag/bts/)', + }; + const pdf = svg2pdf.make([svg], props, 'landscape'); + + const ab = pdf.output('arraybuffer'); + zip.file(title.replace(/\s*\/\s*/g, ', ') + '.pdf', ab); + + uiu.empty(container); + progress.value = pos + 1; + setTimeout(function () { + _render_scoresheet(task, pos + 1, cb); + }, 0); + }, '/bupdev/'); + } - const body = uiu.qs('body'); - const dialog_bg = uiu.el(body, 'div', 'dialog_bg allscoresheets_dialog'); - const dialog = uiu.el(dialog_bg, 'div', 'dialog'); + function ui_allscoresheets() { + crouting.set('t/' + curt.key + '/allscoresheets', {}, _cancel_ui_allscoresheets); - uiu.el(dialog, 'h3', {}, 'Generiere Schiedsrichterzettel'); + cbts_utils.esc_stack_push(_cancel_ui_allscoresheets); - const status = uiu.el(dialog, 'div', {}, 'Lade Daten ...'); + const body = uiu.qs('body'); + const dialog_bg = uiu.el(body, 'div', 'dialog_bg allscoresheets_dialog'); + const dialog = uiu.el(dialog_bg, 'div', 'dialog'); - const progress = uiu.el(dialog, 'progress', { - style: 'min-width: 60vw;', - }); - send({ - type: 'fetch_allscoresheets_data', - tournament_key: curt.key, - }, function (err, response) { - if (err) { - return cerror.net(err); - } + uiu.el(dialog, 'h3', {}, 'Generiere Schiedsrichterzettel'); - const matches = response.matches; - progress.max = matches.length; - uiu.text(status, 'Starte Rendering (' + matches.length + ' Spiele)'); + const status = uiu.el(dialog, 'div', {}, 'Lade Daten ...'); - const zip = new JSZip(); - const container = uiu.el(dialog, 'div', { - 'class': 'allscoresheets_svg_container', + const progress = uiu.el(dialog, 'progress', { + style: 'min-width: 60vw;', }); - printing.set_orientation('landscape'); - - const lang = 'en'; - const pseudo_state = { - settings: { - shuttle_counter: true, - }, - lang, - }; - i18n.update_state(pseudo_state, lang); - i18n.register_lang(i18n_de); - i18n.register_lang(i18n_en); - - const task = { - container, - status, - progress, - matches, - pseudo_state, - tournament_name: curt.name, - zip, - }; + send({ + type: 'fetch_allscoresheets_data', + tournament_key: curt.key, + }, function (err, response) { + if (err) { + return cerror.net(err); + } - _render_scoresheet(task, 0, function() { - uiu.text(status, 'Generiere Zip.'); - const zip_fn = curt.name + ' Schiedsrichterzettel.zip'; - zip.generateAsync({type: 'blob'}).then(function(blob) { - uiu.text(status, 'Starte Download.'); + const matches = response.matches; + progress.max = matches.length; + uiu.text(status, 'Starte Rendering (' + matches.length + ' Spiele)'); - save_file(blob, zip_fn); - uiu.text(status, 'Fertig.'); - }).catch(function(error) { - uiu.text(status, 'Fehler: ' + error.stack); + const zip = new JSZip(); + const container = uiu.el(dialog, 'div', { + 'class': 'allscoresheets_svg_container', + }); + printing.set_orientation('landscape'); + + const lang = 'en'; + const pseudo_state = { + settings: { + shuttle_counter: true, + }, + lang, + }; + i18n.update_state(pseudo_state, lang); + i18n.register_lang(i18n_de); + i18n.register_lang(i18n_en); + + const task = { + container, + status, + progress, + matches, + pseudo_state, + tournament_name: curt.name, + zip, + }; + + _render_scoresheet(task, 0, function () { + uiu.text(status, 'Generiere Zip.'); + const zip_fn = curt.name + ' Schiedsrichterzettel.zip'; + zip.generateAsync({ type: 'blob' }).then(function (blob) { + uiu.text(status, 'Starte Download.'); + + save_file(blob, zip_fn); + uiu.text(status, 'Fertig.'); + }).catch(function (error) { + uiu.text(status, 'Fehler: ' + error.stack); + }); }); }); - }); - const cancel_btn = uiu.el(dialog, 'div', 'vlink', 'Zurück'); - cancel_btn.addEventListener('click', _cancel_ui_allscoresheets); -} -crouting.register(/t\/([a-z0-9]+)\/allscoresheets$/, function(m) { - ctournament.switch_tournament(m[1], function() { - ui_allscoresheets(); - }); -}, change.default_handler(ui_allscoresheets)); - - -return { - init, - // For other modules - switch_tournament, - ui_show, - ui_list, - update_match, - update_upcoming_match, -}; + const cancel_btn = uiu.el(dialog, 'div', 'vlink', 'Zurück'); + cancel_btn.addEventListener('click', _cancel_ui_allscoresheets); + } + crouting.register(/t\/([a-z0-9]+)\/allscoresheets$/, function (m) { + ctournament.switch_tournament(m[1], function () { + ui_allscoresheets(); + }); + }, change.default_handler(ui_allscoresheets)); + + + return { + init, + // For other modules + switch_tournament, + ui_show, + ui_list, + update_match, + update_upcoming_match, + }; })(); From d137f69161a8b2530d3729c9b84bb5f112e7709a Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 9 Jun 2024 12:32:13 +0200 Subject: [PATCH 098/224] New Function: Now it is possible to adminsterthe speed of the announcements --- bts/admin.js | 2 +- static/js/announcements.js | 2 +- static/js/change.js | 2 +- static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/ctournament.js | 17 ++++++++++++++--- 6 files changed, 19 insertions(+), 6 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 487eba3..658ba84 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -59,7 +59,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'warmup', 'warmup_ready', 'warmup_start', 'ticker_enabled', 'ticker_url', 'ticker_password', 'language', 'dm_style', 'displaysettings_general', - 'tabletoperator_enabled', 'tabletoperator_break_seconds', + 'tabletoperator_enabled', 'tabletoperator_break_seconds','announcement_speed', 'tabletoperator_set_break_after_tabletservice','tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', diff --git a/static/js/announcements.js b/static/js/announcements.js index 34cb0c0..334748e 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -253,7 +253,7 @@ function announce(callArray) { if (part && part != null) { var words = new SpeechSynthesisUtterance(part); words.lang = ci18n('announcements:lang'); - words.rate = 1.05; + words.rate = curt.announcement_speed ? curt.announcement_speed: 1.05; words.pitch = 0; words.volume = 1; words.voice = voice; diff --git a/static/js/change.js b/static/js/change.js index aefeb4a..c8f1db6 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -72,7 +72,7 @@ function default_handler_func(rerender, special_funcs, c) { 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', - 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds', + 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds','announcement_speed', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', 'preparation_meetingpoint_enabled','preparation_tabletoperator_setup_enabled']; for (const cb_name of CHECKBOXES) { diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 32b3a57..ed7a050 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -109,6 +109,7 @@ var ci18n_de = { 'tournament:edit:tabletoperator_break_seconds': 'Pausenzeit nach Tabletbedienung (sec)', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', + 'tournament:edit:announcement_speed': 'Ansagegeschwindigkeit (0.8-1.3)', 'tournament:edit:preparation_meetingpoint_enabled': 'Meetingpoint für Vorbereitung nutzen', 'tournament:edit:preparation_tabletoperator_setup_enabled': 'Tabletbediener in Vorbereitung aufrufen', 'tournament:edit:ticker_enabled': 'Ticker aktivieren', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 944e988..c2c976a 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -113,6 +113,7 @@ var ci18n_en = { 'tournament:edit:tabletoperator_with_state_enabled': 'Call up national association instead of player', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', + 'tournament:edit:announcement_speed': 'Announcementspeed (0.8-1.3)', 'tournament:edit:preparation_meetingpoint_enabled': 'Use Meetingpoint for preparation', 'tournament:edit:preparation_tabletoperator_setup_enabled': 'Call up tablet operator in preparation', 'tournament:edit:displays': 'Manage Displays:', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index e99c028..eefae08 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -768,7 +768,7 @@ var ctournament = (function() { curt.tabletoperator_break_seconds = 300; } create_input(curt, "number", tablet_fieldset, 'tabletoperator_break_seconds') - + create_numeric_input(curt, tablet_fieldset, 'announcement_speed', 0.8, 1.3, 1.05,0.01); @@ -805,6 +805,7 @@ var ctournament = (function() { tabletoperator_use_manual_counting_boards_enabled: (!!data.tabletoperator_use_manual_counting_boards_enabled), tabletoperator_enabled: (!!data.tabletoperator_enabled), tabletoperator_break_seconds: data.tabletoperator_break_seconds, + announcement_speed: data.announcement_speed, preparation_meetingpoint_enabled: (!!data.preparation_meetingpoint_enabled), preparation_tabletoperator_setup_enabled: (!!data.preparation_tabletoperator_setup_enabled) }; @@ -1005,8 +1006,18 @@ var ctournament = (function() { }); } - - + function create_numeric_input(curt, parent_el, filed_id, min_value, max_value, default_value, step_value) { + const text_input = uiu.el(parent_el, 'label'); + uiu.el(text_input, 'span', {}, ci18n('tournament:edit:' + filed_id)); + uiu.el(text_input, 'input', { + type: "number", + name: filed_id, + value: curt[filed_id] || default_value, + min: min_value, + max: max_value, + step: step_value + }); + } function createCourtSelectBox(parentEl, parent_id, court_id) { const court_select_box = uiu.el(parentEl, 'select', { From da582a4f0345c88b458db369474c015c49be50a4 Mon Sep 17 00:00:00 2001 From: tengmel Date: Mon, 10 Jun 2024 21:46:53 +0200 Subject: [PATCH 099/224] Bugfix: Database run out of space. Now every 10 minutes it will be compressed. --- bts/database.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bts/database.js b/bts/database.js index 62927c1..57fedbe 100644 --- a/bts/database.js +++ b/bts/database.js @@ -20,14 +20,13 @@ const TABLES = [ 'displaysettings', 'display_court_displaysettings', 'normalizations' - ]; async function init_test() { const db = {} for (const key of TABLES) { - db[key] = new Datastore({inMemoryOnly: true}); + db[key] = new Datastore({ inMemoryOnly: true }); } await promisify(prepare)(db); return db; @@ -51,7 +50,9 @@ function init(callback) { } TABLES.forEach(function(key) { - db[key] = new Datastore({filename: path.join(db_dir, key), autoload: true}); + var d = new Datastore({ filename: path.join(db_dir, key), autoload: true }); + d.persistence.setAutocompactionInterval(60000*10); + db[key] = d; }); prepare(db, callback); @@ -65,7 +66,7 @@ function prepare(db, callback) { db.tournaments.ensureIndex({fieldName: 'key', unique: true}); db.umpires.ensureIndex({fieldName: 'name', unique: true}); db.umpires.ensureIndex({fieldName: 'tournament_key', unique: false}); - db.logs.ensureIndex({fieldName: 'tournament_key', unique: false}); + db.logs.ensureIndex({ fieldName: 'tournament_key', unique: false }); setup_helpers(db); From dabc8c56dc9a382bbd74eb0eedb57f5628f44ef3 Mon Sep 17 00:00:00 2001 From: tengmel Date: Mon, 10 Jun 2024 22:50:55 +0200 Subject: [PATCH 100/224] Enhancement: Displays and Adminpages will be updated first, Then ticker and other things will be handled --- bts/http_api.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/bts/http_api.js b/bts/http_api.js index a543d6c..23cf127 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -288,6 +288,17 @@ function score_handler(req, res) { async.waterfall([ cb => db.matches.update(query, {$set: update}, {returnUpdatedDocs: true}, (err, _, match) => cb(err, match)), + (match, cb) => { + bupws.handle_score_change(req.app, tournament_key, match.setup.court_id); + admin.notify_change(req.app, tournament_key, 'score', { + match_id, + network_score: update.network_score, + team1_won: update.team1_won, + shuttle_count: update.shuttle_count, + presses: match.presses, + }); + cb(null,match); + }, (match, cb) => { if (!match) { return cb(new Error('Cannot find match ' + JSON.stringify(match))); @@ -340,18 +351,6 @@ function score_handler(req, res) { cb(null, match); }, - (match, cb) => { - admin.notify_change(req.app, tournament_key, 'score', { - match_id, - network_score: update.network_score, - team1_won: update.team1_won, - shuttle_count: update.shuttle_count, - presses: match.presses, - }); - - bupws.handle_score_change(req.app, tournament_key, match.setup.court_id); - cb(); - }, ], function(err) { if (err) { res.json({ From 229ccfd6a5276c47d773db0cefb34a07d4a56985 Mon Sep 17 00:00:00 2001 From: tengmel Date: Mon, 10 Jun 2024 22:51:17 +0200 Subject: [PATCH 101/224] Enhancement: Somtimes Displays will not be initialited properly --- bts/bupws.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bts/bupws.js b/bts/bupws.js index 7fcb58a..a59c2df 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -139,6 +139,7 @@ async function initialize_client(ws, app, tournament_key, court_id, displaysetti ws.court_id = display_setting.court_id; court_id = display_setting.court_id; notify_change_ws(ws, tournament_key, court_id, "settings-update", display_setting); + notify_change_ws(ws, tournament_key, court_id, "settings-update", display_setting); } } matches_handler(app, ws, tournament_key, ws.court_id); From c31fc54566474fa72cb6d1b2f6619e379070ef23 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 11 Jun 2024 22:40:13 +0200 Subject: [PATCH 102/224] Fix: Matches are only updated if there has really been a change. --- bts/btp_sync.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 7ca5203..39eefa1 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -407,6 +407,14 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { match.setup.warmup_start = cur_match.setup.warmup_start; } } + + for(let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { + for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++){ + if('tablet_break_active' in cur_match.setup.teams[team_index].players[player_index]){ + match.setup.teams[team_index].players[player_index].tablet_break_active = cur_match.setup.teams[team_index].players[player_index].tablet_break_active; + } + } + } if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { // No update required @@ -420,7 +428,6 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { for(let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++){ cur_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; - match.setup.teams[team_index].players[player_index].tablet_break_active = cur_match.setup.teams[team_index].players[player_index].tablet_break_active; } } From 7d813ccdaba70917077fc369406c8fea6ba041a6 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 11 Jun 2024 23:04:09 +0200 Subject: [PATCH 103/224] Fix: Prevent group games from being created twice. This caused tablet operators to be entered twice in the list. --- bts/btp_sync.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 39eefa1..ed153fc 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -319,6 +319,10 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { const discipline_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + '_' + draw.Name[0]; const btp_id = tkey + '_' + discipline_name + '_' + bm.ID[0]; + if(bm.ReverseHomeAway){ + cb(null); + return; + } const query = { btp_id, tournament_key: tkey, From a11aefb27419ac089cadebb4213950ad386e04b1 Mon Sep 17 00:00:00 2001 From: tengmel Date: Wed, 12 Jun 2024 07:35:56 +0200 Subject: [PATCH 104/224] Bugfix: If Default Tournament does not exists an NPE was thrown --- bts/bupws.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index a59c2df..33ce30f 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -23,7 +23,7 @@ async function on_connect(app, ws) { async function notify_admin_display_status_changed(app, ws, ws_online) { app.db.tournaments.findOne({ key: default_tournament_key }, async (err, tournament) => { - if (!err && !tournament) { + if (!err || !tournament) { err = { message: 'No tournament ' + default_tournament_key }; } const client_id = determine_client_id(ws); @@ -32,7 +32,7 @@ async function notify_admin_display_status_changed(app, ws, ws_online) { display_court_displaysetting = create_display_court_displaysettings(client_id, null, generate_default_displaysettings_id(tournament)); } display_court_displaysetting.online = ws_online; - admin.notify_change(app, default_tournament_key, 'display_status_changed', { 'display_court_displaysetting': display_court_displaysetting }); + admin.notify_change(app, default_tournament_key, 'display_status_changed', { 'display_court_displaysetting': display_court_displaysetting }); }); } From 26ca2917b4d66abc8e5c918b82dd22685a68d33e Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 22 Jun 2024 17:45:28 +0200 Subject: [PATCH 105/224] Bugfix: There are Problems reading Matches from DB --- bts/http_api.js | 49 ++++++++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/bts/http_api.js b/bts/http_api.js index 23cf127..027ffd2 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -289,14 +289,16 @@ function score_handler(req, res) { async.waterfall([ cb => db.matches.update(query, {$set: update}, {returnUpdatedDocs: true}, (err, _, match) => cb(err, match)), (match, cb) => { - bupws.handle_score_change(req.app, tournament_key, match.setup.court_id); - admin.notify_change(req.app, tournament_key, 'score', { - match_id, - network_score: update.network_score, - team1_won: update.team1_won, - shuttle_count: update.shuttle_count, - presses: match.presses, - }); + if (match) { + bupws.handle_score_change(req.app, tournament_key, match.setup.court_id); + admin.notify_change(req.app, tournament_key, 'score', { + match_id, + network_score: update.network_score, + team1_won: update.team1_won, + shuttle_count: update.shuttle_count, + presses: match.presses, + }); + } cb(null,match); }, (match, cb) => { @@ -307,17 +309,20 @@ function score_handler(req, res) { }, (match, cb) => db.courts.findOne(court_q, (err, court) => cb(err, match, court)), (match, court, cb) => { - if (court.match_id === match_id) { - cb(null, match, court, false); - return; - } + if (!match) { + if (court.match_id === match_id) { + cb(null, match, court, false); + return; + } - db.courts.update(court_q, {$set: {match_id: match_id}}, {}, (err) => { - cb(err, match, court, true); - }); + db.courts.update(court_q, {$set: {match_id: match_id}}, {}, (err) => { + cb(err, match, court, true); + }); + } + cb(null, match, court, true); }, (match, court, changed_court, cb) => { - if (changed_court) { + if (match && changed_court) { admin.notify_change(req.app, tournament_key, 'court_current_match', { match__id: match_id, match: match, @@ -326,12 +331,13 @@ function score_handler(req, res) { cb(null, match, changed_court); }, (match, changed_court, cb) => { - btp_manager.update_score(req.app, match); - + if (match) { + btp_manager.update_score(req.app, match); + } cb(null, match, changed_court); }, (match, changed_court, cb) => { - if (match.setup.highlight && + if (match && match.setup.highlight && match.setup.highlight == 6 && match.network_score && match.network_score.length > 0 && @@ -346,9 +352,10 @@ function score_handler(req, res) { if (changed_court) { ticker_manager.pushall(req.app, tournament_key); } else { - ticker_manager.update_score(req.app, match); + if (match) { + ticker_manager.update_score(req.app, match); + } } - cb(null, match); }, ], function(err) { From 0400af3b83dc1e69bf122f0d1b0dd4106dd36b22 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Tue, 25 Jun 2024 21:17:23 +0200 Subject: [PATCH 106/224] Enhancement: Matches that not has to be played and which were removed by BTP also will be removed in BTS --- bts/btp_sync.js | 481 +++++++++++++++++++++------------------ static/js/cmatch.js | 11 +- static/js/ctournament.js | 14 ++ 3 files changed, 280 insertions(+), 226 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index ed153fc..3d68489 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -20,7 +20,7 @@ function date_str(dt) { async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, officials, bm, match_ids_on_court, match_types, is_league) { return new Promise((resolve, reject) => { - + const gtid = event.GameTypeID[0]; assert((gtid === 1) || (gtid === 2)); @@ -32,9 +32,9 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, const btp_player_ids = []; - if(bm.bts_players && bm.bts_players.length > 0) { + if (bm.bts_players && bm.bts_players.length > 0) { for (const team of bm.bts_players) { - if (team && team.length > 0){ + if (team && team.length > 0) { for (const p of team) { btp_player_ids.push(p.ID[0]); } @@ -43,39 +43,39 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, } const links = {}; - try{ + try { links.from1 = bm.From1[0]; links.from2 = bm.From2[0]; - if(bm.WinnerTo) { + if (bm.WinnerTo) { links.winner_to = bm.WinnerTo[0]; } - if(bm.LoserTo) { + if (bm.LoserTo) { links.loser_to = bm.LoserTo[0]; } - if(bm.Link) { + if (bm.Link) { links.from_link = bm.Link; } } catch (err) { console.log(err); } - if(teams[0].players.length < 1) { + if (teams[0].players.length < 1) { const link1 = btp_links.find(l => { return (l.DrawID[0] === bm.DrawID[0] && l.PlanningID[0] === links.from1); }); - if(link1){ + if (link1) { links.from1_link = link1.Link[0]; } } - if(teams[1].players.length < 1) { + if (teams[1].players.length < 1) { const link2 = btp_links.find(l => { return (l.DrawID[0] === bm.DrawID[0] && l.PlanningID[0] === links.from2); }); - if(link2){ + if (link2) { links.from2_link = link2.Link[0]; } } @@ -95,13 +95,13 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, highlight: bm.Highlight[0], }; - app.db.tournaments.findOne({key: tkey}, (err, tournament) => { + app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { - if(err) { + if (err) { console.log("reject"); reject(err); } - + if (tournament.warmup) { setup.warmup = tournament.warmup; } @@ -117,13 +117,13 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, } if (tournament.btp_settings.check_in_per_match && teams.length > 1 && teams[0].players.length > 0) { teams[0].players[0].checked_in = (bm.Status & 0b0001) > 0; - if(teams[0].players.length > 1) { + if (teams[0].players.length > 1) { teams[0].players[1].checked_in = (bm.Status & 0b0010) > 0; } if (teams[1].players.length > 0) { teams[1].players[0].checked_in = (bm.Status & 0b0100) > 0; - if(teams[1].players.length > 1) { + if (teams[1].players.length > 1) { teams[1].players[1].checked_in = (bm.Status & 0b1000) > 0; } } @@ -131,7 +131,7 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, if (match_name) { setup.match_name = match_name; } - + if (scheduled_time_str) { setup.scheduled_time_str = scheduled_time_str; } @@ -155,14 +155,14 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, assert(o); setup.service_judge_name = o.FirstName + ' ' + o.Name; } - + const btp_match_ids = [{ id: bm.ID[0], nr: bm.MatchNr[0], draw: bm.DrawID[0], planning: bm.PlanningID[0], }]; - + const match = { tournament_key: tkey, btp_id, @@ -188,17 +188,17 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, match._id = 'btp_' + btp_id; resolve(match); }); - }); + }); } function _craft_team(par) { if (!par) { - return {players: []}; + return { players: [] }; } const players = par.map(p => { - const asian_name = !! (p.Asianname && p.Asianname[0]); - const pres = {asian_name}; + const asian_name = !!(p.Asianname && p.Asianname[0]); + const pres = { asian_name }; if (p.Firstname && p.Lastname) { if (asian_name) { pres.name = p.Lastname[0].toUpperCase() + ' ' + p.Firstname[0]; @@ -219,7 +219,7 @@ function _craft_team(par) { } - if(p.ID && p.ID[0]) { + if (p.ID && p.ID[0]) { pres.btp_id = p.ID[0]; } @@ -227,18 +227,18 @@ function _craft_team(par) { pres.nationality = p.Country[0]; } - if(p.LastTimeOnCourt && p.LastTimeOnCourt[0]) { + if (p.LastTimeOnCourt && p.LastTimeOnCourt[0]) { let date = new Date(p.LastTimeOnCourt[0].year, - p.LastTimeOnCourt[0].month - 1, - p.LastTimeOnCourt[0].day, - p.LastTimeOnCourt[0].hour, - p.LastTimeOnCourt[0].minute, - p.LastTimeOnCourt[0].second, - p.LastTimeOnCourt[0].ms); + p.LastTimeOnCourt[0].month - 1, + p.LastTimeOnCourt[0].day, + p.LastTimeOnCourt[0].hour, + p.LastTimeOnCourt[0].minute, + p.LastTimeOnCourt[0].second, + p.LastTimeOnCourt[0].ms); pres.last_time_on_court_ts = date.getTime(); } - if(p.CheckedIn && p.CheckedIn.length > 0) { + if (p.CheckedIn && p.CheckedIn.length > 0) { pres.checked_in = p.CheckedIn[0]; } @@ -303,23 +303,62 @@ function _parse_score(bm) { return bm.Sets[0].Set.map(s => [s.T1[0], s.T2[0]]); } +async function cleanup_matches(app, tkey, btp_state, callback) { + + const { draws, events } = btp_state; + var btpMaptches = {} + btp_state.matches.forEach(function (match) { + btpMaptches[calculate_btp_match_id(tkey, match, draws, events)] = true; + }) + + app.db.matches.find({ 'tournament_key': tkey }, (err, matches, cb) => { + if (err) { + return callback(err); + } + matches.forEach(function (match) { + + if (btpMaptches[match.btp_id] === true) { + //TODO invert query + return; + } else { + const match_q = { _id: match._id }; + app.db.matches.remove(match_q, {}, (err) => { + const admin = require('./admin'); + admin.notify_change(app, match.tournament_key, 'match_remove', { + match__id: match._id + }); + return; + }); + + } + }) + }); + return callback(null); +} + + +function calculate_btp_match_id(tkey, bm, draws, events) { + const draw = draws.get(bm.DrawID[0]); + const event = events.get(draw.EventID[0]); + const discipline_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + '_' + draw.Name[0]; + return tkey + '_' + discipline_name + '_' + bm.ID[0]; +} async function integrate_matches(app, tkey, btp_state, court_map, callback) { const admin = require('./admin'); // avoid dependency cycle - const {draws, events, officials} = btp_state; + const { draws, events, officials } = btp_state; const match_ids_on_court = calculate_match_ids_on_court(btp_state); - async.each(btp_state.matches, function(bm, cb) { + async.each(btp_state.matches, function (bm, cb) { const draw = draws.get(bm.DrawID[0]); assert(draw); const event = events.get(draw.EventID[0]); assert(event); - const discipline_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + '_' + draw.Name[0]; - const btp_id = tkey + '_' + discipline_name + '_' + bm.ID[0]; + const btp_id = calculate_btp_match_id(tkey, bm, draws, events); - if(bm.ReverseHomeAway){ + if (bm.ReverseHomeAway) { cb(null); return; } @@ -338,105 +377,105 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { cb(null); return; } - + craft_match(app, tkey, btp_id, court_map, event, draw, btp_state.links, officials, bm, match_ids_on_court).then(match => { - + if (cur_match) { if (cur_match.team1_won === null) { cur_match.team1_won = undefined; } - if(!match.network_score && cur_match.network_score) { + if (!match.network_score && cur_match.network_score) { match.network_score = cur_match.network_score; } - - if(cur_match.setup.called_timestamp) { + + if (cur_match.setup.called_timestamp) { // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. match.setup.called_timestamp = cur_match.setup.called_timestamp; } - + if (cur_match.setup.tabletoperators) { // tabletoperators is not from btp so we have to coppy it to the match generated by btp. match.setup.tabletoperators = cur_match.setup.tabletoperators; } - - for(let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { - for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++){ - - if (cur_match.setup.teams[team_index].players[player_index].now_playing_on_court != undefined) { - match.setup.teams[team_index].players[player_index].now_playing_on_court = cur_match.setup.teams[team_index].players[player_index].now_playing_on_court; - } - - if (cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court != undefined) { - match.setup.teams[team_index].players[player_index].now_tablet_on_court = cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court; - } - - if(cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts || match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { - if(!cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { - cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; - } - - if(!match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { - match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; - } - - let max_ts = Math.max( cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts, - match.setup.teams[team_index].players[player_index].last_time_on_court_ts); - - cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; - match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; - } - } - } + + for (let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { + for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { + + if (cur_match.setup.teams[team_index].players[player_index].now_playing_on_court != undefined) { + match.setup.teams[team_index].players[player_index].now_playing_on_court = cur_match.setup.teams[team_index].players[player_index].now_playing_on_court; + } + + if (cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court != undefined) { + match.setup.teams[team_index].players[player_index].now_tablet_on_court = cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court; + } + + if (cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts || match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { + if (!cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { + cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; + } + + if (!match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { + match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; + } + + let max_ts = Math.max(cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts, + match.setup.teams[team_index].players[player_index].last_time_on_court_ts); + + cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; + match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; + } + } + } match.btp_needsync = cur_match.btp_needsync; match.network_team1_left = cur_match.network_team1_left; match.network_team1_serving = cur_match.network_team1_serving; match.network_teams_player1_even = cur_match.network_teams_player1_even; match.presses = cur_match.presses; - match.duration_ms = cur_match.duration_ms; + match.duration_ms = cur_match.duration_ms; match.end_ts = cur_match.end_ts; - if(match.setup.now_on_court === false) { - if(cur_match.setup.warmup) { + if (match.setup.now_on_court === false) { + if (cur_match.setup.warmup) { match.setup.warmup = cur_match.setup.warmup; } - if (cur_match.setup.warmup_ready) { - match.setup.warmup_ready = cur_match.setup.warmup_ready; - } - - if(cur_match.setup.warmup_start) { - match.setup.warmup_start = cur_match.setup.warmup_start; - } + if (cur_match.setup.warmup_ready) { + match.setup.warmup_ready = cur_match.setup.warmup_ready; + } + + if (cur_match.setup.warmup_start) { + match.setup.warmup_start = cur_match.setup.warmup_start; + } } - for(let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { - for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++){ - if('tablet_break_active' in cur_match.setup.teams[team_index].players[player_index]){ + for (let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { + for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { + if ('tablet_break_active' in cur_match.setup.teams[team_index].players[player_index]) { match.setup.teams[team_index].players[player_index].tablet_break_active = cur_match.setup.teams[team_index].players[player_index].tablet_break_active; } } } - - if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { - // No update required + + if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { + // No update required cb(null); - return; - } - // equals checked_in changed and check if it was the only change - let only_change_check_in = false; + return; + } + // equals checked_in changed and check if it was the only change + let only_change_check_in = false; let result_enterd_in_btp = false; - for(let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { - for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++){ + for (let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { + for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { cur_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; - } - } + } + } if (!cur_match.team1_won && cur_match.team1_won != match.team1_won) { - if (!match.end_ts) { + if (!match.end_ts) { result_enterd_in_btp = true; match.setup.warmup = 'none'; match.end_ts = Date.now(); @@ -453,53 +492,56 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { } } - if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { - only_change_check_in = true; - } - - app.db.matches.update({_id: cur_match._id}, {$set: match}, {}, (err) => { - if (err) { + if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { + only_change_check_in = true; + } + + app.db.matches.update({ _id: cur_match._id }, { $set: match }, {}, (err) => { + if (err) { cb(err); return; }; - + // render onli if is_match flag is set. else it's nessasary to have the game (it's a link) in the db, but not to rerender if (match.setup.is_match) { if (!only_change_check_in || result_enterd_in_btp) { - admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, - match: match}); - } else { - admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); - } + admin.notify_change(app, match.tournament_key, 'match_edit', { + match__id: match._id, + match: match + }); + } else { + admin.notify_change(app, match.tournament_key, 'update_player_status', { + match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup + }); + } } - }); + }); cb(null); - return; + return; } - app.db.matches.insert(match, function(err) { + app.db.matches.insert(match, function (err) { if (err) { cb(null); return; } - - admin.notify_change(app, tkey, 'match_add', {match}); + + admin.notify_change(app, tkey, 'match_add', { match }); cb(null) return; }); - }, error => - { + }, error => { cb(null); return; }); }); }, (error) => { - if (error){ + if (error) { console.log(error); } callback(null); - }); + }); } // Returns a map btp_court_id => court._id @@ -550,7 +592,7 @@ function integrate_courts(app, tournament_key, btp_state, callback) { if (cur_court) { // Add BTP ID - app.db.courts.update(alt_query, {$set: {btp_id}}, {}, (err) => cb(err)); + app.db.courts.update(alt_query, { $set: { btp_id } }, {}, (err) => cb(err)); return; } @@ -562,8 +604,8 @@ function integrate_courts(app, tournament_key, btp_state, callback) { if (err) return callback(err); if (changed) { - stournament.get_courts(app.db, tournament_key, function(err, all_courts) { - admin.notify_change(app, tournament_key, 'courts_changed', {all_courts}); + stournament.get_courts(app.db, tournament_key, function (err, all_courts) { + admin.notify_change(app, tournament_key, 'courts_changed', { all_courts }); callback(err, res); }); } else { @@ -582,13 +624,13 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { tournament.btp_settings = {}; changed = true; } - + const tournament_name = btp_state.btp_settings.get(1001).Value[0]; const tournament_urn = btp_state.btp_settings.get(1008).Value[0]; const check_in_per_match = btp_state.btp_settings.get(1003).Value[0] ? false : true; const pause_duration_ms = btp_state.btp_settings.get(1303).Value[0] * 60 * 1000; - + if (tournament.btp_settings.tournament_name != tournament_name) { tournament.btp_settings.tournament_name = tournament_name; @@ -611,7 +653,7 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { changed = true; toChange.btp_settings = tournament.btp_settings; } - + if (changed) { app.db.tournaments.update({ key: tkey }, { $set: toChange }, {}, (err) => { if (err) { @@ -628,31 +670,31 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { async function integrate_player_state(app, tkey, btp_state, callback) { const btp_manager = require('./btp_manager'); - app.db.tournaments.findOne({key: tkey}, (err, tournament) => { - if(err) return callback(err); - - if(!tournament.btp_settings.check_in_per_match) { + app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { + if (err) return callback(err); + + if (!tournament.btp_settings.check_in_per_match) { let ids_to_change = []; let players_to_change = []; async.eachOfSeries(btp_state.matches, async (match, key) => { - let cur_match = await get_match_form_db (app, tkey, btp_state, match); + let cur_match = await get_match_form_db(app, tkey, btp_state, match); if (cur_match && cur_match != null) { for (let team_nr = 0; team_nr < cur_match.setup.teams.length; team_nr++) { for (let player_nr = 0; player_nr < cur_match.setup.teams[team_nr].players.length; player_nr++) { - let id = pause_is_done(match, team_nr, player_nr, tournament.btp_settings); + let id = pause_is_done(match, team_nr, player_nr, tournament.btp_settings); if (id != undefined && id != null) { - - if (!cur_match.setup.teams[team_nr].players[player_nr].now_tablet_on_court && + + if (!cur_match.setup.teams[team_nr].players[player_nr].now_tablet_on_court && !cur_match.setup.teams[team_nr].players[player_nr].now_playing_on_court && !cur_match.setup.called_timestamp && !cur_match.network_score) { btp_state.matches[key].bts_players[team_nr][player_nr].CheckedIn[0] = true; - + const player = cur_match.setup.teams[team_nr].players[player_nr]; - if(ids_to_change.indexOf(id) == -1) { + if (ids_to_change.indexOf(id) == -1) { player.checked_in = true; player.tablet_break_active = false; ids_to_change.push(id); @@ -664,34 +706,22 @@ async function integrate_player_state(app, tkey, btp_state, callback) { } } }, (err) => { - if(err) return callback(err); + if (err) return callback(err); btp_manager.update_players(app, tkey, players_to_change); return callback(null); }); } - else - { + else { return callback(null); } - + }); } -async function get_match_form_db (app, tkey, btp_state, match) { +async function get_match_form_db(app, tkey, btp_state, match) { return new Promise((resolve, reject) => { - const {draws, events, officials} = btp_state; - const draw = draws.get(match.DrawID[0]); - if(!draw) { - return reject("Draw is unset!"); - } - - const event = events.get(draw.EventID[0]); - if (!event) { - return reject("Event is unset"); - } - - const discipline_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + '_' + draw.Name[0]; - const btp_id = tkey + '_' + discipline_name + '_' + match.ID[0]; + const { draws, events } = btp_state; + const btp_id = calculate_btp_match_id(tkey, match, draws, events); const query = { btp_id: btp_id, @@ -714,8 +744,8 @@ async function get_match_form_db (app, tkey, btp_state, match) { } function pause_is_done(match, team_nr, player_nr, btp_settings) { - if(match.bts_players && match.bts_players.length > team_nr) { - if(match.bts_players[team_nr] && match.bts_players[team_nr].length > player_nr) { + if (match.bts_players && match.bts_players.length > team_nr) { + if (match.bts_players[team_nr] && match.bts_players[team_nr].length > player_nr) { const player = match.bts_players[team_nr][player_nr]; if (player.CheckedIn[0]) { @@ -724,12 +754,12 @@ function pause_is_done(match, team_nr, player_nr, btp_settings) { if (player.LastTimeOnCourt && player.LastTimeOnCourt[0]) { const date = new Date(player.LastTimeOnCourt[0].year, - player.LastTimeOnCourt[0].month - 1, - player.LastTimeOnCourt[0].day, - player.LastTimeOnCourt[0].hour, - player.LastTimeOnCourt[0].minute, - player.LastTimeOnCourt[0].second, - player.LastTimeOnCourt[0].ms); + player.LastTimeOnCourt[0].month - 1, + player.LastTimeOnCourt[0].day, + player.LastTimeOnCourt[0].hour, + player.LastTimeOnCourt[0].minute, + player.LastTimeOnCourt[0].second, + player.LastTimeOnCourt[0].ms); const last_time_on_court_ts = date.getTime(); const now = new Date(); @@ -759,14 +789,14 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { } const btp_id = o.ID[0]; - app.db.umpires.findOne({tournament_key, name}, (err, cur) => { + app.db.umpires.findOne({ tournament_key, name }, (err, cur) => { if (err) return cb(err); if (cur) { if (cur.btp_id === btp_id) { return cb(); } else { - app.db.umpires.update({tournament_key, name}, {$set: {btp_id}}, {}, (err) => cb(err)); + app.db.umpires.update({ tournament_key, name }, { $set: { btp_id } }, {}, (err) => cb(err)); return; } } @@ -782,9 +812,9 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { }); }, err => { if (changed) { - stournament.get_umpires(app.db, tournament_key, function(err, all_umpires) { + stournament.get_umpires(app.db, tournament_key, function (err, all_umpires) { if (!err) { - admin.notify_change(app, tournament_key, 'umpires_changed', {all_umpires}); + admin.notify_change(app, tournament_key, 'umpires_changed', { all_umpires }); } callback(err); }); @@ -811,9 +841,9 @@ async function integrate_now_on_court(app, tkey, callback) { const stournament = require('./stournament'); // avoid dependency cycle const btp_manager = require('./btp_manager'); const bupws = require('./bupws'); - + // TODO after switching to async, this should happen during court&match construction - app.db.tournaments.findOne({key: tkey}, async (err, tournament) => { + app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { if (err) { return callback(err); } @@ -822,11 +852,11 @@ async function integrate_now_on_court(app, tkey, callback) { return callback(null); // Nothing to do here } - app.db.matches.find({'setup.now_on_court': true}, async (err, now_on_court_matches) => { + app.db.matches.find({ 'setup.now_on_court': true }, async (err, now_on_court_matches) => { if (err) return callback(err); await Promise.all(now_on_court_matches.map(async (match) => { - + const court_id = match.setup.court_id; const match_id = match._id; const called_timestamp = Date.now(); @@ -836,7 +866,7 @@ async function integrate_now_on_court(app, tkey, callback) { } const setup = match.setup; - if(!setup.called_timestamp) { + if (!setup.called_timestamp) { setup.called_timestamp = called_timestamp; try { if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { @@ -857,13 +887,13 @@ async function integrate_now_on_court(app, tkey, callback) { } btp_manager.update_players(app, tkey, setup.tabletoperators); } - + if (setup.highlight == 6) { setup.highlight = 0; } - const match_q = {_id: match_id}; - app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + const match_q = { _id: match_id }; + app.db.matches.update(match_q, { $set: { setup } }, {}, (err) => { if (err) { console.error(err); return; @@ -871,36 +901,38 @@ async function integrate_now_on_court(app, tkey, callback) { btp_manager.update_highlight(app, match); - const court_q = {_id: court_id}; + const court_q = { _id: court_id }; app.db.courts.find(court_q, (err, courts) => { if (err) { console.error(err); return; } - if (courts.length !== 1) return; + if (courts.length !== 1) return; - app.db.courts.update(court_q, {$set: {match_id}}, {}, (err) => { - if (err) { - console.error(err); + app.db.courts.update(court_q, { $set: { match_id } }, {}, (err) => { + if (err) { + console.error(err); return; - } + } - admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, - match: match}); - admin.notify_change(app, tkey, 'match_called_on_court', match); + admin.notify_change(app, match.tournament_key, 'match_edit', { + match__id: match._id, + match: match + }); + admin.notify_change(app, tkey, 'match_called_on_court', match); bupws.handle_score_change(app, tkey, court_id); - async.waterfall([ wcb => set_player_on_court(app, tkey, match.setup, wcb), - wcb => set_player_on_tablet(app, tkey, match.setup, wcb)], - (err) => { - if (err) { - console.error(err); - return; - } - }); + async.waterfall([wcb => set_player_on_court(app, tkey, match.setup, wcb), + wcb => set_player_on_tablet(app, tkey, match.setup, wcb)], + (err) => { + if (err) { + console.error(err); + return; + } + }); }); }); }); - } + } })); callback(null); }); @@ -935,39 +967,39 @@ function get_last_looser_on_court(admin, app, tkey, court_id) { admin.notify_change(app, tkey, 'tabletoperator_removed', { tabletoperator: changed_tabletoperator }); return resolve(returnvalue); }); - } else { + } else { return resolve(returnvalue); } }); }); } -function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { - - if(!match_on_court_setup.tabletoperators || match_on_court_setup.tabletoperators.length == 0) { +function set_player_on_tablet(app, tkey, match_on_court_setup, callback) { + + if (!match_on_court_setup.tabletoperators || match_on_court_setup.tabletoperators.length == 0) { return; } - + const admin = require('./admin'); // avoid dependency cycle - app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { + app.db.matches.find({ 'tournament_key': tkey }, async (err, matches) => { if (err) { callback(err); } async.each(matches, async (match, cb) => { - if(match.setup.now_on_court == false) { + if (match.setup.now_on_court == false) { return; } - + const match_id = match._id; let tablet_operatorns_btp_ids = [match_on_court_setup.tabletoperators[0].btp_id]; - if(match_on_court_setup.tabletoperators.length > 1) { + if (match_on_court_setup.tabletoperators.length > 1) { tablet_operatorns_btp_ids.push(match_on_court_setup.tabletoperators[1].btp_id); } let change = false; - + if (match.setup.teams[0].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { match.setup.teams[0].players[0].now_tablet_on_court = match_on_court_setup.court_id; match.setup.teams[0].players[0].checked_in = false; @@ -994,12 +1026,14 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { if (change) { const setup = match.setup; - const match_q = {_id: match_id}; - app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + const match_q = { _id: match_id }; + app.db.matches.update(match_q, { $set: { setup } }, {}, (err) => { if (err) return callback(err); - admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); + admin.notify_change(app, match.tournament_key, 'update_player_status', { + match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup + }); }); } }); @@ -1009,32 +1043,32 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { } -function set_player_on_court (app, tkey, match_on_court_setup, callback) { +function set_player_on_court(app, tkey, match_on_court_setup, callback) { const admin = require('./admin'); // avoid dependency cycle - app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { + app.db.matches.find({ 'tournament_key': tkey }, async (err, matches) => { if (err) { callback(err); } async.each(matches, async (match, cb) => { - if(match.setup.now_on_court == false) { + if (match.setup.now_on_court == false) { return; } - + const match_id = match._id; - let on_court_btp_ids = [match_on_court_setup.teams[0].players[0].btp_id, - match_on_court_setup.teams[1].players[0].btp_id]; + let on_court_btp_ids = [match_on_court_setup.teams[0].players[0].btp_id, + match_on_court_setup.teams[1].players[0].btp_id]; - if(match_on_court_setup.teams[0].players.length > 1) { + if (match_on_court_setup.teams[0].players.length > 1) { on_court_btp_ids.push(match_on_court_setup.teams[0].players[1].btp_id); } - - if(match_on_court_setup.teams[1].players.length > 1) { + + if (match_on_court_setup.teams[1].players.length > 1) { on_court_btp_ids.push(match_on_court_setup.teams[1].players[1].btp_id); } let change = false; - + if (match.setup.teams[0].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { match.setup.teams[0].players[0].now_playing_on_court = match_on_court_setup.court_id; match.setup.teams[0].players[0].tablet_break_active = false; @@ -1060,13 +1094,15 @@ function set_player_on_court (app, tkey, match_on_court_setup, callback) { } if (change) { const setup = match.setup; - const match_q = {_id: match_id}; - app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + const match_q = { _id: match_id }; + app.db.matches.update(match_q, { $set: { setup } }, {}, (err) => { if (err) return callback(err); - admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); + admin.notify_change(app, match.tournament_key, 'update_player_status', { + match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup + }); }); } }); @@ -1091,6 +1127,7 @@ function fetch(app, tkey, response, callback) { cb => integrate_courts(app, tkey, btp_state, cb), (court_map, cb) => integrate_matches(app, tkey, btp_state, court_map, cb), cb => integrate_now_on_court(app, tkey, cb), + cb => cleanup_matches(app, tkey, btp_state, cb), ], callback); } diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 9c0ca41..27a4a7b 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -448,8 +448,7 @@ function update_player(match_id, player, now_on_court, show_player_status) { }); } -function update_match(m, old_section, new_section) { - if(old_section != new_section) { + function remove_match_from_gui(m, old_section) { switch (old_section) { case 'finished': case 'unassigned': @@ -459,13 +458,16 @@ function update_match(m, old_section, new_section) { break; default: uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { - while(match_row_el.childElementCount > 1){ + while (match_row_el.childElementCount > 1) { match_row_el.removeChild(match_row_el.lastChild); } }); break; } - + } +function update_match(m, old_section, new_section) { + if(old_section != new_section) { + remove_match_from_gui(m, old_section); switch (new_section) { case 'finished': uiu.qsEach('.finished_container', (finished_container) => { @@ -1505,6 +1507,7 @@ return { render_upcoming_matches, update_match_score, update_match, + remove_match_from_gui, update_players, }; diff --git a/static/js/ctournament.js b/static/js/ctournament.js index eefae08..ff1883d 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -146,6 +146,19 @@ var ctournament = (function() { cmatch.update_players(m); } + function remove_match(c) { + const cval = c.val; + const match_id = cval.match__id; + + const m = utils.find(curt.matches, m => m._id === match_id); + if (!m) { + cerror.silent('Cannot find match to update, ID: ' + JSON.stringify(match_id)); + return; + } + const section = cmatch.calc_section(m); + cmatch.remove_match_from_gui(m, section); + + } function update_match(c) { const cval = c.val; const match_id = cval.match__id; @@ -383,6 +396,7 @@ var ctournament = (function() { court_current_match: update_current_match, update_player_status: update_player_status, match_edit: update_match, + match_remove: remove_match, tabletoperator_add: tabletoperator_add, tabletoperator_removed: tabletoperator_removed, })); From 32e570f6ed2f945ea902d4ec469edcc3e28eb053 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 29 Jun 2024 14:12:38 +0200 Subject: [PATCH 107/224] Enhancement: Now it is possible to create the Match call on court manually until the first point is scored --- bts/admin.js | 10 ++ static/css/cmatch.css | 17 ++- static/icons/manual_call.svg | 238 +++++++++++++++++++++++++++++++++++ static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/cmatch.js | 55 ++++++-- 6 files changed, 305 insertions(+), 17 deletions(-) create mode 100644 static/icons/manual_call.svg diff --git a/bts/admin.js b/bts/admin.js index 658ba84..7062c2d 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -454,6 +454,15 @@ function handle_begin_to_play_call(app, ws, msg) { ws.respond(msg); } +function handle_announce_match_manually(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'match'])) { + return; + } + notify_change(app, msg.tournament_key, 'match_called_on_court', msg.match); + ws.respond(msg); +} + + function handle_free_announce(app, ws, msg) { if (!_require_msg(ws, msg, ['text'])) { return; @@ -681,6 +690,7 @@ module.exports = { async_handle_match_delete, async_handle_tournament_upload_logo, handle_begin_to_play_call, + handle_announce_match_manually, handle_btp_fetch, handle_tabletoperator_add, handle_tabletoperator_remove, diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 1d27dbd..b49452f 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -13,6 +13,7 @@ .match_scoresheet_button, .match_preparation_call_button, .match_begin_to_play_button, +.match_manual_call_button, .match_second_call_button, .match_rawinfo { display: inline-block; @@ -37,12 +38,18 @@ margin: 0 0 -0.1em 0.3em; background-image: url(../icons/preperation.svg); } - +.match_manual_call_button { + height: 1.5em; + min-height: 1em; + width: 2.3em; + margin: 0 0 -0.1em 0.3em; + background-image: url(../icons/manual_call.svg); +} .match_begin_to_play_button { height: 1.5em; - min-height: 1em; - width: 2.3em; - margin: 0 0 -0.1em 0.3em; + min-height: 1em; + width: 2.3em; + margin: 0 0 -0.1em 0.3em; background-image: url(../icons/start.svg); } @@ -75,6 +82,7 @@ .match_second_call_button:hover, .match_preparation_call_button:hover, .match_begin_to_play_button:hover, +.match_manual_call_button:hover, .match_scoresheet_button:hover { opacity: 0.5; } @@ -194,6 +202,7 @@ match_preparation_call_button, match_begin_to_play_button, match_second_call_button, +match_manual_call_button, .match_scoresheet_buttons { position: fixed; left: 0; diff --git a/static/icons/manual_call.svg b/static/icons/manual_call.svg new file mode 100644 index 0000000..da559ed --- /dev/null +++ b/static/icons/manual_call.svg @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index ed7a050..aacd085 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -131,6 +131,7 @@ var ci18n_de = { 'to_stats:total': 'Total', 'match:rawinfo': 'Technische Informationen', + 'match:manualcall': 'Aufruf: Manueller Spielaufruf', 'match:preparationcall': 'Aufruf: Spiel in Vorbereitung', 'match:begintoplay': 'Aufruf: Mit dem Spielen beginnen', 'match:secondcallteamone': 'Aufruf: Zweiter Aufruf Team 1', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index c2c976a..44891fc 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -132,6 +132,7 @@ var ci18n_en = { 'to_stats:total': 'Total', 'match:rawinfo': 'Technical information', + 'match:manualcall': 'Announcement: Manuall call of Match', 'match:preparationcall': 'Announcement: Match in preparation', 'match:begintoplay': 'Announcement: Beginn to play', 'match:secondcallteamone': 'Announcement: Second call Team 1', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 27a4a7b..2c7b8fe 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -209,6 +209,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ create_match_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); } else { + create_match_button(call_td, 'vlink match_manual_call_button', 'match:manualcall', on_announce_match_manually_button_click, match._id); create_match_button(call_td, 'vlink match_begin_to_play_button', 'match:begintoplay', on_begin_to_play_button_click, match._id); } } @@ -228,6 +229,13 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ uiu.hide(button_el); } }); + uiu.qsEach('.match_manual_call_button[data-match_id=' + JSON.stringify(match._id) + ']', (button_el) => { + if (match.setup.now_on_court) { + button_el.style.visibility = 'hidden'; + } else { + uiu.hide(button_el); + } + }); } } function create_match_button(targetEl, cssClass, title, listener, matchId,) { @@ -265,6 +273,9 @@ function update_match_score(m) { uiu.qsEach('.match_begin_to_play_button[data-match_id=' + JSON.stringify(m._id) + ']', (button_el) => { button_el.style.visibility = 'hidden'; }); + uiu.qsEach('.match_manual_call_button[data-match_id=' + JSON.stringify(m._id) + ']', (button_el) => { + button_el.style.visibility = 'hidden'; + }); } else { uiu.qsEach('.match_second_call_button[data-match_id=' + JSON.stringify(m._id) + ']', (button_el) => { button_el.style.visibility = 'visible'; @@ -272,6 +283,9 @@ function update_match_score(m) { uiu.qsEach('.match_begin_to_play_button[data-match_id=' + JSON.stringify(m._id) + ']', (button_el) => { button_el.style.visibility = 'visible'; }); + uiu.qsEach('.match_manual_call_button[data-match_id=' + JSON.stringify(m._id) + ']', (button_el) => { + button_el.style.visibility = 'visible'; + }); } @@ -736,20 +750,35 @@ function on_second_call_tabletoperator_button_click(e) { } } -function on_begin_to_play_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - send({ - type: 'begin_to_play_call', - tournament_key: curt.key, - setup: match.setup, - }, err => { - if (err) { - return cerror.net(err); - } - }); + function on_begin_to_play_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'begin_to_play_call', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } + } + + function on_announce_match_manually_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'announce_match_manually', + tournament_key: curt.key, + match: match, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } } -} function fetchMatchFromEvent(e) { const btn = e.target; const match_id = btn.getAttribute('data-match_id'); From 33e30bc8c4fa34141ca91f74981be1f9494f03d9 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 29 Jun 2024 14:43:23 +0200 Subject: [PATCH 108/224] Enhancement: Secondcall for Umpires and Tabletoperators now can be initiated seperatly --- bts/admin.js | 14 +++++++++++ static/js/announcements.js | 16 +++++++----- static/js/change.js | 3 +++ static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/cmatch.js | 50 ++++++++++++++++++++++++++------------ 6 files changed, 63 insertions(+), 22 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 7062c2d..a5c458a 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -488,6 +488,19 @@ function handle_second_call_tabletoperator(app, ws, msg) { ws.respond(msg); } +function handle_second_call_umpire(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + notify_change(app, tournament_key, 'second_call_umpire', { setup }); + + ws.respond(msg); +} + function handle_second_call_team_one(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { return; @@ -703,6 +716,7 @@ module.exports = { handle_ticker_pushall, handle_ticker_reset, handle_free_announce, + handle_second_call_umpire, handle_second_call_tabletoperator, handle_second_call_team_one, handle_second_call_team_two, diff --git a/static/js/announcements.js b/static/js/announcements.js index 334748e..32ef5c6 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -49,18 +49,22 @@ function announceSecondCallTabletoperator(matchSetup) { if (!(window.localStorage.getItem('enable_announcements') === 'true')) { return; } - const umpireCall = createUmpire(matchSetup);; - if (umpireCall != null) { - const call = createFieldAnnouncement(matchSetup) + createSecondCallAnnouncement() + umpireCall; - announce([call]); - } - const tabletOperatorCall = createTabletOperator(matchSetup);; if (tabletOperatorCall != null) { const call = createFieldAnnouncement(matchSetup) + createSecondCallAnnouncement() + tabletOperatorCall; announce([call]); } } +function announceSecondCallUmpire(matchSetup) { + if (!(window.localStorage.getItem('enable_announcements') === 'true')) { + return; + } + const umpireCall = createUmpire(matchSetup);; + if (umpireCall != null) { + const call = createFieldAnnouncement(matchSetup) + createSecondCallAnnouncement() + umpireCall; + announce([call]); + } +} function announceSecondCall(matchSetup, team) { if(!(window.localStorage.getItem('enable_announcements') === 'true')) { diff --git a/static/js/change.js b/static/js/change.js index c8f1db6..47d76fd 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -132,6 +132,9 @@ function default_handler_func(rerender, special_funcs, c) { case 'second_call_tabletoperator': announceSecondCallTabletoperator(c.val.setup); break; + case 'second_call_umpire': + announceSecondCallUmpire(c.val.setup); + break; case 'second_call_team_one': announceSecondCallTeamOne(c.val.setup); break; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index aacd085..bb4ec6b 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -136,6 +136,7 @@ var ci18n_de = { 'match:begintoplay': 'Aufruf: Mit dem Spielen beginnen', 'match:secondcallteamone': 'Aufruf: Zweiter Aufruf Team 1', 'match:secondcallteamtwo': 'Aufruf: Zweiter Aufruf Team 2', + 'match:secondcallumpire': 'Announcement: Zweiter Aufruf Schiedsrichter', 'match:secondcaltabletoperator': 'Aufruf: Zweiter Aufruf Tabletbediener', 'match:scoresheet': 'Schiedsrichterzettel', 'match:edit': 'Bearbeiten', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 44891fc..cd2dd9a 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -137,6 +137,7 @@ var ci18n_en = { 'match:begintoplay': 'Announcement: Beginn to play', 'match:secondcallteamone': 'Announcement: Second call Team 1', 'match:secondcallteamtwo': 'Announcement: Second call Team 2', + 'match:secondcallumpire': 'Announcement: Second call Umpire', 'match:secondcaltabletoperator': 'Announcement: Second call Tabletoperator', 'match:scoresheet': 'Scoresheet', 'match:edit': 'Edit', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 2c7b8fe..3e3a8cf 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -126,6 +126,9 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ if (setup.umpire_name) { uiu.el(to_td, 'div', 'umpire', ''); uiu.el(to_td, 'span', {}, setup.umpire_name); + if (style === 'default' || style === 'plain') { + create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcallumpire', on_second_call_umpire_button_click, match._id); + } if (setup.service_judge_name) { uiu.el(to_td, 'span', {}, ' \u200B+ '); uiu.el(to_td, 'span', {}, setup.service_judge_name); @@ -138,6 +141,9 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ uiu.el(to_td, 'span', 'match_no_umpire', ' \u200B/ '); uiu.el(to_td, 'span', 'match_no_umpire', setup.tabletoperators[1].name ); } + if (style === 'default' || style === 'plain') { + create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); + } } if (!setup.umpire_name && (!setup.tabletoperators || setup.tabletoperators.length == 0)) { @@ -149,9 +155,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ uiu.el(to_td, 'span', 'preperation', 'Spiel in Vorbereitung!'); } - if (style === 'default' || style === 'plain') { - create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); - } + const score_td = uiu.el(tr, 'td'); if (court && (court.match_id !== match._id) && (typeof match.team1_won !== 'boolean') && setup.umpire_name) { @@ -735,20 +739,34 @@ function on_second_call_team_two_button_click(e) { }); } } -function on_second_call_tabletoperator_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - send({ - type: 'second_call_tabletoperator', - tournament_key: curt.key, - setup: match.setup, - }, err => { - if (err) { - return cerror.net(err); - } - }); + function on_second_call_tabletoperator_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_call_tabletoperator', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } + } + function on_second_call_umpire_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_call_umpire', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } } -} function on_begin_to_play_button_click(e) { const match = fetchMatchFromEvent(e); From dfccd09f2bfdb9c63f01a1cb8774ffc6f04a4184 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Sat, 29 Jun 2024 15:14:35 +0200 Subject: [PATCH 109/224] Enhancement: Full integration of ServiceJudges in Announcements --- bts/admin.js | 14 ++++ static/css/cmatch.css | 31 ++++---- static/icons/service_judge.svg | 133 +++++++++++++++++++++++++++++++++ static/js/announcements.js | 24 +++++- static/js/change.js | 3 + static/js/ci18n_de.js | 4 +- static/js/ci18n_en.js | 2 + static/js/cmatch.js | 21 +++++- 8 files changed, 213 insertions(+), 19 deletions(-) create mode 100644 static/icons/service_judge.svg diff --git a/bts/admin.js b/bts/admin.js index a5c458a..f02cf95 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -500,6 +500,19 @@ function handle_second_call_umpire(app, ws, msg) { ws.respond(msg); } +function handle_second_call_servicejudge(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + notify_change(app, tournament_key, 'second_call_servicejudge', { setup }); + + ws.respond(msg); +} + function handle_second_call_team_one(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { @@ -717,6 +730,7 @@ module.exports = { handle_ticker_reset, handle_free_announce, handle_second_call_umpire, + handle_second_call_servicejudge, handle_second_call_tabletoperator, handle_second_call_team_one, handle_second_call_team_two, diff --git a/static/css/cmatch.css b/static/css/cmatch.css index b49452f..ce5b2ff 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -134,15 +134,16 @@ padding-left: 0.3em; } -.match_score_current > .tablet, -.match_score_current > .umpire { - min-width: 1em; - width: 1em; - min-height: 0.65em; - height: 0.65em; - margin: 0px 5px; - /*cursor: none;*/ -} + .match_score_current > .tablet, + .match_score_current > .service_judge, + .match_score_current > .umpire { + min-width: 1em; + width: 1em; + min-height: 0.65em; + height: 0.65em; + margin: 0px 5px; + /*cursor: none;*/ + } .match_score_current > .match_no_umpire, .match_score_current > span { @@ -276,6 +277,7 @@ match_manual_call_button, .tablet, .tablet_inline, .umpire, +.service_judge, .no_umpire, .court, .court_history { @@ -291,12 +293,7 @@ match_manual_call_button, text-align: center; -webkit-text-stroke-width: 1px; -webkit-text-stroke-color: black; - text-shadow: - 3px 3px 3px #000, - -1px -1px 3px #000, - 1px -1px 3px #000, - -1px 1px 3px #000, - 1px 1px 3px #000; + text-shadow: 3px 3px 3px #000, -1px -1px 3px #000, 1px -1px 3px #000, -1px 1px 3px #000, 1px 1px 3px #000; } .tablet, @@ -319,6 +316,10 @@ match_manual_call_button, background-image: url(../icons/umpire.svg); } +.service_judge { + background-image: url(../icons/service_judge.svg); +} + .no_umpire { background-image: url(../icons/no_umpire.svg); } diff --git a/static/icons/service_judge.svg b/static/icons/service_judge.svg new file mode 100644 index 0000000..cc0f4bc --- /dev/null +++ b/static/icons/service_judge.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/static/js/announcements.js b/static/js/announcements.js index 32ef5c6..d008689 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -10,8 +10,9 @@ function announceNewMatch(matchSetup) { const round = createRoundAnnouncement(matchSetup); const teams = createTeamAnnouncement(matchSetup); const umpire = createUmpire(matchSetup); + const serviceJudge = createServiceJudge(matchSetup); const tabletOperator = createTabletOperator(matchSetup); - announce([field, matchNumber, eventName, round, teams, umpire, tabletOperator, field]); + announce([field, matchNumber, eventName, round, teams, umpire, serviceJudge, tabletOperator, field]); } function announcePreparationMatch(matchSetup) { @@ -24,12 +25,13 @@ function announcePreparationMatch(matchSetup) { var round = createRoundAnnouncement(matchSetup); var teams = createTeamAnnouncement(matchSetup); const umpire = createUmpire(matchSetup); + const serviceJudge = createServiceJudge(matchSetup); const tabletOperator = createTabletOperator(matchSetup); var lastPart = preparation; if (curt.preparation_meetingpoint_enabled) { lastPart = createMeetingPointAnnouncement(); } - announce([preparation, matchNumber, eventName, round, teams, umpire, tabletOperator, lastPart]); + announce([preparation, matchNumber, eventName, round, teams, umpire, serviceJudge, tabletOperator, lastPart]); } function announceSecondCallTeamOne(matchSetup) { if(!(window.localStorage.getItem('enable_announcements') === 'true')) { @@ -65,6 +67,17 @@ function announceSecondCallUmpire(matchSetup) { announce([call]); } } +function announceSecondCallServiceJudge(matchSetup) { + if (!(window.localStorage.getItem('enable_announcements') === 'true')) { + return; + } + const servicejudgeCall = createServiceJudge(matchSetup);; + if (servicejudgeCall != null) { + const call = createFieldAnnouncement(matchSetup) + createSecondCallAnnouncement() + servicejudgeCall; + announce([call]); + } +} + function announceSecondCall(matchSetup, team) { if(!(window.localStorage.getItem('enable_announcements') === 'true')) { @@ -104,6 +117,13 @@ function createUmpire(matchSetup) { return null; } +function createServiceJudge(matchSetup) { + if (matchSetup.service_judge_name && matchSetup.service_judge_name != null) { + return ci18n('announcements:service_judge') + normalizeNames(matchSetup.service_judge_name); + } + return null; +} + function createSingleTeam(playersSetup) { var team = normalizeNames(playersSetup[0].name); if (playersSetup.length == 2) { diff --git a/static/js/change.js b/static/js/change.js index 47d76fd..ed2431e 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -135,6 +135,9 @@ function default_handler_func(rerender, special_funcs, c) { case 'second_call_umpire': announceSecondCallUmpire(c.val.setup); break; + case 'second_call_servicejudge': + announceSecondCallServiceJudge(c.val.setup); + break; case 'second_call_team_one': announceSecondCallTeamOne(c.val.setup); break; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index bb4ec6b..61355cd 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -52,6 +52,7 @@ var ci18n_de = { 'announcements:counting_board_service': 'Klapptafelbedienung:', 'announcements:table_service': 'Tabletbedienung:', 'announcements:umpire': 'Schiedsrichter:', + 'announcements:service_judge': 'Aufschlagrichter:', 'announcements:and': ' und ', 'announcements:preparation': 'In Vorbereitung:', 'announcements:meetingpoint': 'Treffen am Meetingpoint!', @@ -136,7 +137,8 @@ var ci18n_de = { 'match:begintoplay': 'Aufruf: Mit dem Spielen beginnen', 'match:secondcallteamone': 'Aufruf: Zweiter Aufruf Team 1', 'match:secondcallteamtwo': 'Aufruf: Zweiter Aufruf Team 2', - 'match:secondcallumpire': 'Announcement: Zweiter Aufruf Schiedsrichter', + 'match:secondcallumpire': 'Aufruf: Zweiter Aufruf Schiedsrichter', + 'match:secondcallservicejudge': 'Aufruf: Zweiter Aufruf Aufschlagrichter', 'match:secondcaltabletoperator': 'Aufruf: Zweiter Aufruf Tabletbediener', 'match:scoresheet': 'Schiedsrichterzettel', 'match:edit': 'Bearbeiten', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index cd2dd9a..a35da56 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -52,6 +52,7 @@ var ci18n_en = { 'announcements:counting_board_service': 'Countingboard service:', 'announcements:table_service': 'Tablet service:', 'announcements:umpire': 'Umpire:', + 'announcements:service_judge': 'Servicejudge:', 'announcements:and': ' and ', 'announcements:preparation': 'In preparation:', 'announcements:meetingpoint': 'Come to the meetingpoint!', @@ -138,6 +139,7 @@ var ci18n_en = { 'match:secondcallteamone': 'Announcement: Second call Team 1', 'match:secondcallteamtwo': 'Announcement: Second call Team 2', 'match:secondcallumpire': 'Announcement: Second call Umpire', + 'match:secondcallservicejudge': 'Announcement: Second call Servicejudge', 'match:secondcaltabletoperator': 'Announcement: Second call Tabletoperator', 'match:scoresheet': 'Scoresheet', 'match:edit': 'Edit', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 3e3a8cf..35ddcf7 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -130,8 +130,11 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcallumpire', on_second_call_umpire_button_click, match._id); } if (setup.service_judge_name) { - uiu.el(to_td, 'span', {}, ' \u200B+ '); + uiu.el(to_td, 'div', 'service_judge', ''); uiu.el(to_td, 'span', {}, setup.service_judge_name); + if (style === 'default' || style === 'plain') { + create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcallservicejudge', on_second_call_servicejudge_button_click, match._id); + } } } if (setup.tabletoperators && setup.tabletoperators.length > 0) { @@ -768,6 +771,22 @@ function on_second_call_team_two_button_click(e) { } } + function on_second_call_servicejudge_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_call_servicejudge', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } + } + + function on_begin_to_play_button_click(e) { const match = fetchMatchFromEvent(e); if (match != null) { From 4ac2121dfd07d7ae63973391734b3a57ec47faf2 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Mon, 1 Jul 2024 13:52:18 +0200 Subject: [PATCH 110/224] Enhancement: Status of different Components of BTS and ErrorMessages will be displaied compact in Tournament Settings Region in te upper right. --- bts/btp_conn.js | 22 ++++---- bts/btp_manager.js | 2 +- bts/ticker_conn.js | 22 ++++---- bts/ticker_manager.js | 3 +- static/cbts.html | 4 -- static/css/admin.css | 57 ++++++++++++++------- static/icons/signal_black.svg | 93 ++++++++++++++++++++++++++++++++++ static/icons/signal_green.svg | 93 ++++++++++++++++++++++++++++++++++ static/icons/signal_red.svg | 93 ++++++++++++++++++++++++++++++++++ static/icons/signal_yellow.svg | 93 ++++++++++++++++++++++++++++++++++ static/js/cerror.js | 8 +-- static/js/change.js | 6 --- static/js/ci18n_de.js | 6 +-- static/js/ci18n_en.js | 6 +-- static/js/conn_ui.js | 4 +- static/js/ctournament.js | 62 +++++++++++++++++++---- 16 files changed, 504 insertions(+), 70 deletions(-) create mode 100644 static/icons/signal_black.svg create mode 100644 static/icons/signal_green.svg create mode 100644 static/icons/signal_red.svg create mode 100644 static/icons/signal_yellow.svg diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 53bd3fa..35ebbff 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -89,21 +89,21 @@ class BTPConn { return; } - this.report_status('Connecting ...'); + this.report_status('connecting','Try to establish connection to BTP.'); this.send(btp_proto.login_request(this.password), response => { if (!response.Action || !response.Action[0] || !response.Action[0].ID[0] || (response.Action[0].ID[0] !== 'REPLY')) { - this.report_status('Invalid reply to login request'); + this.report_status('error','Invalid reply to login request'); this.schedule_reconnect(); return; } if (response.Action[0].Result[0] !== 1) { - this.report_status('Invalid password'); + this.report_status('error', 'Invalid password'); this.schedule_reconnect(); return; } - this.report_status('Logged in.'); + this.report_status('connected','Logged in.'); this.key_unicode = response.Action[0].Unicode[0]; this.pushall(); @@ -119,7 +119,7 @@ class BTPConn { this.send(ir, response => { btp_sync.fetch(this.app, this.tkey, response, (err) => { if (err) { - this.report_status('Synchronisations-Fehler: ' + err.stack); + this.report_status('error','Synchronisations-Fehler: ' + err.stack); console.error(err.stack); } }); @@ -142,7 +142,7 @@ class BTPConn { terminate() { this.terminated = true; - this.report_status('Terminated.'); + this.report_status('deactivated','Terminated.'); } send(xml_req, success_cb) { @@ -151,7 +151,7 @@ class BTPConn { const port = this.is_team ? BLP_PORT : BTP_PORT; send_request(this.ip, port, xml_req, this.timeZone, (err, response) => { if (err) { - this.report_status('Connection error: ' + err.message); + this.report_status('error', 'Connection error: ' + err.message); this.schedule_reconnect(); return; } @@ -195,11 +195,15 @@ class BTPConn { } on_end() { - this.report_status('Verbindung verloren, versuche erneut ...'); + this.report_status('error', 'Verbindung verloren, versuche erneut ...'); this.schedule_reconnect(); } - report_status(msg) { + report_status(status, message) { + const msg = { + status: status, + message: message + } this.last_status = msg; const admin = require('./admin'); admin.notify_change(this.app, this.tkey, 'btp_status', msg); diff --git a/bts/btp_manager.js b/bts/btp_manager.js index e2e1ffd..767e82a 100644 --- a/bts/btp_manager.js +++ b/bts/btp_manager.js @@ -103,7 +103,7 @@ function init(app, cb) { function get_status(tkey) { const conn = conns_by_tkey.get(tkey); if (!conn) { - return 'deactivated.'; + return { status: 'deactivated', message: '' }; } return conn.last_status; diff --git a/bts/ticker_conn.js b/bts/ticker_conn.js index 5ff353c..4bce812 100644 --- a/bts/ticker_conn.js +++ b/bts/ticker_conn.js @@ -43,9 +43,9 @@ class TickerConn { return; } - this.report_status('Verbindung wird hergestellt ...'); + this.report_status('connecting','Verbindung wird hergestellt ...'); if (!/^wss?:\/\/.*\/update/.test(this.url)) { - this.report_status('Ungültige Ticker-URL: ' + JSON.stringify(this.url)); + this.report_status('error','Ungültige Ticker-URL: ' + JSON.stringify(this.url)); return; } const ws_url = this.url + '?password=' + encodeURIComponent(this.password); @@ -53,7 +53,7 @@ class TickerConn { const tc = this; tc.ws = ws; ws.on('open', function() { - tc.report_status('Connected.'); + tc.report_status('connected',''); tc.pushall(); }); ws.on('message', function(data) { @@ -61,11 +61,11 @@ class TickerConn { try { msg = JSON.parse(data); } catch (e) { - tc.report_status('Failed to receive ticker message: ' + e.message); + tc.report_status('error', 'Failed to receive ticker message: ' + e.message); return; } if ((msg.type === 'error') || ((msg.type === 'dmsg') && (msg.dtype === 'error'))) { - tc.report_status('Error: ' + msg.message); + tc.report_status('error', msg.message); } }); ws.on('error', function() { @@ -91,7 +91,7 @@ class TickerConn { ws.close(); } this.terminated = true; - this.report_status('Ended.'); + this.report_status('deactivated'); } schedule_reconnect() { @@ -105,7 +105,7 @@ class TickerConn { this._craft_event((err, event) => { if (err) { serror.silent('Failed to craft event: ' + err.message + ' ' + err.stack); - this.report_status('Failed to craft data'); + this.report_status('error','Failed to craft data'); return; } @@ -140,11 +140,15 @@ class TickerConn { on_end() { this.ws = null; - this.report_status('Verbindung verloren, versuche erneut ...'); + this.report_status('error','Verbindung verloren, versuche erneut ...'); this.schedule_reconnect(); } - report_status(msg) { + report_status(status, message) { + const msg = { + status: status, + message: message + } this.last_status = msg; const admin = require('./admin'); admin.notify_change(this.app, this.tournament_key, 'ticker_status', msg); diff --git a/bts/ticker_manager.js b/bts/ticker_manager.js index 4acc258..b5c34ab 100644 --- a/bts/ticker_manager.js +++ b/bts/ticker_manager.js @@ -71,9 +71,8 @@ function init(app, cb) { function get_status(tkey) { const conn = conns_by_tkey.get(tkey); if (!conn) { - return 'deactivated.'; + return { status: 'deactivated', message: '' }; } - return conn.last_status; } diff --git a/static/cbts.html b/static/cbts.html index 62cdcd7..18bceb4 100644 --- a/static/cbts.html +++ b/static/cbts.html @@ -21,10 +21,6 @@
    -
    -
    -
    -
    diff --git a/static/css/admin.css b/static/css/admin.css index 17f3164..ef5fd35 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -33,37 +33,25 @@ h3 { background: rgba(255, 255, 255, 0.8); font-size: 50px; } - +.status_label, .status { - position: fixed; - bottom: 0; - right: 0; color: #666; - background: rgba(255, 255, 255, 0.8); } +.btp_status_label, .btp_status { - position: fixed; - bottom: 1.5em; - right: 0; color: #666; - background: rgba(255, 255, 255, 0.8); } +.ticker_status_label, .ticker_status { - position: fixed; - bottom: 3em; - right: 0; color: #666; - background: rgba(255, 255, 255, 0.8); } .errors { - position: fixed; - bottom: 4.5em; - right: 0.5em; color: #f00; - background: rgba(255, 255, 255, 0.8); white-space: pre-wrap; + max-height: 50px; + overflow-y: scroll; } .vlink, @@ -86,8 +74,7 @@ a:hover { .tournament_btp_fetch, .tournament_ticker_push, .tournament_settings_link { - float: right; - margin-right: 0.5em; + display: inline-block; } .tournament_settings fieldset { @@ -178,3 +165,35 @@ table.striped-table { justify-content: space-evenly; } +.status_error, +.status_deactivated, +.status_connecting, +.status_connected { + display: inline-block; + min-width: 1em; + min-height: 1em; + background-size: 100%; + background-repeat: no-repeat; + background-position: center center; + height: 1.2em; + min-height: 1em; + width: 1.9em; + margin: 0 0.2em -0.2em 0.2em; +} + +.status_connected { + background-image: url(../icons/signal_green.svg); +} + +.status_connecting { + background-image: url(../icons/signal_yellow.svg); +} + +.status_error { + background-image: url(../icons/signal_red.svg); +} + +.status_deactivated { + background-image: url(../icons/signal_black.svg); +} + diff --git a/static/icons/signal_black.svg b/static/icons/signal_black.svg new file mode 100644 index 0000000..efa6ff0 --- /dev/null +++ b/static/icons/signal_black.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/static/icons/signal_green.svg b/static/icons/signal_green.svg new file mode 100644 index 0000000..a3d6165 --- /dev/null +++ b/static/icons/signal_green.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/static/icons/signal_red.svg b/static/icons/signal_red.svg new file mode 100644 index 0000000..9dbed28 --- /dev/null +++ b/static/icons/signal_red.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/static/icons/signal_yellow.svg b/static/icons/signal_yellow.svg new file mode 100644 index 0000000..3febeab --- /dev/null +++ b/static/icons/signal_yellow.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/static/js/cerror.js b/static/js/cerror.js index 2f2ee2c..fbf946c 100644 --- a/static/js/cerror.js +++ b/static/js/cerror.js @@ -9,10 +9,12 @@ var error_list = []; var report_enabled = true; function show(msg) { - error_list.push(msg); + error_list.unshift(msg); if (typeof uiu !== 'undefined') { - uiu.show_qs('.errors'); - uiu.text_qs('.errors', error_list.join('\n')); + try { + uiu.show_qs('.errors'); + uiu.text_qs('.errors', error_list.join('\n')); + } catch (e) { } } } diff --git a/static/js/change.js b/static/js/change.js index ed2431e..3017c36 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -158,12 +158,6 @@ function default_handler_func(rerender, special_funcs, c) { change_current_match(c.val); // Most dialogs don't show any matches, so do not rerender break; - case 'btp_status': - uiu.text_qs('.btp_status', 'BTP status: ' + c.val); - break; - case 'ticker_status': - uiu.text_qs('.ticker_status', 'Ticker status: ' + c.val); - break; case 'update_player_status': //nothing todo here break; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 61355cd..824b73b 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -5,7 +5,7 @@ var ci18n_de = { 'Unassigned Matches': 'Nicht zugewiesene Spiele', 'Next Matches': 'Nächste Spiele', - 'edit tournament': 'Turnier bearbeiten', + 'edit tournament': 'bearbeiten', 'Court': 'Court', 'Match': 'Spiel', 'Players': 'Spieler', @@ -33,8 +33,8 @@ var ci18n_de = { 'Not assigned': 'Nicht zugewiesen', 'team competition': 'Mannschafts-Wettbewerb', 'nation competition': 'Internationales Turnier', - 'update from BTP': 'Von BTP aktualisieren', - 'update ticker': 'Ticker aktualisieren', + 'update from BTP': 'aktualisieren', + 'update ticker': 'aktualisieren', 'Tournaments': 'Turniere', 'referee view': 'Referee-Ansicht', 'Connecting ...': 'Verbinde ...', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index a35da56..b414738 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -5,7 +5,7 @@ var ci18n_en = { 'Unassigned Matches': 'Unassigned Matches', 'Next Matches': 'Next Matches', - 'edit tournament': 'edit tournament', + 'edit tournament': 'edit', 'Court': 'Court', 'Match': 'Match', 'Players': 'Players', @@ -33,8 +33,8 @@ var ci18n_en = { 'Not assigned': 'Not assigned', 'team competition': 'team competition', 'nation competition': 'international competition', - 'update from BTP': 'update from BTP', - 'update ticker': 'update ticker', + 'update from BTP': 'update', + 'update ticker': 'update', 'Tournaments': 'Tournaments', 'referee view': 'Referee view', 'Connecting ...': 'Connecting ...', diff --git a/static/js/conn_ui.js b/static/js/conn_ui.js index 7415efe..1b91b4a 100644 --- a/static/js/conn_ui.js +++ b/static/js/conn_ui.js @@ -16,7 +16,8 @@ function ui_on_status(status) { text = 'Unsupported status: ' + status.code; } - uiu.text_qs('.status', text); + ctournament.bts_status_changed({ val: { status: status.code, message: text } }) + uiu.visible_qs('.connecting', (status.code === 'connecting') || (status.code === 'waiting')); if (status.code === 'connected') { @@ -56,6 +57,7 @@ if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var ci18n = require('./ci18n'); var conn = require('./conn'); var crouting = require('./crouting'); + var ctournament = require('./ctournament'); var uiu = null; // UI only module.exports = conn_ui; diff --git a/static/js/ctournament.js b/static/js/ctournament.js index ff1883d..f50b2cb 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -26,8 +26,6 @@ var ctournament = (function() { if (curt.language && curt.language !== 'auto') { ci18n.switch_language(curt.language); } - uiu.text_qs('.btp_status', 'BTP status: ' + curt.btp_status); - uiu.text_qs('.ticker_status', 'Ticker status: ' + curt.ticker_status); success_cb(); }); } @@ -399,24 +397,71 @@ var ctournament = (function() { match_remove: remove_match, tabletoperator_add: tabletoperator_add, tabletoperator_removed: tabletoperator_removed, + btp_status: btp_status_changed, + ticker_status: ticker_status_changed, })); function render_settings(target) { - const settings_div = uiu.el(target, 'div', 'metadata_right_container'); + const settings_div = uiu.el(target, 'div', 'metadata_right_container_2'); uiu.el(settings_div, 'h3', {}, 'Turnier-Einstellungen'); - const settings_btn = uiu.el(settings_div, 'div', 'tournament_settings_link vlink', ci18n('edit tournament')); + + const settings_table = uiu.el(settings_div, 'table'); + var tr = uiu.el(settings_table, 'tr'); + var td = uiu.el(tr, 'td'); + uiu.el(td, 'div', 'status_label', 'BTS status:'); + var td = uiu.el(tr, 'td'); + uiu.el(td, 'div', 'status status_connected',''); + var td = uiu.el(tr, 'td'); + const settings_btn = uiu.el(td, 'button', 'tournament_settings_link vlink', ci18n('edit tournament')); settings_btn.addEventListener('click', ui_edit); + var tr = uiu.el(settings_table, 'tr'); + var td = uiu.el(tr, 'td'); + uiu.el(td, 'div', 'btp_status_label', 'BTP status:'); + var td = uiu.el(tr, 'td'); + uiu.el(td, 'div', 'btp_status', ''); + btp_status_changed({ val: curt.btp_status }); + var td = uiu.el(tr, 'td'); if (curt.btp_enabled) { - const btp_fetch_btn = uiu.el(settings_div, 'button', 'tournament_btp_fetch', ci18n('update from BTP')); + const btp_fetch_btn = uiu.el(td, 'button', 'tournament_btp_fetch vlink', ci18n('update from BTP')); btp_fetch_btn.addEventListener('click', ui_btp_fetch); } + var tr = uiu.el(settings_table, 'tr'); + var td = uiu.el(tr, 'td'); + uiu.el(td, 'div', 'ticker_status_label', 'Ticker status:'); + var td = uiu.el(tr, 'td'); + uiu.el(td, 'div', 'ticker_status', ''); + ticker_status_changed({ val: curt.ticker_status }); + var td = uiu.el(tr, 'td'); if (curt.ticker_enabled) { - const ticker_push_btn = uiu.el(settings_div, 'button', 'tournament_ticker_push', ci18n('update ticker')); + const ticker_push_btn = uiu.el(td, 'button', 'tournament_ticker_push vlink', ci18n('update ticker')); ticker_push_btn.addEventListener('click', ui_ticker_push); } + + uiu.el(settings_div, 'div', 'errors'); } + function btp_status_changed(c) { + set_service_status('btp_status', c); + } + function ticker_status_changed(c) { + set_service_status('ticker_status', c); + } + + function bts_status_changed(c) { + set_service_status('status', c); + } + + function set_service_status(service_id, c) { + if (curt) { + curt[service_id] = c.val.status; + } + uiu.qsEach('.' + service_id, (div_el) => { + div_el.className = service_id +' status_' + c.val.status; + div_el.title = c.val.message; + }); + } + function _upload_logo(e) { const input = e.target; if (!input.files.length) return; @@ -1138,10 +1183,6 @@ var ctournament = (function() { uiu.empty(main); main.classList.add('main_upcoming'); - uiu.hide_qs('.btp_status'); - uiu.hide_qs('.ticker_status'); - uiu.hide_qs('.status'); - render_upcoming(main); main.addEventListener('click', () => { fullscreen.toggle(); @@ -1332,6 +1373,7 @@ var ctournament = (function() { ui_list, update_match, update_upcoming_match, + bts_status_changed }; })(); From 90e922537a9668d1f9585862fbba948baadf4fa5 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Mon, 1 Jul 2024 17:11:50 +0200 Subject: [PATCH 111/224] Enhancement: One Connectionstat was not fullfilled. Cleanup the CSS an Reduce Information and Space in GUI once again --- static/css/admin.css | 8 ++++---- static/js/ctournament.js | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/static/css/admin.css b/static/css/admin.css index ef5fd35..57375ab 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -166,18 +166,17 @@ table.striped-table { } .status_error, +.status_waiting, .status_deactivated, .status_connecting, .status_connected { display: inline-block; - min-width: 1em; - min-height: 1em; background-size: 100%; background-repeat: no-repeat; background-position: center center; - height: 1.2em; + height: 1.0em; min-height: 1em; - width: 1.9em; + width: 1.0em; margin: 0 0.2em -0.2em 0.2em; } @@ -189,6 +188,7 @@ table.striped-table { background-image: url(../icons/signal_yellow.svg); } +.status_waiting, .status_error { background-image: url(../icons/signal_red.svg); } diff --git a/static/js/ctournament.js b/static/js/ctournament.js index f50b2cb..75e5feb 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -408,7 +408,7 @@ var ctournament = (function() { const settings_table = uiu.el(settings_div, 'table'); var tr = uiu.el(settings_table, 'tr'); var td = uiu.el(tr, 'td'); - uiu.el(td, 'div', 'status_label', 'BTS status:'); + uiu.el(td, 'div', 'status_label', 'BTS'); var td = uiu.el(tr, 'td'); uiu.el(td, 'div', 'status status_connected',''); var td = uiu.el(tr, 'td'); @@ -417,7 +417,7 @@ var ctournament = (function() { var tr = uiu.el(settings_table, 'tr'); var td = uiu.el(tr, 'td'); - uiu.el(td, 'div', 'btp_status_label', 'BTP status:'); + uiu.el(td, 'div', 'btp_status_label', 'BTP'); var td = uiu.el(tr, 'td'); uiu.el(td, 'div', 'btp_status', ''); btp_status_changed({ val: curt.btp_status }); @@ -428,7 +428,7 @@ var ctournament = (function() { } var tr = uiu.el(settings_table, 'tr'); var td = uiu.el(tr, 'td'); - uiu.el(td, 'div', 'ticker_status_label', 'Ticker status:'); + uiu.el(td, 'div', 'ticker_status_label', 'Ticker'); var td = uiu.el(tr, 'td'); uiu.el(td, 'div', 'ticker_status', ''); ticker_status_changed({ val: curt.ticker_status }); From d6c3a381150a7a425fcfd90bb254493a254c16e4 Mon Sep 17 00:00:00 2001 From: "t.englich" Date: Mon, 1 Jul 2024 17:47:33 +0200 Subject: [PATCH 112/224] GUI Improvemt: Tabletoperatorlist ane Input will be visualized in fixed size --- static/css/ctabletoperator.css | 17 +++++++++++++++-- static/js/ctabletoperator.js | 9 +++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/static/css/ctabletoperator.css b/static/css/ctabletoperator.css index f4cb744..2f0de49 100644 --- a/static/css/ctabletoperator.css +++ b/static/css/ctabletoperator.css @@ -6,9 +6,9 @@ height: 25px; background-size: cover; width: 25px; - margin: 0 0.2em -0.2em 0.2em; background-image: url(../icons/tabletoperator_add.svg); - border-width: 0px; + padding: 0.4em 0.4em; + margin: 0.4em 0 0 0.25em; } .tabletoperator_add_button { display: inline-block; @@ -55,3 +55,16 @@ padding: 0.1em 0.1em; min-width: 5em; } + +.unassigned_tableoperators_content { + height: 130px; + max-height: 130px; + width: 325px; + max-height: 325px; + overflow: auto; +} + +.tabletoperator_add_custom_input { + width: 290px; +} + diff --git a/static/js/ctabletoperator.js b/static/js/ctabletoperator.js index 8b0c253..55c16d8 100644 --- a/static/js/ctabletoperator.js +++ b/static/js/ctabletoperator.js @@ -6,14 +6,15 @@ function render_unassigned(container) { uiu.empty(container); uiu.el(container, 'h3', {}, ci18n('tabletoperator:unassigned')); const unassigned_tabletoperators = curt.tabletoperators.filter(m => m.court == null); - render_tabletoperator_table(container, unassigned_tabletoperators); + const tableoperator_content = uiu.el(container, 'div', 'unassigned_tableoperators_content'); + render_tabletoperator_table(tableoperator_content, unassigned_tabletoperators); render_tabletoperator_formular(container); } function render_tabletoperator_table(container, tabletoperators) { const table = uiu.el(container, 'table', 'tabletoperators_table'); - render_tabletoperator_table_header(table); + //render_tabletoperator_table_header(table); const tbody = uiu.el(table, 'tbody'); for (const t of tabletoperators) { @@ -85,12 +86,12 @@ function render_tabletoperator_formular(target) { const form = uiu.el(announcements, 'form'); uiu.el(form, 'input', { type: 'input', + class: 'tabletoperator_add_custom_input', id: 'tabletoperator_name', name: 'tabletoperator_name' }); const btp_fetch_btn = uiu.el(form, 'button', { - 'class': 'vlink tabletoperator_add_custom_button', - height: 50, + class: 'vlink tabletoperator_add_custom_button', role: 'submit', }); form_utils.onsubmit(form, function (d) { From b78db279b1200db151713bdf36915178b991b56b Mon Sep 17 00:00:00 2001 From: tengmel Date: Tue, 2 Jul 2024 11:52:25 +0200 Subject: [PATCH 113/224] Cleanup Errorhandlin in Client --- static/js/cerror.js | 120 ++++++++++++++------------------------------ 1 file changed, 39 insertions(+), 81 deletions(-) diff --git a/static/js/cerror.js b/static/js/cerror.js index fbf946c..b0ef6f6 100644 --- a/static/js/cerror.js +++ b/static/js/cerror.js @@ -3,99 +3,57 @@ var cerror = (function() { -var REPORT_URL = 'https://aufschlagwechsel.de/bupbug/'; -var count = -1; -var error_list = []; -var report_enabled = true; -function show(msg) { - error_list.unshift(msg); - if (typeof uiu !== 'undefined') { - try { - uiu.show_qs('.errors'); - uiu.text_qs('.errors', error_list.join('\n')); - } catch (e) { } + var error_list = []; + var report_enabled = true; + + function show(msg) { + error_list.unshift(msg); + if (typeof uiu !== 'undefined') { + try { + uiu.show_qs('.errors'); + uiu.text_qs('.errors', error_list.join('\n')); + } catch (e) { } + } } -} - -function get_platform_info() { - return { - size: document.documentElement.clientWidth + 'x' + document.documentElement.clientHeight, - ua: window.navigator.userAgent, - }; -} -function on_error(msg, script_url, line, col, err) { - show(msg); - if (! report_enabled) { - return; + function on_error(msg, script_url, line, col, err) { + show(msg); } - count++; - if (count > 5) { - return; + function silent(msg) { + console.error(msg); // eslint-disable-line no-console + on_error(msg, undefined, undefined, undefined, new Error()); } - var report = { - msg, - count, - _type: 'bts-error', - bts_type: 'client', - platform: get_platform_info(), - }; - if (script_url !== undefined) { - report.script_url = script_url; - } - if (line !== undefined) { - report.line = line; - } - if (col !== undefined) { - report.col = col; + function net(err) { + silent(err.message); } - if (err) { - report.stack = err.stack; - } - - var report_json = JSON.stringify(report); - var xhr = new XMLHttpRequest(); - xhr.open('POST', REPORT_URL, true); - xhr.setRequestHeader('Content-type', 'text/plain'); // To be a simple CORS request (avoid CORS preflight) - xhr.send(report_json); -} - -function silent(msg) { - console.error(msg); // eslint-disable-line no-console - on_error(msg, undefined, undefined, undefined, new Error()); -} -function net(err) { - silent(err.message); -} - -function init() { - var report_enabled_json = document.getElementById('bts-data-holder').getAttribute('data-error-reporting'); - try { - report_enabled = JSON.parse(report_enabled_json); - } catch(e) { - silent('Error reporting JSON invalid: ' + report_enabled_json); - return; - } - if (report_enabled === null) { - silent('Error reporting not configured'); - return; - } - if (report_enabled) { - window.onerror = on_error; + function init() { + var report_enabled_json = document.getElementById('bts-data-holder').getAttribute('data-error-reporting'); + try { + report_enabled = JSON.parse(report_enabled_json); + } catch(e) { + silent('Error reporting JSON invalid: ' + report_enabled_json); + return; + } + if (report_enabled === null) { + silent('Error reporting not configured'); + return; + } + if (report_enabled) { + window.onerror = on_error; + } } -} -return { - init, - net, - on_error, - silent, -}; + return { + init, + net, + on_error, + silent, + }; })(); From b8eb4343e82a10e397ed84942c8b52d44e913e93 Mon Sep 17 00:00:00 2001 From: tengmel Date: Tue, 2 Jul 2024 14:10:07 +0200 Subject: [PATCH 114/224] Consolidation: Removed unused configuration entry only_now_on_court. Removed all obsolet code which was covered by this entry --- bts/admin.js | 2 +- bts/btp_sync.js | 5 +---- bts/bupws.js | 2 +- bts/http_api.js | 6 ++---- static/js/change.js | 23 ++-------------------- static/js/ci18n_de.js | 1 - static/js/ci18n_en.js | 1 - static/js/cmatch.js | 41 ++++++++++------------------------------ static/js/ctournament.js | 20 +------------------- 9 files changed, 18 insertions(+), 83 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index f02cf95..0a49061 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -55,7 +55,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'name','tguid', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'btp_ip', 'btp_password', - 'is_team', 'is_nation_competition', 'only_now_on_court', + 'is_team', 'is_nation_competition', 'warmup', 'warmup_ready', 'warmup_start', 'ticker_enabled', 'ticker_url', 'ticker_password', 'language', 'dm_style', 'displaysettings_general', diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 3d68489..f536559 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -848,10 +848,7 @@ async function integrate_now_on_court(app, tkey, callback) { return callback(err); } assert(tournament); - if (!tournament.only_now_on_court) { - return callback(null); // Nothing to do here - } - + app.db.matches.find({ 'setup.now_on_court': true }, async (err, now_on_court_matches) => { if (err) return callback(err); diff --git a/bts/bupws.js b/bts/bupws.js index 33ce30f..cf16f28 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -329,7 +329,7 @@ function matches_handler(app, ws, tournament_key, court_id) { if(db_matches){ let matches = db_matches.map(dbm => create_match_representation(tournament, dbm)); - if (!court_id && tournament.only_now_on_court) { + if (!court_id) { matches = matches.filter(m => m.setup.now_on_court); } diff --git a/bts/http_api.js b/bts/http_api.js index 027ffd2..fc9c66e 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -153,10 +153,8 @@ function matches_handler(req, res) { } let matches = db_matches.map(dbm => create_match_representation(tournament, dbm)); - if (tournament.only_now_on_court) { - matches = matches.filter(m => m.setup.now_on_court); - } - + matches = matches.filter(m => m.setup.now_on_court); + db_courts.sort(utils.cmp_key('num')); const courts = db_courts.map(function(dc) { var res = { diff --git a/static/js/change.js b/static/js/change.js index 3017c36..5f5ce8e 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -21,16 +21,6 @@ function change_score(cval) { m.team1_won = cval.team1_won; } -//function change_current_match(cval) { -// // Do not use courts_by_id since that may not be initialized in all views -// const court = utils.find(curt.courts, c => c._id === cval.court_id); -// if (court) { -// court.match_id = cval.match_id; -// } else { -// cerror.silent('Cannot find court ' + JSON.stringify(cval.court_id)); -// } -//} - function default_handler_func(rerender, special_funcs, c) { if (special_funcs && special_funcs[c.ctype]) { special_funcs[c.ctype](c); @@ -46,7 +36,6 @@ function default_handler_func(rerender, special_funcs, c) { curt.is_team = c.val.is_team; curt.tguid = c.val.tguid; curt.is_nation_competition = c.val.is_nation_competition; - curt.only_now_on_court = c.val.only_now_on_court; curt.btp_timezone = c.val.btp_timezone; curt.warmup = c.val.warmup; curt.warmup_ready = c.val.warmup_ready; @@ -68,7 +57,7 @@ function default_handler_func(rerender, special_funcs, c) { } }); const CHECKBOXES = [ - 'is_team', 'is_nation_competition', 'only_now_on_court', + 'is_team', 'is_nation_competition', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', @@ -154,13 +143,6 @@ function default_handler_func(rerender, special_funcs, c) { change_score(c.val); // Most dialogs don't show any matches, so do not rerender break; - case 'court_current_match': - change_current_match(c.val); - // Most dialogs don't show any matches, so do not rerender - break; - case 'update_player_status': - //nothing todo here - break; case 'display_status_changed': const display_setting = c.val.display_court_displaysetting; const d = utils.find(curt.displays, m => m.client_id === display_setting.client_id); @@ -185,8 +167,7 @@ function default_handler_func(rerender, special_funcs, c) { } return { - default_handler, - //change_current_match, + default_handler }; })(); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 824b73b..8568198 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -85,7 +85,6 @@ var ci18n_de = { 'tournament:edit:courts': 'Felder:', 'tournament:edit:dm_style': 'Standard-Ansicht:', 'tournament:edit:displaysettings_general': 'Standard-Displayeinstellung:', - 'tournament:edit:only_now_on_court': 'Spiele müssen auf Feld gezogen werden', 'tournament:edit:warmup_timer_behavior': 'Verhalten des Vorbereitungs-Countdowns:', 'tournament:edit:warmup_timer_behavior:bwf-2016': 'BWF ab 2016 (ab Auslosung)', 'tournament:edit:warmup_timer_behavior:legacy': 'Deutschland (ab Auslosung)', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index b414738..c42106f 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -85,7 +85,6 @@ var ci18n_en = { 'tournament:edit:courts': 'Courts:', 'tournament:edit:dm_style': 'Default display style:', 'tournament:edit:displaysettings_general': 'Default displaysetting:', - 'tournament:edit:only_now_on_court': 'Matches have to be dragged onto court', 'tournament:edit:warmup_timer_behavior': 'Behaviour of the preparation countdown:', 'tournament:edit:warmup_timer_behavior:bwf-2016': 'BWF 2016+ (after choice of side)', 'tournament:edit:warmup_timer_behavior:legacy': 'legacy (after choice of side)', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 35ddcf7..88e242a 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -16,21 +16,17 @@ function calc_section(m) { if (typeof m.team1_won === 'boolean') { return 'finished'; } - if (m.setup.court_id) { - if (!curt.only_now_on_court || m.setup.now_on_court) { - return 'court_' + m.setup.court_id; - } + if (m.setup.court_id && m.setup.now_on_court) { + return 'court_' + m.setup.court_id; } return 'unassigned'; } -function render_match_table_header(table, include_courts) { +function render_match_table_header(table) { const thead = uiu.el(table, 'thead'); const title_tr = uiu.el(thead, 'tr'); - uiu.el(title_tr, 'th'); // Buttons holder - if (include_courts) { - uiu.el(title_tr, 'th', {}, ci18n('Court')); - } + uiu.el(title_tr, 'th'); + uiu.el(title_tr, 'th', {}, ci18n('Court')); uiu.el(title_tr, 'th', {}, '#'); uiu.el(title_tr, 'th', {}, ci18n('Match')); uiu.el(title_tr, 'th', { @@ -545,23 +541,6 @@ function update_match(m, old_section, new_section) { }); break; } - - - //uiu.qsEach('.match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { - // console.log("render single row: match_num = " + m.setup.match_num); - // switch (new_section) { - // case 'finished': - // render_match_row(match_row_el, m, null, 'default', false, true); - // break; - // case 'unassigned': - // render_match_row(match_row_el, m, null, 'default', true, true); - // break; - // default: - // uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { - // render_match_row(match_row_el, m, null, 'plain', false, false); - // break; - // } - //}); } var active_timers = {'matches': {}, 'players' : {}}; @@ -1068,20 +1047,20 @@ crouting.register(/t\/([a-z0-9]+)\/m\/([-a-zA-Z0-9_ ]+)\/scoresheet$/, function( ui_scoresheet(match_id); })); -function render_match_table(container, matches, include_courts, show_player_status, show_add_tabletoperator) { +function render_match_table(container, matches, show_player_status, show_add_tabletoperator) { if(!show_player_status) { show_player_status = false; } const table = uiu.el(container, 'table', 'match_table'); - render_match_table_header(table, include_courts); + render_match_table_header(table, true); const tbody = uiu.el(table, 'tbody'); for (const m of matches) { if(m.setup.is_match) { const tr = uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); - render_match_row(tr, m, null, include_courts ? 'default' : 'plain', show_player_status, show_add_tabletoperator); + render_match_row(tr, m, null, 'default', show_player_status, show_add_tabletoperator); } } } @@ -1091,7 +1070,7 @@ function render_unassigned(container) { uiu.el(container, 'h3', {}, ci18n('Unassigned Matches')); const unassigned_matches = curt.matches.filter(m => calc_section(m) === 'unassigned'); - render_match_table(container, unassigned_matches, curt.only_now_on_court, true,true); + render_match_table(container, unassigned_matches, true, true); } function render_upcoming_matches(container) { @@ -1124,7 +1103,7 @@ function render_finished(container) { uiu.el(container, 'h3', {}, ci18n('Finished Matches')); const matches = curt.matches.filter(m => calc_section(m) === 'finished').sort((a, b) => {return b.end_ts - a.end_ts}); - render_match_table(container, matches, true, false, true); + render_match_table(container, matches, false, true); } function render_courts(container, style) { diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 75e5feb..09019eb 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -223,12 +223,10 @@ var ctournament = (function() { } function update_current_match(c) { - //change.change_current_match(c.val); update_match(c); } function update_upcoming_current_match(c) { - //change.change_current_match(c.val); update_upcoming_match(c); } @@ -601,19 +599,7 @@ var ctournament = (function() { uiu.el(displaysettings_style_label, 'span', {}, ci18n('tournament:edit:displaysettings_general')); createGeneralDisplaySettingsSelectBox(displaysettings_style_label, curt.displaysettings_general ? curt.displaysettings_general : "default"); - - - // Placed on court required? - const only_now_on_court_label = uiu.el(form, 'label'); - const attrs = { - type: 'checkbox', - name: 'only_now_on_court', - }; - if (curt.only_now_on_court) { - attrs.checked = 'checked'; - } - uiu.el(only_now_on_court_label, 'input', attrs); - uiu.el(only_now_on_court_label, 'span', {}, ci18n('tournament:edit:only_now_on_court')); + // Warmup Timer if (!curt.warmup_ready) { @@ -841,7 +827,6 @@ var ctournament = (function() { language: data.language, is_team: (!!data.is_team), is_nation_competition: (!!data.is_nation_competition), - only_now_on_court: (!!data.only_now_on_court), btp_enabled: (!!data.btp_enabled), btp_autofetch_enabled: (!!data.btp_autofetch_enabled), btp_readonly: (!!data.btp_readonly), @@ -1191,10 +1176,7 @@ var ctournament = (function() { _route_single(/t\/([a-z0-9]+)\/upcoming/, ui_upcoming, change.default_handler(_update_all_ui_elements_upcoming, { score: update_score, court_current_match: update_upcoming_current_match, - //update_player_status: update_player_status, match_edit: update_upcoming_match, - //tabletoperator_add: tabletoperator_add, - //tabletoperator_removed: tabletoperator_removed, })); From e101550c94993ad116d0426244608073f0dadddc Mon Sep 17 00:00:00 2001 From: tengmel Date: Wed, 3 Jul 2024 12:24:11 +0200 Subject: [PATCH 115/224] New Function: Now it is possible te add and remove normalization Values for the optimized pronouncement of names --- bts/admin.js | 35 +++++ static/js/change.js | 322 +++++++++++++++++++++------------------ static/js/ci18n_de.js | 4 + static/js/ci18n_en.js | 4 + static/js/ctournament.js | 202 ++++++++++++++++++------ 5 files changed, 367 insertions(+), 200 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 0a49061..21b905d 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -278,6 +278,39 @@ function handle_match_add(app, ws, msg) { }); } +function handle_normalization_add(app, ws, msg) { + if (!msg.tournament_key) { + return ws.respond(msg, { message: 'Missing tournament_key' }); + } + + if (!msg.normalization) { + return ws.respond(msg, { message: 'Missing required normalization' }); + } + + app.db.normalizations.insert(msg.normalization, function (err, inserted_normalization) { + if (err) { + ws.respond(msg, err); + return; + } + notify_change(app, msg.tournament_key, 'normalization_add', { normalization: inserted_normalization }); + }); +} + +function handle_normalization_remove(app, ws, msg) { + if (!msg.tournament_key) { + return ws.respond(msg, { message: 'Missing tournament_key' }); + } + + if (!msg.normalization_id) { + return ws.respond(msg, { message: 'Missing required normalization' }); + } + + const query = { _id: msg.normalization_id }; + app.db.normalizations.remove(query, {}, (err) => { + notify_change(app, msg.tournament_key, 'normalization_removed', {normalization_id: msg.normalization_id}); + return; + }); +} function handle_tabletoperator_remove(app, ws, msg) { if (!msg.tournament_key) { return ws.respond(msg, { message: 'Missing tournament_key' }); @@ -718,6 +751,8 @@ module.exports = { handle_begin_to_play_call, handle_announce_match_manually, handle_btp_fetch, + handle_normalization_add, + handle_normalization_remove, handle_tabletoperator_add, handle_tabletoperator_remove, handle_fetch_allscoresheets_data, diff --git a/static/js/change.js b/static/js/change.js index 5f5ce8e..f05e40a 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -6,169 +6,187 @@ function default_handler(rerender, special_funcs) { }; } -function change_score(cval) { - const match_id = cval.match_id; + function change_score(cval) { + const match_id = cval.match_id; - // Find the match - const m = utils.find(curt.matches, m => m._id === match_id); - if (!m) { - cerror.silent('Cannot find match to update score, ID: ' + JSON.stringify(match_id)); - return; - } - - m.network_score = cval.network_score; - m.presses = cval.presses; - m.team1_won = cval.team1_won; -} + // Find the match + const m = utils.find(curt.matches, m => m._id === match_id); + if (!m) { + cerror.silent('Cannot find match to update score, ID: ' + JSON.stringify(match_id)); + return; + } -function default_handler_func(rerender, special_funcs, c) { - if (special_funcs && special_funcs[c.ctype]) { - special_funcs[c.ctype](c); - return; + m.network_score = cval.network_score; + m.presses = cval.presses; + m.team1_won = cval.team1_won; } - switch (c.ctype) { - case 'free_announce': - announce([c.val.text]); - break; - case 'props': { - curt.name = c.val.name; - curt.is_team = c.val.is_team; - curt.tguid = c.val.tguid; - curt.is_nation_competition = c.val.is_nation_competition; - curt.btp_timezone = c.val.btp_timezone; - curt.warmup = c.val.warmup; - curt.warmup_ready = c.val.warmup_ready; - curt.warmup_start = c.val.warmup_start; - curt.btp_enabled = c.val.btp_enabled; - curt.btp_autofetch_enabled = c.val.btp_autofetch_enabled; - curt.btp_readonly = c.val.btp_readonly; - curt.btp_ip = c.val.btp_ip; - curt.ticker_enabled = c.val.ticker_enabled; - curt.ticker_url = c.val.ticker_url; - curt.ticker_password = c.val.ticker_password; - curt.logo_id = c.val.logo_id; + function default_handler_func(rerender, special_funcs, c) { + if (special_funcs && special_funcs[c.ctype]) { + special_funcs[c.ctype](c); + return; + } - uiu.qsEach('.ct_name', function(el) { - if (el.tagName.toUpperCase() === 'INPUT') { - el.value = c.val.name; - } else { - uiu.text(el, c.val.name); + switch (c.ctype) { + case 'free_announce': + announce([c.val.text]); + break; + case 'props': { + curt.name = c.val.name; + curt.is_team = c.val.is_team; + curt.tguid = c.val.tguid; + curt.is_nation_competition = c.val.is_nation_competition; + curt.btp_timezone = c.val.btp_timezone; + curt.warmup = c.val.warmup; + curt.warmup_ready = c.val.warmup_ready; + curt.warmup_start = c.val.warmup_start; + curt.btp_enabled = c.val.btp_enabled; + curt.btp_autofetch_enabled = c.val.btp_autofetch_enabled; + curt.btp_readonly = c.val.btp_readonly; + curt.btp_ip = c.val.btp_ip; + curt.ticker_enabled = c.val.ticker_enabled; + curt.ticker_url = c.val.ticker_url; + curt.ticker_password = c.val.ticker_password; + curt.logo_id = c.val.logo_id; + + uiu.qsEach('.ct_name', function(el) { + if (el.tagName.toUpperCase() === 'INPUT') { + el.value = c.val.name; + } else { + uiu.text(el, c.val.name); + } + }); + const CHECKBOXES = [ + 'is_team', 'is_nation_competition', + 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', + 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_with_state_enabled', + 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', + 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds','announcement_speed', + 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', + 'preparation_meetingpoint_enabled','preparation_tabletoperator_setup_enabled']; + for (const cb_name of CHECKBOXES) { + uiu.qsEach('input[name="' + cb_name + '"]', function(el) { + el.checked = curt[cb_name]; + }); } - }); - const CHECKBOXES = [ - 'is_team', 'is_nation_competition', - 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', - 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_with_state_enabled', - 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', - 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds','announcement_speed', - 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', - 'preparation_meetingpoint_enabled','preparation_tabletoperator_setup_enabled']; - for (const cb_name of CHECKBOXES) { - uiu.qsEach('input[name="' + cb_name + '"]', function(el) { - el.checked = curt[cb_name]; + uiu.qsEach('input[name="btp_ip"]', function(el) { + el.value = curt.btp_ip; }); - } - uiu.qsEach('input[name="btp_ip"]', function(el) { - el.value = curt.btp_ip; - }); - uiu.qsEach('input[name="ticker_url"]', function(el) { - el.value = curt.ticker_url; - }); - uiu.qsEach('input[name="ticker_password"]', function(el) { - el.value = curt.ticker_password; - }); - break;} - case 'tabletoperator_add': - //nothing to do here - break; - case 'tabletoperator_removed': - //nothing todo here - break; - case 'match_edit': - ctournament.update_match(c); - break; - case 'match_add': - curt.matches.push(c.val.match); - rerender(); - break; - case 'match_delete': - { - const match_id = c.val.match__id; - const deleted = utils.remove_cb(curt.matches, m => m._id === match_id); - if (!deleted) { - cerror.silent('Cannot find deleted match ' + match_id); - } - rerender(); - } - break; - case 'courts_changed': - curt.courts = c.val.all_courts; - rerender(); - break; - case 'match_preparation_call': - announcePreparationMatch(c.val.match.setup); - ctournament.update_match(c); - ctournament.update_upcoming_match(c); - break; - case 'match_called_on_court': - announceNewMatch(c.val.setup); - break; - case 'begin_to_play_call': - announceBeginnToPlay(c.val.setup); - break; - case 'second_call_tabletoperator': - announceSecondCallTabletoperator(c.val.setup); - break; - case 'second_call_umpire': - announceSecondCallUmpire(c.val.setup); - break; - case 'second_call_servicejudge': - announceSecondCallServiceJudge(c.val.setup); - break; - case 'second_call_team_one': - announceSecondCallTeamOne(c.val.setup); - break; - case 'second_call_team_two': - announceSecondCallTeamTwo(c.val.setup); - break; - case 'umpires_changed': - curt.umpires = c.val.all_umpires; - uiu.qsEach('select[name="umpire_name"]', function(select) { - cmatch.render_umpire_options(select, select.value); - }); - break; - case 'score': - change_score(c.val); - // Most dialogs don't show any matches, so do not rerender - break; - case 'display_status_changed': - const display_setting = c.val.display_court_displaysetting; - const d = utils.find(curt.displays, m => m.client_id === display_setting.client_id); - var laststatus = false; - if (!d) { - curt.displays[curt.displays.length] = display_setting; - curt.displays.sort(utils.cmp_key('client_id')); - return; - } else { - laststatus = d.online; - d.court_id = display_setting.court_id; - d.displaysetting_id = display_setting.displaysetting_id; - d.online = display_setting.online; - } - if (laststatus != d.online) { - cerror.silent('Display ' + display_setting.client_id + ' is ' + (display_setting.online ? 'online' : 'offline')); + uiu.qsEach('input[name="ticker_url"]', function(el) { + el.value = curt.ticker_url; + }); + uiu.qsEach('input[name="ticker_password"]', function(el) { + el.value = curt.ticker_password; + }); + break;} + case 'tabletoperator_add': + //nothing to do here + break; + case 'tabletoperator_removed': + //nothing todo here + break; + case 'match_edit': + ctournament.update_match(c); + break; + case 'match_add': + curt.matches.push(c.val.match); + rerender(); + break; + case 'match_delete': + { + const match_id = c.val.match__id; + const deleted = utils.remove_cb(curt.matches, m => m._id === match_id); + if (!deleted) { + cerror.silent('Cannot find deleted match ' + match_id); + } + rerender(); + } + break; + case 'courts_changed': + curt.courts = c.val.all_courts; + rerender(); + break; + case 'match_preparation_call': + announcePreparationMatch(c.val.match.setup); + ctournament.update_match(c); + ctournament.update_upcoming_match(c); + break; + case 'match_called_on_court': + announceNewMatch(c.val.setup); + break; + case 'begin_to_play_call': + announceBeginnToPlay(c.val.setup); + break; + case 'second_call_tabletoperator': + announceSecondCallTabletoperator(c.val.setup); + break; + case 'second_call_umpire': + announceSecondCallUmpire(c.val.setup); + break; + case 'second_call_servicejudge': + announceSecondCallServiceJudge(c.val.setup); + break; + case 'second_call_team_one': + announceSecondCallTeamOne(c.val.setup); + break; + case 'second_call_team_two': + announceSecondCallTeamTwo(c.val.setup); + break; + case 'btp_status': + ctournament.btp_status_changed(c); + break; + case 'ticker_status': + ctournament.ticker_status_changed(c); + break; + case 'bts_status': + ctournament.bts_status_changed(c); + break; + case 'normalization_add': + ctournament.add_normalization(c); + break; + case 'normalization_add': + ctournament.add_normalization(c); + break; + case 'normalization_removed': + ctournament.remove_normalization(c); + break; + case 'umpires_changed': + curt.umpires = c.val.all_umpires; + uiu.qsEach('select[name="umpire_name"]', function(select) { + cmatch.render_umpire_options(select, select.value); + }); + break; + case 'score': + change_score(c.val); + // Most dialogs don't show any matches, so do not rerender + break; + case 'display_status_changed': + const display_setting = c.val.display_court_displaysetting; + const d = utils.find(curt.displays, m => m.client_id === display_setting.client_id); + var laststatus = false; + if (!d) { + curt.displays[curt.displays.length] = display_setting; + curt.displays.sort(utils.cmp_key('client_id')); + return; + } else { + laststatus = d.online; + d.court_id = display_setting.court_id; + d.displaysetting_id = display_setting.displaysetting_id; + d.online = display_setting.online; + } + if (laststatus != d.online) { + cerror.silent('Display ' + display_setting.client_id + ' is ' + (display_setting.online ? 'online' : 'offline')); + } + break; + default: + cerror.silent('Unsupported change type ' + c.ctype); } - break; - default: - cerror.silent('Unsupported change type ' + c.ctype); } -} -return { - default_handler -}; + return { + default_handler + }; })(); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 8568198..813849d 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -109,6 +109,10 @@ var ci18n_de = { 'tournament:edit:tabletoperator_break_seconds': 'Pausenzeit nach Tabletbedienung (sec)', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', + 'tournament:edit:normalizations': 'Ausspracheoptimierung', + 'tournament:edit:normalizations:origin': 'Original', + 'tournament:edit:normalizations:replace': 'Ersetzung', + 'tournament:edit:normalizations:language': 'Sprache', 'tournament:edit:announcement_speed': 'Ansagegeschwindigkeit (0.8-1.3)', 'tournament:edit:preparation_meetingpoint_enabled': 'Meetingpoint für Vorbereitung nutzen', 'tournament:edit:preparation_tabletoperator_setup_enabled': 'Tabletbediener in Vorbereitung aufrufen', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index c42106f..cf95097 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -116,6 +116,10 @@ var ci18n_en = { 'tournament:edit:announcement_speed': 'Announcementspeed (0.8-1.3)', 'tournament:edit:preparation_meetingpoint_enabled': 'Use Meetingpoint for preparation', 'tournament:edit:preparation_tabletoperator_setup_enabled': 'Call up tablet operator in preparation', + 'tournament:edit:normalizations': 'Pronunciation optimization', + 'tournament:edit:normalizations:origin': 'Origin', + 'tournament:edit:normalizations:replace': 'Replacement', + 'tournament:edit:normalizations:language': 'Language', 'tournament:edit:displays': 'Manage Displays:', 'tournament:edit:displays:num': 'Displaynumber', 'tournament:edit:displays:court': 'Ar Court', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 09019eb..2f23448 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -222,6 +222,26 @@ var ctournament = (function() { _show_render_tabletoperators(); } + function add_normalization(c) { + curt.normalizations.push(c.val.normalization); + update_normalization_values(c) + } + + function remove_normalization(c) { + const changed_t = utils.find(curt.normalizations, m => m._id === c.val.normalization_id); + if (changed_t) { + curt.normalizations.splice(curt.normalizations.indexOf(changed_t), 1); + } + update_normalization_values(c) + } + + function update_normalization_values(c) { + uiu.qsEach('.normalizations_values_div',(div_el) => { + div_el.innerHTML = ""; + render_normalisation_values(div_el); + }); + } + function update_current_match(c) { update_match(c); } @@ -393,6 +413,8 @@ var ctournament = (function() { update_player_status: update_player_status, match_edit: update_match, match_remove: remove_match, + normalization_removed: remove_normalization, + normalization_add: add_normalization, tabletoperator_add: tabletoperator_add, tabletoperator_removed: tabletoperator_removed, btp_status: btp_status_changed, @@ -451,13 +473,15 @@ var ctournament = (function() { } function set_service_status(service_id, c) { - if (curt) { - curt[service_id] = c.val.status; + if (c && c.val) { + if (curt) { + curt[service_id] = c.val.status; + } + uiu.qsEach('.' + service_id, (div_el) => { + div_el.className = service_id + ' status_' + c.val.status; + div_el.title = c.val.message; + }); } - uiu.qsEach('.' + service_id, (div_el) => { - div_el.className = service_id +' status_' + c.val.status; - div_el.title = c.val.message; - }); } function _upload_logo(e) { @@ -928,6 +952,115 @@ var ctournament = (function() { }); }); + + render_courts(main); + render_displaysettings(main); + render_normalisation_values(uiu.el(main, 'div','normalizations_values_div')); + } + _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit); + + function render_normalisation_values(main) { + uiu.el(main, 'h2', {}, ci18n('tournament:edit:normalizations')); + + const display_table = uiu.el(main, 'table'); + const display_tbody = uiu.el(display_table, 'tbody'); + const tr = uiu.el(display_tbody, 'tr'); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:normalizations:origin')); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:normalizations:replace')); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:normalizations:language')); + uiu.el(tr, 'th', {}, ''); + const tr_input = uiu.el(display_tbody, 'tr'); + create_undecorated_input("text", uiu.el(tr_input, 'td', {}), 'normalizations_origin'); + create_undecorated_input("text", uiu.el(tr_input, 'td', {}), 'normalizations_replace'); + create_undecorated_input("text", uiu.el(tr_input, 'td', {}), 'normalizations_language'); + const actions_td = uiu.el(tr_input, 'td', {}); + const add_btn = uiu.el(actions_td, 'button', {}, 'Add'); + add_btn.addEventListener('click', function (e) { + + var new_normalization = {} + new_normalization.origin = document.getElementById('normalizations_origin').value; + new_normalization.replace = document.getElementById('normalizations_replace').value; + new_normalization.language = document.getElementById('normalizations_language').value; + + send({ + type: 'normalization_add', + tournament_key: curt.key, + normalization: new_normalization, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + for (const nv of curt.normalizations) { + const tr = uiu.el(display_tbody, 'tr'); + uiu.el(tr, 'td', {}, nv.origin); + uiu.el(tr, 'td', {}, nv.replace); + uiu.el(tr, 'td', {}, nv.language); + const actions_td = uiu.el(tr, 'td', {}); + const delete_btn = uiu.el(actions_td, 'button', { + 'data-normalization-id': nv._id, + }, 'Delete'); + + delete_btn.addEventListener('click', function (e) { + const del_btn = e.target; + const normalization_id = del_btn.getAttribute('data-normalization-id'); + send({ + type: 'normalization_remove', + tournament_key: curt.key, + normalization_id: normalization_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + } + } + + + function render_displaysettings(main) { + uiu.el(main, 'h2', {}, ci18n('tournament:edit:displays')); + + const display_table = uiu.el(main, 'table'); + const display_tbody = uiu.el(display_table, 'tbody'); + const tr = uiu.el(display_tbody, 'tr'); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:num')); + uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:court')); + uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:setting')); + uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:onlinestatus')); + + for (const c of curt.displays) { + const tr = uiu.el(display_tbody, 'tr'); + uiu.el(tr, 'th', {}, c.client_id); + createCourtSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.court_id); + createDisplaySettingsSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.displaysetting_id); + uiu.el(tr, 'td', {}, (!c.online) ? 'offline' : 'online'); + const actions_td = uiu.el(tr, 'td', {}); + const reset_btn = uiu.el(actions_td, 'button', { + 'data-display-setting-id': c.client_id, + }, 'Restart'); + + if (!c.online) { + reset_btn.setAttribute('disabled', 'disabled'); + } + reset_btn.addEventListener('click', function (e) { + const del_btn = e.target; + const display_setting_id = del_btn.getAttribute('data-display-setting-id'); + send({ + type: 'reset_display', + tournament_key: curt.key, + display_setting_id: display_setting_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + } + } + + function render_courts(main) { uiu.el(main, 'h2', {}, ci18n('tournament:edit:courts')); const courts_table = uiu.el(main, 'table'); @@ -984,48 +1117,7 @@ var ctournament = (function() { ui_edit(); }); }); - - uiu.el(main, 'h2', {}, ci18n('tournament:edit:displays')); - - const display_table = uiu.el(main, 'table'); - const display_tbody = uiu.el(display_table, 'tbody'); - const tr = uiu.el(display_tbody, 'tr'); - uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:num')); - uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:court')); - uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:setting')); - uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:onlinestatus')); - - for (const c of curt.displays) { - const tr = uiu.el(display_tbody, 'tr'); - uiu.el(tr, 'th', {}, c.client_id); - createCourtSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.court_id); - createDisplaySettingsSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.displaysetting_id); - uiu.el(tr, 'td', {}, (!c.online) ? 'offline' : 'online'); - const actions_td = uiu.el(tr, 'td', {}); - const reset_btn = uiu.el(actions_td, 'button', { - 'data-display-setting-id': c.client_id, - }, 'Restart'); - - if (!c.online) { - reset_btn.setAttribute('disabled', 'disabled'); - } - reset_btn.addEventListener('click', function (e) { - const del_btn = e.target; - const display_setting_id = del_btn.getAttribute('data-display-setting-id'); - send({ - type: 'reset_display', - tournament_key: curt.key, - display_setting_id: display_setting_id, - }, err => { - if (err) { - return cerror.net(err); - } - }); - }); - } } - _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit); - function create_checkbox(curt, parent_el, filed_id) { const label = uiu.el(parent_el, 'label'); @@ -1050,6 +1142,15 @@ var ctournament = (function() { }); } + function create_undecorated_input(type, parent_el, filed_id) { + uiu.el(parent_el, 'input', { + type: type, + name: filed_id, + id: filed_id, + value: '', + }); + } + function create_numeric_input(curt, parent_el, filed_id, min_value, max_value, default_value, step_value) { const text_input = uiu.el(parent_el, 'label'); uiu.el(text_input, 'span', {}, ci18n('tournament:edit:' + filed_id)); @@ -1355,7 +1456,12 @@ var ctournament = (function() { ui_list, update_match, update_upcoming_match, - bts_status_changed + btp_status_changed, + ticker_status_changed, + bts_status_changed, + remove_normalization, + add_normalization, + }; })(); From e3d123568db4cf4108116b98eef1f8136144f3f5 Mon Sep 17 00:00:00 2001 From: tengmel Date: Wed, 3 Jul 2024 21:39:41 +0200 Subject: [PATCH 116/224] Bugfix: Status of all services were not viisualized properly if you are swizcheing between editing the tournament an the mainpage --- static/js/ctournament.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 2f23448..a48e02b 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -475,7 +475,7 @@ var ctournament = (function() { function set_service_status(service_id, c) { if (c && c.val) { if (curt) { - curt[service_id] = c.val.status; + curt[service_id] = c.val; } uiu.qsEach('.' + service_id, (div_el) => { div_el.className = service_id + ' status_' + c.val.status; From 237d5357598e188fe8ce048986516c4221e92efc Mon Sep 17 00:00:00 2001 From: tengmel Date: Thu, 4 Jul 2024 10:37:32 +0200 Subject: [PATCH 117/224] Enhencement: Compress Header of Adminpanel and remove duplicate Informations. --- static/css/admin.css | 18 ++++++++++-------- static/css/toprow.css | 6 ++++-- static/js/ctournament.js | 6 ++---- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/static/css/admin.css b/static/css/admin.css index 57375ab..a83f918 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -14,7 +14,7 @@ select { } .main { - padding: 0 0 0 1em; + padding: 0 0 0 0; } h1 { @@ -75,6 +75,7 @@ a:hover { .tournament_ticker_push, .tournament_settings_link { display: inline-block; + color: black; } .tournament_settings fieldset { @@ -150,13 +151,14 @@ table.striped-table { white-space: pre-wrap; } -.metadata_container{ - background-color: #aaaa; - display: flex; - flex-direction: row; - justify-content: space-around; - margin: 20px 20px 20px 0px; - padding: 10px; +.metadata_container { + background-color: #00000020; + display: flex; + flex-direction: row; + justify-content: space-around; + padding: 0.25em 0 0 0; + margin: 0 0 0.75em 0; + border-bottom: 1px solid black; } .announcements_container > form{ diff --git a/static/css/toprow.css b/static/css/toprow.css index 063ef0a..b39d9d0 100644 --- a/static/css/toprow.css +++ b/static/css/toprow.css @@ -1,12 +1,14 @@ .toprow { - border-bottom: 1px solid #ddd; padding: 0.3em 0 0.3em 1em; - margin-bottom: 1em; + background-color: #00000020; + border-bottom: 1px solid black; } .toprow > .toprow_link, .toprow_right > .toprow_link { display: inline-block; + color: black; + font-weight: bold; } .toprow_sep { margin: 0 0.5em; diff --git a/static/js/ctournament.js b/static/js/ctournament.js index a48e02b..55f7aaf 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -333,7 +333,7 @@ var ctournament = (function() { }); enable_announcements.checked = (window.localStorage.getItem('enable_announcements') === 'true'); - uiu.el(form, 'label', { for: 'enable_announcements' }, 'Ansagen auf diesem Gerät aktivieren'); + uiu.el(form, 'label', { for: 'enable_announcements' }, 'aktiv'); enable_announcements.addEventListener('change', change_announcements); } @@ -378,9 +378,7 @@ var ctournament = (function() { render_enable_announcement(meta_right_div); render_settings(meta_right_div); - - uiu.el(main, 'h1', 'tournament_name ct_name', curt.name || curt.key); - + cmatch.prepare_render(curt); From 778e82b6fcfd82bdbf4f8ad5aef557c27ec44b9f Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 14 Jul 2024 10:53:17 +0200 Subject: [PATCH 118/224] #44: Integrate Umpiremanagement in BTS --- bts/admin.js | 8 ++ bts/btp_sync.js | 113 ++++++++++++++++++++++---- bts/http_api.js | 44 ++++++++-- static/css/admin.css | 20 ++--- static/css/cumpires.css | 6 ++ static/js/change.js | 23 +++++- static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/cmatch.js | 1 + static/js/ctournament.js | 21 ++--- static/js/cumpires.js | 171 +++++++++++++++++---------------------- 11 files changed, 262 insertions(+), 147 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 21b905d..9068020 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -449,6 +449,14 @@ function handle_match_preparation_call(app, ws, msg) { } + if (setup.umpire_name) { + btp_sync.update_umpire(app, tournament_key, setup.umpire_name, 'standby', null, null); + } + + if (setup.service_judge_name) { + btp_sync.update_umpire(app, tournament_key, setup.service_judge_name, 'standby', null, null); + } + app.db.matches.update({ _id: msg.id, tournament_key }, { $set: { setup } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_match) { if (err) { ws.respond(msg, err); diff --git a/bts/btp_sync.js b/bts/btp_sync.js index f536559..d9c3f85 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -146,14 +146,14 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, setup.now_on_court = match_ids_on_court.has(bm.ID[0]); } if (bm.Official1ID) { - const o = officials.get(bm.Official1ID[0]); + const o = get_umpire(app, tkey, officials, bm.Official1ID[0]); assert(o); - setup.umpire_name = o.FirstName + ' ' + o.Name; + setup.umpire_name = o.firstName + ' ' + o.surname; } if (bm.Official2ID) { - const o = officials.get(bm.Official2ID[0]); + const o = get_umpire(app, tkey, officials, bm.Official2ID[0]); assert(o); - setup.service_judge_name = o.FirstName + ' ' + o.Name; + setup.service_judge_name = o.firstName + ' ' + o.surname; } const btp_match_ids = [{ @@ -303,7 +303,7 @@ function _parse_score(bm) { return bm.Sets[0].Set.map(s => [s.T1[0], s.T2[0]]); } -async function cleanup_matches(app, tkey, btp_state, callback) { +async function cleanup_entities(app, tkey, btp_state, callback) { const { draws, events } = btp_state; var btpMaptches = {} @@ -333,6 +333,33 @@ async function cleanup_matches(app, tkey, btp_state, callback) { } }) }); + var btpUmpires = {} + btp_state.officials.forEach(function (umpire) { + btpUmpires[umpire.ID[0]] = true; + }) + + const querry = { 'tournament_key': tkey }; + app.db.umpires.find(querry).exec((err, umpires) => { + if (err) { + return callback(err); + } + umpires.forEach(function (umpire) { + if (btpUmpires[umpire.btp_id] === true) { + //TODO invert query + return; + } else { + const mumpire_q = { _id: umpire._id }; + app.db.umpires.remove(mumpire_q, {}, (err) => { + const admin = require('./admin'); + admin.notify_change(app, tkey, 'umpire_removed', { umpire }); + return; + }); + + } + }); + }); + + return callback(null); } @@ -343,12 +370,38 @@ function calculate_btp_match_id(tkey, bm, draws, events) { const discipline_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + '_' + draw.Name[0]; return tkey + '_' + discipline_name + '_' + bm.ID[0]; } + + +function get_umpires(app, tkey) { + return new Promise((resolve, reject) => { + const querry = { 'tournament_key': tkey }; + app.db.umpires.find(querry).exec((err, umpires) => { + if (err) { + return reject(err); + } + return resolve(umpires); + }); + }); +} + +function get_umpire(app, tkey, umpires , btp_id) { + var returnValue = null; + umpires.forEach((umpire) => { + if (umpire.btp_id === btp_id) { + returnValue = umpire; + } + }); + return returnValue; +} + async function integrate_matches(app, tkey, btp_state, court_map, callback) { const admin = require('./admin'); // avoid dependency cycle - const { draws, events, officials } = btp_state; + const { draws, events } = btp_state; const match_ids_on_court = calculate_match_ids_on_court(btp_state); + const officials = await get_umpires(app, tkey); + async.each(btp_state.matches, function (bm, cb) { const draw = draws.get(bm.DrawID[0]); assert(draw); @@ -783,20 +836,23 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { var changed = false; async.each(officials, (o, cb) => { - const name = (o.FirstName ? (o.FirstName[0] + ' ') : '') + ((o.Name && o.Name[0]) ? o.Name[0] : ''); - if (!name) { + const firstName = (o.FirstName ? o.FirstName[0] : ''); + const surname = (o.Name ? o.Name[0] : ''); + const name = (firstName + " " + surname).trim(); + const btp_id = o.ID[0]; + if (!btp_id) { return cb(); } - const btp_id = o.ID[0]; + - app.db.umpires.findOne({ tournament_key, name }, (err, cur) => { + app.db.umpires.findOne({ tournament_key, btp_id }, (err, cur) => { if (err) return cb(err); if (cur) { if (cur.btp_id === btp_id) { return cb(); } else { - app.db.umpires.update({ tournament_key, name }, { $set: { btp_id } }, {}, (err) => cb(err)); + app.db.umpires.update({ tournament_key, btp_id }, { $set: { btp_id } }, {}, (err) => cb(err)); return; } } @@ -804,11 +860,19 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { const u = { _id: tournament_key + '_btp_' + btp_id, btp_id, + firstName, + surname, name, + status: 'ready', tournament_key, }; changed = true; - app.db.umpires.insert(u, err => cb(err)); + app.db.umpires.insert(u, function (err, inserted_umpire) { + if (err) { + return cb(err); + } + admin.notify_change(app, tournament_key, 'umpire_add', { umpire: inserted_umpire }); + }); }); }, err => { if (changed) { @@ -836,9 +900,20 @@ function calculate_match_ids_on_court(btp_state) { return res; } + + +function update_umpire(app, tkey, umpire_name, status, last_time_on_court_ts,court_id) { + app.db.umpires.update({ tournament_key: tkey, name: umpire_name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { + if (err) { + console.error(err); + return; + } + const admin = require('./admin'); + admin.notify_change(app, tkey, 'umpire_updated', changed_umpire); + }); +} async function integrate_now_on_court(app, tkey, callback) { const admin = require('./admin'); // avoid dependency cycle - const stournament = require('./stournament'); // avoid dependency cycle const btp_manager = require('./btp_manager'); const bupws = require('./bupws'); @@ -885,6 +960,15 @@ async function integrate_now_on_court(app, tkey, callback) { btp_manager.update_players(app, tkey, setup.tabletoperators); } + + if (setup.umpire_name) { + update_umpire(app, tkey, setup.umpire_name, 'oncourt', called_timestamp, court_id); + } + + if (setup.service_judge_name) { + update_umpire(app, tkey, setup.service_judge_name, 'oncourt', called_timestamp, court_id); + } + if (setup.highlight == 6) { setup.highlight = 0; } @@ -1124,7 +1208,7 @@ function fetch(app, tkey, response, callback) { cb => integrate_courts(app, tkey, btp_state, cb), (court_map, cb) => integrate_matches(app, tkey, btp_state, court_map, cb), cb => integrate_now_on_court(app, tkey, cb), - cb => cleanup_matches(app, tkey, btp_state, cb), + cb => cleanup_entities(app, tkey, btp_state, cb), ], callback); } @@ -1135,6 +1219,7 @@ module.exports = { fetch, time_str, fetch_tabletoperator, + update_umpire, // test only _integrate_umpires: integrate_umpires, }; diff --git a/bts/http_api.js b/bts/http_api.js index fc9c66e..c1df4e5 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -373,8 +373,8 @@ function reset_player_tabletoperator(app, tournament_key, match_id, end_ts) { async.waterfall([ cb => remove_player_on_court(app, tournament_key, match_id, end_ts, cb), cb => remove_tablet_on_court(app, tournament_key, match_id, end_ts, cb), + cb => remove_umpire_on_court(app, tournament_key, match_id, end_ts,cb), cb => add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts) - ], function (err) { if (err) { return; @@ -382,6 +382,36 @@ function reset_player_tabletoperator(app, tournament_key, match_id, end_ts) { }); } +function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, callback) { + app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { + if (err) { + return reject(err); + } + if (cur_match.setup.umpire_name) { + + update_umpire(app, tournament_key, cur_match.setup.umpire_name, 'ready', end_ts, null); + } + + if (cur_match.setup.service_judge_name) { + + update_umpire(app, tournament_key, cur_match.setup.service_judge_name, 'ready', end_ts, null); + } + return callback(null); + + }); +} + +function update_umpire(app, tkey, umpire_name, status, last_time_on_court_ts, court_id, callback) { + app.db.umpires.update({ tournament_key: tkey, name: umpire_name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { + if (err) { + console.error(err); + return; + } + const admin = require('./admin'); + admin.notify_change(app, tkey, 'umpire_updated', changed_umpire); + }); +} + function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, end_ts) { app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { if (err) { @@ -409,7 +439,7 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ const round = cur_match.setup.match_name; var team = null; - if (tournament.tabletoperator_winner_of_quaterfinals_enabled && (round == 'VF' || round == 'QF')) { + if (tournament.tabletoperator_winner_of_quaterfinals_enabled && (round == 'VF' || round == 'QF')) { team = cur_match.setup.teams[cur_match.btp_winner - 1]; } else { const index = cur_match.btp_winner % 2; @@ -425,11 +455,11 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ var toinsert = player if (tournament.tabletoperator_with_state_enabled && player.state) { toinsert = create_team_from_player_state(player); - } + } var newTeam = { players: [toinsert] }; - teams.push(newTeam); + teams.push(newTeam); } } else { var toinsert = team; @@ -438,7 +468,7 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ players: [create_team_from_player_state(team.players[0])] }; } - teams.push(toinsert); + teams.push(toinsert); } for (const t of teams) { @@ -468,9 +498,9 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ } } - } + } }); - } + } } diff --git a/static/css/admin.css b/static/css/admin.css index a83f918..7994d41 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -115,17 +115,6 @@ a:hover { background: #eee; } -.footer_links { - margin-top: 1em; - margin-bottom: 0.5em; -} -.footer_links > * { - display: inline-block; -} -.footer_links > * + * { - margin-left: 1em; -} - table.striped-table { } @@ -161,10 +150,13 @@ table.striped-table { border-bottom: 1px solid black; } -.announcements_container > form{ +.announcements_container{ + width: 400px; +} +.announcements_container > form { display: flex; - flex-direction: column; - justify-content: space-evenly; + flex-direction: column; + justify-content: space-evenly; } .status_error, diff --git a/static/css/cumpires.css b/static/css/cumpires.css index 46188fe..1dc905f 100644 --- a/static/css/cumpires.css +++ b/static/css/cumpires.css @@ -1,3 +1,9 @@ .umpires_since { color: #666; } + +.umpire_container_content { + height: 160px; + width: 325px; + overflow: auto; +} diff --git a/static/js/change.js b/static/js/change.js index f05e40a..c8179d2 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -157,6 +157,27 @@ function default_handler(rerender, special_funcs) { cmatch.render_umpire_options(select, select.value); }); break; + case 'umpire_updated': + const umpire = c.val; + const u = utils.find(curt.umpires, m => m._id === umpire._id); + if (u) { + u.last_time_on_court_ts = umpire.last_time_on_court_ts; + u.status = umpire.status; + u.court_id = umpire.court_id; + } + cumpires.ui_status(uiu.qs('.umpire_container')); + break; + case 'umpire_add': + const added_umpire = c.val.umpire; + curt.umpires.push(added_umpire); + cumpires.ui_status(uiu.qs('.umpire_container')); + break; + case 'umpire_removed': + const removed_umpire = c.val.umpire; + const ru = utils.find(curt.umpires, m => m._id === removed_umpire._id); + curt.umpires.splice(curt.umpires.indexOf(ru), 1); + cumpires.ui_status(uiu.qs('.umpire_container')); + break; case 'score': change_score(c.val); // Most dialogs don't show any matches, so do not rerender @@ -197,7 +218,7 @@ if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var ctournament = require('./ctournament'); var uiu = require('../bup/js/uiu'); var utils = require('./utils'); - + var cumpires = require('./cumpires'); module.exports = change; } /*/@DEV*/ diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 813849d..b45b654 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -165,6 +165,7 @@ var ci18n_de = { 'umpires:status:ready': 'Bereit', 'umpires:status:paused': 'In Pause', 'umpires:paused_since': 'seit {time}', + 'umpires:oncourt': 'Auf dem Feld', 'umpires:btp_id': 'BTP-ID {btp_id}', 'umpires:last_on_court': 'letztes Spiel endete {time}', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index cf95097..5c470aa 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -166,6 +166,7 @@ var ci18n_en = { 'umpires:status:ready': 'Ready', 'umpires:status:paused': 'Reserve', 'umpires:paused_since': 'since {time}', + 'umpires:oncourt': 'On Court', 'umpires:btp_id': 'BTP ID {btp_id}', 'umpires:last_on_court': 'previous match ended at {time}', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 88e242a..ef518a0 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -1554,6 +1554,7 @@ return { update_match, remove_match_from_gui, update_players, + create_timer }; })(); diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 55f7aaf..8a8d345 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -209,7 +209,6 @@ var ctournament = (function() { function tabletoperator_add(c) { curt.tabletoperators.push(c.val.tabletoperator); - _show_render_tabletoperators(); } @@ -218,7 +217,6 @@ var ctournament = (function() { if (changed_t) { changed_t.court = c.val.tabletoperator.court; } - _show_render_tabletoperators(); } @@ -269,6 +267,10 @@ var ctournament = (function() { ctabletoperator.render_unassigned(uiu.qs('.unassigned_tableoperators_container')); } + function _show_render_umpires() { + cumpires.ui_status(uiu.qs('.umpire_container')); + } + function ui_btp_fetch() { send({ @@ -371,6 +373,7 @@ var ctournament = (function() { const meta_div = uiu.el(main, 'div', 'metadata_container'); uiu.el(meta_div, 'div', 'unassigned_tableoperators_container'); + uiu.el(meta_div, 'div', 'umpire_container'); render_announcement_formular(meta_div); const meta_right_div = uiu.el(meta_div, 'div', 'metadata_right_container'); @@ -390,20 +393,8 @@ var ctournament = (function() { _show_render_matches(); - const footer_links = uiu.el(main, 'div', 'footer_links'); - const umpires_link = uiu.el(footer_links, 'span', 'vlink', ci18n('umpires:status:heading')); - umpires_link.addEventListener('click', cumpires.ui_status); - - if (/^dmo35/.test(curt.key)) { - const csvexport_link = uiu.el(footer_links, 'span', 'vlink', ci18n('csvexport:winners')); - csvexport_link.addEventListener('click', ccsvexport.export_winners); - } - - if (curt.is_nation_competition) { - crouting.render_link(footer_links, `t/${curt.key}/nationstats`, ci18n('nationstats')); - } - _show_render_tabletoperators(); + _show_render_umpires(); } _route_single(/t\/([a-z0-9]+)\/$/, ui_show, change.default_handler(_update_all_ui_elements, { score: update_score, diff --git a/static/js/cumpires.js b/static/js/cumpires.js index f6f0097..e90515d 100644 --- a/static/js/cumpires.js +++ b/static/js/cumpires.js @@ -2,113 +2,92 @@ var cumpires = (function() { -function calc_umpire_status(t) { - if (!t.umpires) return []; - const umpires_by_name = new Map(); - for (const u of t.umpires) { - umpires_by_name.set(u.name, u); - if (u.paused_since_ts) { - u.status = 'paused'; - } else { - u.status = 'ready'; - } - } - - for (const m of t.matches) { - if (!m.setup.umpire_name) continue; - const u = umpires_by_name.get(m.setup.umpire_name); - if (!u) continue; - if (m.end_ts) { - if (u.last_on_court_ts) { - u.last_on_court_ts = Math.max(m.end_ts, u.last_on_court_ts); - } else { - u.last_on_court_ts = m.end_ts; + function _ui_render_table(container, umpires) { + const table = uiu.el(container, 'table'); + const tbody = uiu.el(table, 'tbody'); + for (const u of umpires) { + + const tr = uiu.el(tbody, 'tr'); + if (curt.is_nation_competition) { + const flag_td = uiu.el(tr, 'td'); + cflags.render_flag_el(flag_td, u.nationality); + } + uiu.el(tr, 'td', { + class: 'umpires_firstname', + title: ci18n('umpires:btp_id', { btp_id: u.btp_id }), + }, u.firstName); + uiu.el(tr, 'td', { + class: 'umpires_name', + title: ci18n('umpires:btp_id', {btp_id: u.btp_id}), + }, u.surname); + if (u.status === 'oncourt') { + const td = uiu.el(tr, 'td', 'umpires_since', ''); + let parts = u.court_id.split("_"); + let court_number = parts[parts.length - 1]; + uiu.el(td, 'div', 'court', court_number) + } else if (u.status === 'standby') { + uiu.el(tr, 'td', 'umpires_since', 'In Vorbereitung'); + } else { + var timer_state = _extract_umpire_timer_state(u); + var timer = cmatch.create_timer(timer_state, uiu.el(tr, 'td', 'umpires_since', ''), "#ffffff", "#ffffff"); } - } - if (typeof m.team1_won !== 'boolean') { - u.status = 'oncourt'; } } - const umpires = Array.from(umpires_by_name.values()); - umpires.sort((u0, u1) => { - if (!u0.last_on_court_ts && u1.last_on_court_ts) return -1; - if (u0.last_on_court_ts && !u1.last_on_court_ts) return 1; - let cmp = utils.cmp_key('last_on_court_ts')(u0, u1); - if (cmp !== 0) return cmp; - return utils.cmp_key('name')(u0, u1); - }); - return umpires; -} - -function _ui_render_table(container, umpires, status) { - const table = uiu.el(container, 'table'); - const tbody = uiu.el(table, 'tbody'); - for (const u of umpires) { - if (u.status !== status) continue; - const tr = uiu.el(tbody, 'tr'); - if (curt.is_nation_competition) { - const flag_td = uiu.el(tr, 'td'); - cflags.render_flag_el(flag_td, u.nationality); + function ui_status(container) { + uiu.empty(container); + var umpires = curt.umpires; + if (umpires.length > 0) { + + umpires = umpires.sort((a, b) => { + + + if (a.status === b.status) { + + if (!b.last_time_on_court_ts) { + return 1; + } else if (!a.last_time_on_court_ts) { + return -1; + } else { + return a.last_time_on_court_ts - b.last_time_on_court_ts; + } + } else { + if (a.status === "oncourt") { + return 1; + } + if (a.status === "ready") { + return -1; + } + return 0; + } + }); + uiu.el(container, 'h3', {}, ci18n('Umpire:')); + const tableoperator_content = uiu.el(container, 'div', 'umpire_container_content'); + _ui_render_table(tableoperator_content, umpires, 'ready'); } - uiu.el(tr, 'td', { - title: ci18n('umpires:btp_id', {btp_id: u.btp_id}), - }, u.name); - if (status === 'paused') { - uiu.el(tr, 'td', 'umpires_since', - (u.paused_since_ts ? ci18n('umpires:paused_since', {time: utils.time_str(u.paused_since_ts)}) : '')); - } - uiu.el(tr, 'td', 'umpires_since', - (u.last_on_court_ts ? ci18n('umpires:last_on_court', {time: utils.time_str(u.last_on_court_ts)}) : '')); - } -} - -function _ui_status_update() { - const container = uiu.qs('.umpires_status'); - uiu.empty(container); - - cmatch.render_courts(container, 'umpires'); - - const umpires = calc_umpire_status(curt); - - uiu.el(container, 'h3', {}, ci18n('umpires:status:ready')); - _ui_render_table(container, umpires, 'ready'); - uiu.el(container, 'h3', {}, ci18n('umpires:status:paused')); - _ui_render_table(container, umpires, 'paused'); -} + } -function ui_status() { - crouting.set('t/:key/umpires', {key: curt.key}); - toprow.set([{ - label: ci18n('Tournaments'), - func: ctournament.ui_list, - }, { - label: curt.name || curt.key, - func: ctournament.ui_show, - 'class': 'ct_name', - }, { - label: ci18n('umpires:status:heading'), - }]); - - const main = uiu.qs('.main'); - uiu.empty(main); - - uiu.el(main, 'div', 'umpires_status'); - _ui_status_update(); -} -crouting.register(/t\/([a-z0-9]+)\/umpires$/, function(m) { - ctournament.switch_tournament(m[1], function() { - ui_status(); - }); -}, change.default_handler(ui_status)); + function _extract_umpire_timer_state(umpire) { + let s = {}; + s.settings = {}; + s.settings.negative_timers = false; + s.lang = "de"; + s.timer = {}; + s.timer.duration = curt.btp_settings.pause_duration_ms; + s.timer.start = (umpire.last_time_on_court_ts ? umpire.last_time_on_court_ts : false); + s.timer.upwards = false; + s.timer.exigent = false; + s.bgColor = "#FF0000"; + return s; + } -return { - ui_status, -}; + return { + ui_status, + }; })(); From c282bd1cd407e1a42ad091d10bee88de177c795f Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 14 Jul 2024 11:07:14 +0200 Subject: [PATCH 119/224] #44: Container willl be hidden whne no umpires are configured in turnamnet --- static/js/cumpires.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/js/cumpires.js b/static/js/cumpires.js index e90515d..f7b3e96 100644 --- a/static/js/cumpires.js +++ b/static/js/cumpires.js @@ -39,8 +39,8 @@ var cumpires = (function() { function ui_status(container) { uiu.empty(container); var umpires = curt.umpires; - if (umpires.length > 0) { - + if (umpires.length > 0) { + container.style.display = "block"; umpires = umpires.sort((a, b) => { @@ -66,6 +66,8 @@ var cumpires = (function() { uiu.el(container, 'h3', {}, ci18n('Umpire:')); const tableoperator_content = uiu.el(container, 'div', 'umpire_container_content'); _ui_render_table(tableoperator_content, umpires, 'ready'); + } else { + container.style.display = "none"; } } From 128ca1e92b3da4e635af3d1b37d3b651e9ef81d4 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 20 Jul 2024 13:25:05 +0200 Subject: [PATCH 120/224] =?UTF-8?q?[#54]=20Umpires:=20=C3=9Cbernahme=20all?= =?UTF-8?q?er=20vom=20BTP=20=C3=BCbermittelten=20Werte=20in=20den?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bts/btp_sync.js | 16 ++++++++++++++-- static/js/change.js | 4 ++++ static/js/cumpires.js | 6 +++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index d9c3f85..16728d8 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -839,6 +839,7 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { const firstName = (o.FirstName ? o.FirstName[0] : ''); const surname = (o.Name ? o.Name[0] : ''); const name = (firstName + " " + surname).trim(); + const country = (o.Country ? o.Country[0] : ''); const btp_id = o.ID[0]; if (!btp_id) { return cb(); @@ -849,10 +850,20 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { if (err) return cb(err); if (cur) { - if (cur.btp_id === btp_id) { + if (cur.btp_id === btp_id && + cur.firstName == firstName && + cur.surname == surname && + cur.country === country) { return cb(); } else { - app.db.umpires.update({ tournament_key, btp_id }, { $set: { btp_id } }, {}, (err) => cb(err)); + app.db.umpires.update({ tournament_key, btp_id }, { $set: { btp_id, firstName, surname, name, country } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { + if (err) { + console.error(err); + return; + } + const admin = require('./admin'); + admin.notify_change(app, tournament_key, 'umpire_updated', changed_umpire); + }); return; } } @@ -865,6 +876,7 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { name, status: 'ready', tournament_key, + country }; changed = true; app.db.umpires.insert(u, function (err, inserted_umpire) { diff --git a/static/js/change.js b/static/js/change.js index c8179d2..21c3970 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -162,6 +162,10 @@ function default_handler(rerender, special_funcs) { const u = utils.find(curt.umpires, m => m._id === umpire._id); if (u) { u.last_time_on_court_ts = umpire.last_time_on_court_ts; + u.firstName = umpire.firstName; + u.surname = umpire.surname; + u.name = umpire.name; + u.country = umpire.country; u.status = umpire.status; u.court_id = umpire.court_id; } diff --git a/static/js/cumpires.js b/static/js/cumpires.js index f7b3e96..ddb573d 100644 --- a/static/js/cumpires.js +++ b/static/js/cumpires.js @@ -9,9 +9,9 @@ var cumpires = (function() { for (const u of umpires) { const tr = uiu.el(tbody, 'tr'); - if (curt.is_nation_competition) { - const flag_td = uiu.el(tr, 'td'); - cflags.render_flag_el(flag_td, u.nationality); + const flag_td = uiu.el(tr, 'td'); + if (u.country) { + cflags.render_flag_el(flag_td, u.country); } uiu.el(tr, 'td', { class: 'umpires_firstname', From a0879bcd92ec690a82f632f546f1e20e3a7f8a92 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 6 Jul 2024 18:13:47 +0200 Subject: [PATCH 121/224] Make the metch settings dialog working: -move some methods to new match_utils class --- bts/admin.js | 103 +++++- bts/btp_conn.js | 26 ++ bts/btp_manager.js | 17 + bts/btp_proto.js | 54 ++- bts/btp_sync.js | 256 +------------- bts/http_api.js | 328 +----------------- bts/match_utils.js | 724 +++++++++++++++++++++++++++++++++++++++ static/js/cmatch.js | 61 +++- static/js/ctournament.js | 19 +- 9 files changed, 991 insertions(+), 597 deletions(-) create mode 100644 bts/match_utils.js diff --git a/bts/admin.js b/bts/admin.js index 9068020..f750367 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -397,33 +397,102 @@ function handle_tabletoperator_add(app, ws, msg) { } function handle_match_edit(app, ws, msg) { + const match_utils = require('./match_utils'); + if (!_require_msg(ws, msg, ['tournament_key', 'id', 'match'])) { return; } const tournament_key = msg.tournament_key; - const setup = _extract_setup(msg.setup); - // TODO get old setup, make sure no key has been removed - app.db.matches.update({_id: msg.id, tournament_key}, {$set: {setup}}, {returnUpdatedDocs: true}, function(err, numAffected, changed_match) { + const setup = msg.match.setup; + + app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { if (err) { - ws.respond(msg, err); - return; + return ws.respond(msg, err); } - if (numAffected !== 1) { - ws.respond(msg, new Error('Cannot find match ' + msg.id + ' of tournament ' + tournament_key + ' in database')); - return; + + if(setup.now_on_court && !setup.called_timestamp) { + match_utils.call_match(app, tournament, msg.match, (err, match) => { + if (err) { + ws.respond(msg, err); + return; + } + + update_btp_courts(app, tournament_key, msg.match, (err) => { + ws.respond(msg, err); + return; + }) + }); + } else if (!setup.now_on_court && setup.called_timestamp) { + match_utils.uncall_match(app, tournament, msg.match, (err) => { + if (err) { + ws.respond(msg, err); + return; + } + + update_btp_courts(app, tournament_key, msg.match, (err) => { + ws.respond(msg, err); + return; + }) + }); + + } else { + // TODO get old setup, make sure no key has been removed + app.db.matches.update({_id: msg.id, tournament_key}, {$set: {setup}}, {returnUpdatedDocs: true}, function(err, numAffected, changed_match) { + if (err) { + ws.respond(msg, err); + return; + } + if (numAffected !== 1) { + ws.respond(msg, new Error('Cannot find match ' + msg.id + ' of tournament ' + tournament_key + ' in database')); + return; + } + if (changed_match._id !== msg.id) { + const errmsg = 'Match ' + changed_match._id + ' changed by accident, intended to change ' + msg.id + ' (old nedb version?)'; + serror.silent(errmsg); + ws.respond(msg, new Error(errmsg)); + return; + } + + notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, changed_match}); + if (msg.btp_update) { + btp_manager.update_score(app, changed_match); + } + ws.respond(msg, err); + }); } - if (changed_match._id !== msg.id) { - const errmsg = 'Match ' + changed_match._id + ' changed by accident, intended to change ' + msg.id + ' (old nedb version?)'; - serror.silent(errmsg); - ws.respond(msg, new Error(errmsg)); + }); +} + +function update_btp_courts(app, tournament_key, match, callback) { + stournament.get_courts(app.db, tournament_key, (err, all_courts) => { + if (err) { + callback(err); return; } - notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, match}); - if (msg.btp_update) { - btp_manager.update_score(app, changed_match); - } - ws.respond(msg, err); + const courts = []; + + all_courts.forEach((element, index) => { + if(match.setup.court_id === element._id && match.setup.now_on_court) { + const court = { + btp_id: element.btp_id, + btp_match_id: match.btp_match_ids[0].id, + } + + courts.push(court); + } else if (element.match_id && element.match_id == ("btp_" + match.btp_id) && !match.setup.now_on_court) { + const court = { + btp_id: element.btp_id + } + + courts.push(court); + } + }); + + btp_manager.update_courts(app, tournament_key, courts); + + callback(null); + return; }); } diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 35ebbff..2e60d27 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -326,6 +326,32 @@ class BTPConn { } }); } + + update_courts(courts) { + if (this.readonly) { + return; + } + + if (!courts || courts.length < 1) { + return; + } + + if (! this.key_unicode) { + //serror.silent('Trying to update court data, but never logged in. Must retry later'); + return; + } + + const req = btp_proto.update_courts_request(courts, this.key_unicode, this.password); + this.send(req, response => { + const results = response.Action[0].Result; + const rescode = results ? results[0] : 'no-result'; + if (rescode === 1) { + + } else { + serror.silent('Update Courts failed with error code ' + rescode); + } + }); + } } module.exports = { diff --git a/bts/btp_manager.js b/bts/btp_manager.js index 767e82a..1045ad6 100644 --- a/bts/btp_manager.js +++ b/bts/btp_manager.js @@ -70,6 +70,22 @@ function update_players(app, tkey, players) { conn.update_players(players); } +function update_courts(app, tkey, courts) { + assert(tkey); + + if (!courts || courts.length < 1) { + return; + } + + const conn = conns_by_tkey.get(tkey); + if (!conn) { + // Do not output an error; this happens if BTP support gets disabled + return; + } + + conn.update_courts(courts); +} + function update_highlight(app, match) { assert(match); const tkey = match.tournament_key; @@ -116,5 +132,6 @@ module.exports = { reconfigure, update_score, update_players, + update_courts, update_highlight, }; \ No newline at end of file diff --git a/bts/btp_proto.js b/bts/btp_proto.js index 4401049..c957b8f 100644 --- a/bts/btp_proto.js +++ b/bts/btp_proto.js @@ -53,6 +53,7 @@ function login_request(password) { function update_request(match, key_unicode, password, umpire_btp_id, service_judge_btp_id, court_btp_id) { assert(key_unicode); const matches = []; + const courts = []; const res = { Header: { Version: { @@ -69,7 +70,7 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud }, Update: { Tournament: { - Matches: matches, + Matches: matches }, }, }; @@ -219,6 +220,56 @@ function update_players_request(players, key_unicode, password) { return res; } +function update_courts_request(courts, key_unicode, password){ + assert(key_unicode); + const courts_list = []; + const res = { + Header: { + Version: { + Hi: 1, + Lo: 1, + }, + }, + Action: { + ID: 'SENDUPDATE', + Unicode: key_unicode, + }, + Client: { + IP: 'bts', + }, + Update: { + Tournament: { + Courts: courts_list, + }, + }, + }; + + if (password) { + res.Action.Password = password; + } + + assert(courts); + assert(courts.length > 0); + + for (const court of courts) { + assert(court.btp_id); + + if (court.btp_id){ + const c = { + ID: court.btp_id + } + + if(court.btp_match_id){ + c.MatchID = court.btp_match_id; + } + + courts_list.push({Court: c}); + } + } + + return res; +} + function el2obj(el) { const res = {}; @@ -401,6 +452,7 @@ module.exports = { login_request, update_request, update_players_request, + update_courts_request, // Tests only _req2xml: req2xml, }; \ No newline at end of file diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 16728d8..0e7c143 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -574,7 +574,8 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { cb(null); return; } - app.db.matches.insert(match, function (err) { + + app.db.matches.insert(match, function(err) { if (err) { cb(null); return; @@ -928,6 +929,7 @@ async function integrate_now_on_court(app, tkey, callback) { const admin = require('./admin'); // avoid dependency cycle const btp_manager = require('./btp_manager'); const bupws = require('./bupws'); + const match_utils = require('./match_utils'); // TODO after switching to async, this should happen during court&match construction app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { @@ -943,87 +945,16 @@ async function integrate_now_on_court(app, tkey, callback) { const court_id = match.setup.court_id; const match_id = match._id; - const called_timestamp = Date.now(); + //const called_timestamp = Date.now(); if (!court_id || !match_id) { return; // TODO in async we would assert both to be true } const setup = match.setup; - if (!setup.called_timestamp) { - setup.called_timestamp = called_timestamp; - try { - if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { - if (!setup.tabletoperators || setup.tabletoperators == null) { - const value = await fetch_tabletoperator(admin, app, tkey, court_id); - if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { - setup.tabletoperators = value; - } - } - } - } catch (err) { - callback(err) - } - - if (setup.tabletoperators) { - for (let operator of setup.tabletoperators) { - operator.checked_in = false; - } - btp_manager.update_players(app, tkey, setup.tabletoperators); - } - - - if (setup.umpire_name) { - update_umpire(app, tkey, setup.umpire_name, 'oncourt', called_timestamp, court_id); - } - - if (setup.service_judge_name) { - update_umpire(app, tkey, setup.service_judge_name, 'oncourt', called_timestamp, court_id); - } - - if (setup.highlight == 6) { - setup.highlight = 0; - } - - const match_q = { _id: match_id }; - app.db.matches.update(match_q, { $set: { setup } }, {}, (err) => { - if (err) { - console.error(err); - return; - } - - btp_manager.update_highlight(app, match); - - const court_q = { _id: court_id }; - app.db.courts.find(court_q, (err, courts) => { - if (err) { - console.error(err); - return; - } - if (courts.length !== 1) return; - - app.db.courts.update(court_q, { $set: { match_id } }, {}, (err) => { - if (err) { - console.error(err); - return; - } - - admin.notify_change(app, match.tournament_key, 'match_edit', { - match__id: match._id, - match: match - }); - admin.notify_change(app, tkey, 'match_called_on_court', match); - bupws.handle_score_change(app, tkey, court_id); - async.waterfall([wcb => set_player_on_court(app, tkey, match.setup, wcb), - wcb => set_player_on_tablet(app, tkey, match.setup, wcb)], - (err) => { - if (err) { - console.error(err); - return; - } - }); - }); - }); + if(!setup.called_timestamp) { + match_utils.call_match(app, tournament, match, (err) => { + if (err) console.log(err); }); } })); @@ -1033,177 +964,6 @@ async function integrate_now_on_court(app, tkey, callback) { // TODO clear courts (better in async) } -function serialized(fn) { - let queue = Promise.resolve(); - return (...args) => { - const res = queue.then(() => fn(...args)); - queue = res.catch(() => { }); - return res; - } -} -const fetch_tabletoperator = serialized(get_last_looser_on_court); -function get_last_looser_on_court(admin, app, tkey, court_id) { - return new Promise((resolve, reject) => { - const tabletoperator_querry = { 'tournament_key': tkey, court: null }; - let tabletoperators = undefined; - app.db.tabletoperators.find(tabletoperator_querry).sort({ 'start_ts': 1 }).limit(1).exec((err, tabletoperator) => { - if (err) { - return reject(err); - } - var returnvalue = undefined; - if (tabletoperator && tabletoperator.length == 1) { - returnvalue = tabletoperator[0].tabletoperator - app.db.tabletoperators.update({ _id: tabletoperator[0]._id, tournament_key: tkey }, { $set: { court: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_tabletoperator) { - if (err) { - return reject(err); - } - admin.notify_change(app, tkey, 'tabletoperator_removed', { tabletoperator: changed_tabletoperator }); - return resolve(returnvalue); - }); - } else { - return resolve(returnvalue); - } - }); - }); -} - -function set_player_on_tablet(app, tkey, match_on_court_setup, callback) { - - if (!match_on_court_setup.tabletoperators || match_on_court_setup.tabletoperators.length == 0) { - return; - } - - const admin = require('./admin'); // avoid dependency cycle - app.db.matches.find({ 'tournament_key': tkey }, async (err, matches) => { - if (err) { - callback(err); - } - - async.each(matches, async (match, cb) => { - if (match.setup.now_on_court == false) { - return; - } - - const match_id = match._id; - let tablet_operatorns_btp_ids = [match_on_court_setup.tabletoperators[0].btp_id]; - - if (match_on_court_setup.tabletoperators.length > 1) { - tablet_operatorns_btp_ids.push(match_on_court_setup.tabletoperators[1].btp_id); - } - - let change = false; - - if (match.setup.teams[0].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { - match.setup.teams[0].players[0].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[0].checked_in = false; - change = true; - } - - if (match.setup.teams[0].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { - match.setup.teams[0].players[1].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[1].checked_in = false; - change = true; - } - - if (match.setup.teams[1].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { - match.setup.teams[1].players[0].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[0].checked_in = false; - change = true; - } - - if (match.setup.teams[1].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { - match.setup.teams[1].players[1].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[1].checked_in = false; - change = true; - } - - if (change) { - const setup = match.setup; - const match_q = { _id: match_id }; - app.db.matches.update(match_q, { $set: { setup } }, {}, (err) => { - if (err) return callback(err); - admin.notify_change(app, match.tournament_key, 'update_player_status', { - match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup - }); - }); - } - }); - - callback(null); - }); -} - - -function set_player_on_court(app, tkey, match_on_court_setup, callback) { - const admin = require('./admin'); // avoid dependency cycle - app.db.matches.find({ 'tournament_key': tkey }, async (err, matches) => { - if (err) { - callback(err); - } - - async.each(matches, async (match, cb) => { - if (match.setup.now_on_court == false) { - return; - } - - const match_id = match._id; - let on_court_btp_ids = [match_on_court_setup.teams[0].players[0].btp_id, - match_on_court_setup.teams[1].players[0].btp_id]; - - if (match_on_court_setup.teams[0].players.length > 1) { - on_court_btp_ids.push(match_on_court_setup.teams[0].players[1].btp_id); - } - - if (match_on_court_setup.teams[1].players.length > 1) { - on_court_btp_ids.push(match_on_court_setup.teams[1].players[1].btp_id); - } - - let change = false; - - if (match.setup.teams[0].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { - match.setup.teams[0].players[0].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[0].tablet_break_active = false; - change = true; - } - - if (match.setup.teams[0].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { - match.setup.teams[0].players[1].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[1].tablet_break_active = false; - change = true; - } - - if (match.setup.teams[1].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { - match.setup.teams[1].players[0].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[0].tablet_break_active = false; - change = true; - } - - if (match.setup.teams[1].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { - match.setup.teams[1].players[1].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[1].tablet_break_active = false; - change = true; - } - if (change) { - const setup = match.setup; - const match_q = { _id: match_id }; - app.db.matches.update(match_q, { $set: { setup } }, {}, (err) => { - if (err) return callback(err); - - admin.notify_change(app, match.tournament_key, 'update_player_status', { - match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup - }); - }); - } - }); - - callback(null); - }); -} - function fetch(app, tkey, response, callback) { let btp_state; @@ -1230,8 +990,6 @@ module.exports = { date_str, fetch, time_str, - fetch_tabletoperator, - update_umpire, // test only _integrate_umpires: integrate_umpires, }; diff --git a/bts/http_api.js b/bts/http_api.js index c1df4e5..bf824ae 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -370,336 +370,20 @@ function score_handler(req, res) { } function reset_player_tabletoperator(app, tournament_key, match_id, end_ts) { + const match_utils = require('./match_utils'); // avoid dependency cycle async.waterfall([ - cb => remove_player_on_court(app, tournament_key, match_id, end_ts, cb), - cb => remove_tablet_on_court(app, tournament_key, match_id, end_ts, cb), - cb => remove_umpire_on_court(app, tournament_key, match_id, end_ts,cb), - cb => add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts) - ], function (err) { - if (err) { - return; - } - }); -} - -function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, callback) { - app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { - if (err) { - return reject(err); - } - if (cur_match.setup.umpire_name) { - - update_umpire(app, tournament_key, cur_match.setup.umpire_name, 'ready', end_ts, null); - } + cb => match_utils.remove_player_on_court(app, tournament_key, match_id, end_ts, cb), + cb => match_utils.remove_tablet_on_court(app, tournament_key, match_id, end_ts, cb), + cb => match_utils.remove_umpire_on_court(app, tournament_key, match_id, end_ts,cb), + cb => match_utils.add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts, cb) - if (cur_match.setup.service_judge_name) { - - update_umpire(app, tournament_key, cur_match.setup.service_judge_name, 'ready', end_ts, null); - } - return callback(null); - - }); -} - -function update_umpire(app, tkey, umpire_name, status, last_time_on_court_ts, court_id, callback) { - app.db.umpires.update({ tournament_key: tkey, name: umpire_name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { + ], function (err) { if (err) { - console.error(err); return; } - const admin = require('./admin'); - admin.notify_change(app, tkey, 'umpire_updated', changed_umpire); - }); -} - -function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, end_ts) { - app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { - if (err) { - return callback(err); - } - if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { - app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { - if (err) { - return reject(err); - } - add_player_to_tabletoperator_list_by_match(app, tournament, tournament_key, cur_match, end_ts) - }); - } - }); -} - -function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_key, cur_match, end_ts) { - if (cur_match.network_score) { - // walkovers and retirements will not be recorgnized. - app.db.tabletoperators.findOne({ 'tournament_key': tournament_key, 'match_id': cur_match._id }, (err, no_tabletoperator) => { - if (err) { - return reject(err); - } - if (no_tabletoperator == null) { - const round = cur_match.setup.match_name; - var team = null; - - if (tournament.tabletoperator_winner_of_quaterfinals_enabled && (round == 'VF' || round == 'QF')) { - team = cur_match.setup.teams[cur_match.btp_winner - 1]; - } else { - const index = cur_match.btp_winner % 2; - team = cur_match.setup.teams[index]; - } - - // TODO: 'tabletoperator_with_state_enabled' - - if (team && typeof team.players !== 'undefined') { - var teams = []; - if (tournament.tabletoperator_split_doubles && team.players.length > 1) { - for (const player of team.players) { - var toinsert = player - if (tournament.tabletoperator_with_state_enabled && player.state) { - toinsert = create_team_from_player_state(player); - } - var newTeam = { - players: [toinsert] - }; - teams.push(newTeam); - } - } else { - var toinsert = team; - if (tournament.tabletoperator_with_state_enabled && team.players[0].state) { - toinsert = { - players: [create_team_from_player_state(team.players[0])] - }; - } - teams.push(toinsert); - } - - for (const t of teams) { - var tabletoperator = []; - t.players.forEach((player) => { - tabletoperator.push(player); - }); - - const new_tabletoperator = { - tournament_key, - tabletoperator, - 'match_id': cur_match._id, - 'start_ts': end_ts, - 'end_ts': null, - 'court': null, - 'played_on_court': (cur_match.setup.court_id ? cur_match.setup.court_id : null) - }; - - app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { - if (err) { - ws.respond(msg, err); - return; - } - const admin = require('./admin'); // avoid dependency cycle - admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); - }); - } - - } - } - }); - } -} - - -function create_team_from_player_state(player) { - return { - "asian_name": false, - "name": player.state, - "firstname": "", - "lastname": "", - "btp_id": -1 - }; -} -function remove_player_on_court (app, tkey, cur_match_id, end_ts, callback) { - const admin = require('./admin'); // avoid dependency cycle - app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { - if (err) return callback(err); - - app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { - if (err) { - return callback(err); - } - - async.each(matches, (match, cb) => { - if(!match.setup) - { - return cb(null); - } - - if(match.setup.now_on_court == true) { - return cb(null); - } - - const match_id = match._id; - let remove_btp_ids = [ cur_match.setup.teams[0].players[0].btp_id, - cur_match.setup.teams[1].players[0].btp_id]; - - if(cur_match.setup.teams[0].players.length > 1) { - remove_btp_ids.push(cur_match.setup.teams[0].players[1].btp_id); - } - - if(cur_match.setup.teams[1].players.length > 1) { - remove_btp_ids.push(cur_match.setup.teams[1].players[1].btp_id); - } - - let change = false; - - if (match.setup.teams[0].players.length > 0 && - remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && - match.setup.teams[0].players[0].now_playing_on_court) { - match.setup.teams[0].players[0].now_playing_on_court = false; - match.setup.teams[0].players[0].checked_in = false; - match.setup.teams[0].players[0].last_time_on_court_ts = end_ts; - change = true; - } - - if (match.setup.teams[0].players.length > 1 && - remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id) && - match.setup.teams[0].players[1].now_playing_on_court) { - match.setup.teams[0].players[1].now_playing_on_court = false; - match.setup.teams[0].players[1].checked_in = false; - match.setup.teams[0].players[1].last_time_on_court_ts = end_ts; - change = true; - } - - if (match.setup.teams[1].players.length > 0 && - remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && - match.setup.teams[1].players[0].now_playing_on_court) { - match.setup.teams[1].players[0].now_playing_on_court = false; - match.setup.teams[1].players[0].checked_in = false; - match.setup.teams[1].players[0].last_time_on_court_ts = end_ts; - change = true; - } - - if (match.setup.teams[1].players.length > 1 && - remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id) && - match.setup.teams[1].players[1].now_playing_on_court) { - match.setup.teams[1].players[1].now_playing_on_court = false; - match.setup.teams[1].players[1].checked_in = false; - match.setup.teams[1].players[1].last_time_on_court_ts = end_ts; - change = true; - } - - if (change) { - const setup = match.setup; - const match_q = {_id: match_id}; - app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { - if (err) return cb(err); - - admin.notify_change(app, match.tournament_key, 'update_player_status',{ match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); - return cb(null); - }); - } else { - return cb(null); - } - }, callback); - }); - }); -} - - -function remove_tablet_on_court (app, tkey, cur_match_id, end_ts, callback) { - const admin = require('./admin'); // avoid dependency cycle - app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { - if (err) { - return callback(err); - } - app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { - if (err) return callback(err); - - app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { - if (err) { - console.error(err); - return callback(err); - } - - async.each(matches, (match, cb) => { - - if(match.setup.now_on_court == true) { - return cb(null); - } - - if(!cur_match.setup.tabletoperators || cur_match.setup.tabletoperators == 0) { - return cb(null); - } - - const match_id = match._id; - let remove_btp_ids = [ cur_match.setup.tabletoperators[0].btp_id]; - - if(cur_match.setup.tabletoperators.length > 1) { - remove_btp_ids.push(cur_match.setup.tabletoperators[1].btp_id); - } - - let change = false; - - if (match.setup.teams[0].players.length > 0 && - remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { - reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[0].players[0], end_ts); - change = true; - } - - if (match.setup.teams[0].players.length > 1 && - remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { - reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[0].players[1], end_ts); - change = true; - } - - if (match.setup.teams[1].players.length > 0 && - remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { - reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[1].players[0], end_ts); - change = true; - } - - if (match.setup.teams[1].players.length > 1 && - remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { - reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[1].players[1], end_ts); - change = true; - } - - if (change) { - const setup = match.setup; - const match_q = {_id: match_id}; - app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { - if (err) return cb(err); - admin.notify_change(app, match.tournament_key, 'update_player_status', { match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); - return cb(null); - }); - } else { - return cb(null); - } - }, callback); - }); - }); }); } -function reset_tabletoperator_settings_at_player(app, tkey, tournament, player, end_ts) { - player.now_tablet_on_court = false; - if (tournament.tabletoperator_set_break_after_tabletservice) { - var offset = 0; - if (tournament.tabletoperator_break_seconds) { - offset = (parseInt(tournament.tabletoperator_break_seconds) * 1000) - tournament.btp_settings.pause_duration_ms; - } - player.last_time_on_court_ts = end_ts + offset; - player.checked_in = false; - player.tablet_break_active = true; - btp_manager.update_players(app, tkey, [player]); - - } else { - player.checked_in = true; - player.tablet_break_active = false; - btp_manager.update_players(app, tkey, [player]); - } -} - - function logo_handler(req, res) { const {tournament_key, logo_id} = req.params; assert(tournament_key); diff --git a/bts/match_utils.js b/bts/match_utils.js new file mode 100644 index 0000000..d326b86 --- /dev/null +++ b/bts/match_utils.js @@ -0,0 +1,724 @@ +'use_strict'; + +const assert = require('assert'); +const async = require('async'); + +async function uncall_match(app, tournament, match, callback) { + // Imports + + // Requrements + + async.waterfall([ (wcb) => remove_called_timestamp(match, wcb), + (wcb) => remove_tablet_on_court(app, tournament.key, match._id, null, wcb), + (wcb) => remove_tablet_operator_to_list(app, tournament.key, match, wcb), + (wcb) => update_match_btp(app, match, wcb), + (wcb) => update_match_db(app, match, wcb), + (wcb) => update_court_db(app, match, wcb), + (wcb) => notify_change_match_edit(app, match, wcb), + (wcb) => notify_bupws(app, match, wcb), + (wcb) => remove_player_on_court(app, tournament.key, match._id, null, wcb) + ], + (err) => { + return callback(err); + } + ); +} + + +async function call_match(app, tournament, match, callback) { + if (!match.setup.court_id || !match._id) { + return; // TODO in async we would assert both to be true + } + + async.waterfall([ (wcb) => add_called_timestamp(match, wcb), + (wcb) => add_tabletoperators(app, tournament, match, wcb), + (wcb) => remove_highlight_preperation(match, wcb), + (wcb) => update_match_btp(app, match, wcb), + (wcb) => update_match_db(app, match, wcb), + (wcb) => update_court_db(app, match, wcb), + (wcb) => notify_change_match_edit(app, match, wcb), + (wcb) => notify_change_match_called_on_court(app, match, wcb), + (wcb) => notify_bupws(app, match, wcb), + (wcb) => set_player_on_court(app, tournament.key, match.setup, wcb), + (wcb) => set_player_on_tablet(app, tournament.key, match.setup, wcb)], + (err) => { + return callback(err, match); + } + ); +} + +function add_called_timestamp(match, callback) { + const setup = match.setup; + const called_timestamp = Date.now(); + + setup.called_timestamp = called_timestamp; + + return callback(null); +} + +function remove_called_timestamp(match, callback) { + const setup = match.setup; + + setup.called_timestamp = undefined; + + return callback(null); +} + +function remove_tablet_operator_to_list(app, tkey, match, callback) { + add_tabletoperator_to_tabletoperator_list_by_match(app, tkey, match); + + const setup = match.setup; + setup.tabletoperators = undefined; + + return callback(null); +} + +async function add_tabletoperators(app, tournament, match, callback) { + const admin = require('./admin'); // avoid dependency cycle + const btp_manager = require('./btp_manager'); + + const court_id = match.setup.court_id; + const match_id = match._id; + + if (!court_id || !match_id) { + return; // TODO in async we would assert both to be true + } + + const setup = match.setup; + + try { + if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { + if (!setup.tabletoperators || setup.tabletoperators == null) { + const value = await fetch_tabletoperator(admin, app, tournament.key, court_id); + if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { + setup.tabletoperators = value; + } + } + } + } catch (err) { + return callback(err); + } + + if (setup.tabletoperators) { + for (let operator of setup.tabletoperators) { + operator.checked_in = false; + } + btp_manager.update_players(app, tournament.key, setup.tabletoperators); + } + + return callback(null); +} + +function remove_highlight_preperation(match, callback){ + const setup = match.setup; + + if(setup.highlight && setup.highlight == 6){ + setup.highlight = 0; + } + + return callback(null); +} + +function update_match_db (app, match, callback) { + const setup = match.setup; + const match_q = {_id: match._id}; + + app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + if (err) { + return callback(err); + } + + return callback(null); + }); +} + +function update_match_btp(app, match, callback) { + const btp_manager = require('./btp_manager'); + + // this function also send the changes of this match to btp + btp_manager.update_highlight(app, match); + + return callback(null); +} + +function update_court_db (app, match, callback) { + const court_q = {_id: match.setup.court_id}; + app.db.courts.find(court_q, (err, courts) => { + if (err) { + return callback(err); + } + + if (courts.length !== 1) { + return callback(null); + } + + app.db.courts.update(court_q, {$set: {match_id: match._id}}, {}, (err) => { + return callback(err); + }); + }); +} + +function notify_change_match_edit (app, match, callback) { + const admin = require('./admin'); // avoid dependency cycle + + admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, + match: match}); + + return callback(null); +} + + +function notify_change_match_called_on_court (app, match, callback) { + const admin = require('./admin'); // avoid dependency cycle + + admin.notify_change(app, match.tournament_key, 'match_called_on_court', match); + + return callback(null); +} + +function notify_bupws(app, match, callback) { + const bupws = require('./bupws'); + + bupws.handle_score_change(app, match.tournament_key, match.setup.court_id); + + return callback(null); +} + +function serialized(fn) { + let queue = Promise.resolve(); + return (...args) => { + const res = queue.then(() => fn(...args)); + queue = res.catch(() => { }); + return res; + } +} + +const fetch_tabletoperator = serialized(get_last_looser_on_court); + +function get_last_looser_on_court(admin, app, tkey, court_id) { + return new Promise((resolve, reject) => { + const tabletoperator_querry = { 'tournament_key': tkey, court: null }; + let tabletoperators = undefined; + app.db.tabletoperators.find(tabletoperator_querry).sort({ 'start_ts': 1 }).limit(1).exec((err, tabletoperator) => { + if (err) { + return reject(err); + } + var returnvalue = undefined; + if (tabletoperator && tabletoperator.length == 1) { + returnvalue = tabletoperator[0].tabletoperator + app.db.tabletoperators.update({ _id: tabletoperator[0]._id, tournament_key: tkey }, { $set: { court: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_tabletoperator) { + if (err) { + return reject(err); + } + admin.notify_change(app, tkey, 'tabletoperator_removed', { tabletoperator: changed_tabletoperator }); + return resolve(returnvalue); + }); + } else { + return resolve(returnvalue); + } + }); + }); +} + +function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { + + if(!match_on_court_setup.tabletoperators || match_on_court_setup.tabletoperators.length == 0) { + return callback(null); + } + + const admin = require('./admin'); // avoid dependency cycle + app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { + if (err) { + callback(err); + } + + async.each(matches, async (match, cb) => { + if(match.setup.now_on_court == false) { + return; + } + + const match_id = match._id; + let tablet_operatorns_btp_ids = [match_on_court_setup.tabletoperators[0].btp_id]; + + if(match_on_court_setup.tabletoperators.length > 1) { + tablet_operatorns_btp_ids.push(match_on_court_setup.tabletoperators[1].btp_id); + } + + let change = false; + + if (match.setup.teams[0].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + match.setup.teams[0].players[0].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[0].checked_in = false; + change = true; + } + + if (match.setup.teams[0].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + match.setup.teams[0].players[1].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[1].checked_in = false; + change = true; + } + + if (match.setup.teams[1].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + match.setup.teams[1].players[0].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[0].checked_in = false; + change = true; + } + + if (match.setup.teams[1].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + match.setup.teams[1].players[1].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[1].checked_in = false; + change = true; + } + + if (change) { + const setup = match.setup; + const match_q = {_id: match_id}; + app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + if (err) return callback(err); + admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup}); + }); + } + }); + + callback(null); + }); +} + + +function set_player_on_court (app, tkey, match_on_court_setup, callback) { + const admin = require('./admin'); // avoid dependency cycle + app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { + if (err) { + callback(err); + } + + async.each(matches, async (match) => { + if(match.setup.now_on_court == false) { + return; + } + + const match_id = match._id; + let on_court_btp_ids = [match_on_court_setup.teams[0].players[0].btp_id, + match_on_court_setup.teams[1].players[0].btp_id]; + + if(match_on_court_setup.teams[0].players.length > 1) { + on_court_btp_ids.push(match_on_court_setup.teams[0].players[1].btp_id); + } + + if(match_on_court_setup.teams[1].players.length > 1) { + on_court_btp_ids.push(match_on_court_setup.teams[1].players[1].btp_id); + } + + let change = false; + + if (match.setup.teams[0].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + match.setup.teams[0].players[0].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[0].tablet_break_active = false; + change = true; + } + + if (match.setup.teams[0].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + match.setup.teams[0].players[1].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[1].tablet_break_active = false; + change = true; + } + + if (match.setup.teams[1].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + match.setup.teams[1].players[0].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[0].tablet_break_active = false; + change = true; + } + + if (match.setup.teams[1].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + match.setup.teams[1].players[1].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[1].tablet_break_active = false; + change = true; + } + if (change) { + const setup = match.setup; + const match_q = {_id: match_id}; + app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + if (err) return callback(err); + + admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup}); + }); + } + }); + + callback(null); + }); +} + +function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, end_ts, callback) { + app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { + if (err) { + return callback(err); + } + if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { + app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { + if (err) { + return reject(err); + } + add_player_to_tabletoperator_list_by_match(app, tournament, tournament_key, cur_match, end_ts) + }); + } + return callback(null); + }); +} + +function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_key, cur_match, end_ts) { + if (cur_match.network_score) { + // walkovers and retirements will not be recorgnized. + app.db.tabletoperators.findOne({ 'tournament_key': tournament_key, 'match_id': cur_match._id }, (err, no_tabletoperator) => { + if (err) { + return reject(err); + } + if (no_tabletoperator == null) { + const round = cur_match.setup.match_name; + var team = null; + + if (tournament.tabletoperator_winner_of_quaterfinals_enabled && (round == 'VF' || round == 'QF')) { + team = cur_match.setup.teams[cur_match.btp_winner - 1]; + } else { + const index = cur_match.btp_winner % 2; + team = cur_match.setup.teams[index]; + } + + // TODO: 'tabletoperator_with_state_enabled' + + if (team && typeof team.players !== 'undefined') { + var teams = []; + if (tournament.tabletoperator_split_doubles && team.players.length > 1) { + for (const player of team.players) { + var toinsert = player + if (tournament.tabletoperator_with_state_enabled && player.state) { + toinsert = create_team_from_player_state(player); + } + var newTeam = { + players: [toinsert] + }; + teams.push(newTeam); + } + } else { + var toinsert = team; + if (tournament.tabletoperator_with_state_enabled && team.players[0].state) { + toinsert = { + players: [create_team_from_player_state(team.players[0])] + }; + } + teams.push(toinsert); + } + + for (const t of teams) { + var tabletoperator = []; + t.players.forEach((player) => { + tabletoperator.push(player); + }); + + const new_tabletoperator = { + tournament_key, + tabletoperator, + 'match_id': cur_match._id, + 'start_ts': end_ts, + 'end_ts': null, + 'court': null, + 'played_on_court': (cur_match.setup.court_id ? cur_match.setup.court_id : null) + }; + + app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { + if (err) { + ws.respond(msg, err); + return; + } + const admin = require('./admin'); // avoid dependency cycle + admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); + }); + } + + } + } + }); + } +} + +function create_team_from_player_state(player) { + return { + "asian_name": false, + "name": player.state, + "firstname": "", + "lastname": "", + "btp_id": -1 + }; +} + +function add_tabletoperator_to_tabletoperator_list_by_match(app, tournament_key, cur_match) { + + if(cur_match.setup.tabletoperators) { + var tabletoperator = cur_match.setup.tabletoperators; + + const new_tabletoperator = { + tournament_key, + tabletoperator, + 'match_id': cur_match._id, + 'start_ts': tabletoperator[0].last_time_on_court_ts, + 'end_ts': null, + 'court': null, + 'played_on_court': (cur_match.setup.court_id ? cur_match.setup.court_id : null) + }; + + app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { + if (err) { + ws.respond(msg, err); + return; + } + const admin = require('./admin'); // avoid dependency cycle + admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); + }); + } + +} + + +function remove_player_on_court (app, tkey, cur_match_id, end_ts = null, callback) { + const admin = require('./admin'); // avoid dependency cycle + + app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { + if (err) return callback(err); + + app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { + if (err) { + return callback(err); + } + + async.each(matches, (match, cb) => { + if(!match.setup) + { + return cb(null); + } + + if(match.setup.now_on_court == true) { + return cb(null); + } + + const match_id = match._id; + let remove_btp_ids = [ cur_match.setup.teams[0].players[0].btp_id, + cur_match.setup.teams[1].players[0].btp_id]; + + if(cur_match.setup.teams[0].players.length > 1) { + remove_btp_ids.push(cur_match.setup.teams[0].players[1].btp_id); + } + + if(cur_match.setup.teams[1].players.length > 1) { + remove_btp_ids.push(cur_match.setup.teams[1].players[1].btp_id); + } + + let change = false; + + if (match.setup.teams[0].players.length > 0 && + remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && + match.setup.teams[0].players[0].now_playing_on_court) { + match.setup.teams[0].players[0].now_playing_on_court = false; + match.setup.teams[0].players[0].checked_in = false; + if(end_ts) { + match.setup.teams[0].players[0].last_time_on_court_ts = end_ts; + } + change = true; + } + + if (match.setup.teams[0].players.length > 1 && + remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id) && + match.setup.teams[0].players[1].now_playing_on_court) { + match.setup.teams[0].players[1].now_playing_on_court = false; + match.setup.teams[0].players[1].checked_in = false; + if(end_ts) { + match.setup.teams[0].players[1].last_time_on_court_ts = end_ts; + } + change = true; + } + + if (match.setup.teams[1].players.length > 0 && + remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && + match.setup.teams[1].players[0].now_playing_on_court) { + match.setup.teams[1].players[0].now_playing_on_court = false; + match.setup.teams[1].players[0].checked_in = false; + if(end_ts) { + match.setup.teams[1].players[0].last_time_on_court_ts = end_ts; + } + change = true; + } + + if (match.setup.teams[1].players.length > 1 && + remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id) && + match.setup.teams[1].players[1].now_playing_on_court) { + match.setup.teams[1].players[1].now_playing_on_court = false; + match.setup.teams[1].players[1].checked_in = false; + if(end_ts) { + match.setup.teams[1].players[1].last_time_on_court_ts = end_ts; + } + change = true; + } + + if (change) { + const setup = match.setup; + const match_q = {_id: match_id}; + app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + if (err) return cb(err); + + admin.notify_change(app, match.tournament_key, 'update_player_status',{ match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup}); + + return cb(null); + }); + } else { + return cb(null); + } + }, callback); + }); + }); + +} + + +function remove_tablet_on_court (app, tkey, cur_match_id, end_ts, callback) { + const admin = require('./admin'); // avoid dependency cycle + app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { + if (err) { + return callback(err); + } + app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { + if (err) return callback(err); + + app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { + if (err) { + console.error(err); + return callback(err); + } + + async.each(matches, (match, cb) => { + + if(match.setup.now_on_court == true) { + return cb(null); + } + + if(!cur_match.setup.tabletoperators || cur_match.setup.tabletoperators == 0) { + return cb(null); + } + + const match_id = match._id; + let remove_btp_ids = [ cur_match.setup.tabletoperators[0].btp_id]; + + if(cur_match.setup.tabletoperators.length > 1) { + remove_btp_ids.push(cur_match.setup.tabletoperators[1].btp_id); + } + + let change = false; + + if (match.setup.teams[0].players.length > 0 && + remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[0].players[0], end_ts); + change = true; + } + + if (match.setup.teams[0].players.length > 1 && + remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[0].players[1], end_ts); + change = true; + } + + if (match.setup.teams[1].players.length > 0 && + remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[1].players[0], end_ts); + change = true; + } + + if (match.setup.teams[1].players.length > 1 && + remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + reset_tabletoperator_settings_at_player(app, tkey, tournament, match.setup.teams[1].players[1], end_ts); + change = true; + } + + if (change) { + const setup = match.setup; + const match_q = {_id: match_id}; + + app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { + if (err) return cb(err); + admin.notify_change(app, match.tournament_key, 'update_player_status', { match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup}); + + return cb(null); + }); + } else { + return cb(null); + } + }, callback); + }); + }); + }); +} + +function reset_tabletoperator_settings_at_player(app, tkey, tournament, player, end_ts) { + const btp_manager = require('./btp_manager'); + + player.now_tablet_on_court = false; + if (tournament.tabletoperator_set_break_after_tabletservice) { + var offset = 0; + if (tournament.tabletoperator_break_seconds) { + offset = (parseInt(tournament.tabletoperator_break_seconds) * 1000) - tournament.btp_settings.pause_duration_ms; + } + player.last_time_on_court_ts = end_ts + offset; + player.checked_in = false; + player.tablet_break_active = true; + btp_manager.update_players(app, tkey, [player]); + + } else { + player.checked_in = true; + player.tablet_break_active = false; + btp_manager.update_players(app, tkey, [player]); + } +} + +function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, callback) { + app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { + if (err) { + return reject(err); + } + if (cur_match.setup.umpire_name) { + + update_umpire(app, tournament_key, cur_match.setup.umpire_name, 'ready', end_ts, null); + } + + if (cur_match.setup.service_judge_name) { + + update_umpire(app, tournament_key, cur_match.setup.service_judge_name, 'ready', end_ts, null); + } + return callback(null); + + }); +} + +function update_umpire(app, tkey, umpire_name, status, last_time_on_court_ts, court_id, callback) { + app.db.umpires.update({ tournament_key: tkey, name: umpire_name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { + if (err) { + console.error(err); + return; + } + const admin = require('./admin'); + admin.notify_change(app, tkey, 'umpire_updated', changed_umpire); + }); +} + +module.exports ={ + add_player_to_tabletoperator_list, + call_match, + uncall_match, + remove_player_on_court, + remove_tablet_on_court, + remove_umpire_on_court +}; \ No newline at end of file diff --git a/static/js/cmatch.js b/static/js/cmatch.js index ef518a0..8e324c6 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -41,11 +41,17 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ if(!match.setup.is_match) { return; } - + if (!court && match.setup.court_id) { court = curt.courts_by_id[match.setup.court_id]; } + if (style === 'default') { + if(!court){ + tr.setAttribute('draggable', 'true'); + } + } + const waitForMatchStart = match.setup.called_timestamp && ( match.network_score == undefined || ( match.network_score[0] && @@ -816,6 +822,34 @@ function _nation_team_name(nat0, nat1) { return ''; } +function _update_setup(setup, d) { + if(!setup) { + return _make_setup(d); + } + + const result = setup; + + let override_colors = undefined; + if (d.override_colors_checkbox) { + override_colors = {}; + for (let team_id = 0;team_id < 2;team_id++) { + const team_override_colors = {}; + for (const key of OVERRIDE_COLORS_KEYS) { + override_colors[key + team_id] = d[`override_colors_${team_id}_${key}`]; + } + } + } + + result.court_id = d.court_id; + result.now_on_court = !! d.now_on_court; + result.scheduled_time_str = d.scheduled_time_str; + result.umpire_name = d.umpire_name; + result.service_judge_name = d.service_judge_name; + result.override_colors = override_colors; + + return result; +} + function _make_setup(d) { const is_doubles = !! d.team0player1lastname; const teams = [_make_team(d, 0), _make_team(d, 1)]; @@ -922,6 +956,7 @@ function ui_edit(match_id) { uiu.el(sendbtp_label, 'input', { type: 'checkbox', name: 'btp_update', + checked: 'true', }); sendbtp_label.appendChild(document.createTextNode('auch in BTP ändern')); } @@ -933,11 +968,15 @@ function ui_edit(match_id) { form_utils.onsubmit(form, function(d) { const setup = _make_setup(d); + console.log(setup); + console.log(match.setup); + match.setup = _update_setup(match.setup, d); + console.log(match.setup); btn.setAttribute('disabled', 'disabled'); send({ type: 'match_edit', id: d.match_id, - setup, + match, tournament_key: curt.key, btp_update: (curt.btp_enabled && !! d.btp_update), }, function match_edit_callback(err) { @@ -1201,6 +1240,7 @@ function render_edit(form, match) { required: 'required', value: setup.match_num || '', tabindex: 1, + disabled: 'disabled', }); uiu.el(metadata, 'span', 'match_label', 'Event:'); @@ -1210,6 +1250,7 @@ function render_edit(form, match) { placeholder: ci18n('e.g. MX O55'), size: 10, value: setup.event_name || '', + disabled: 'disabled', }); uiu.el(metadata, 'span', 'match_label', 'Match:'); @@ -1219,6 +1260,8 @@ function render_edit(form, match) { placeholder: ci18n('e.g. semi-finals'), size: 10, value: setup.match_name || '', + disabled: 'disabled', + style: 'width: 253px;', }); const start = uiu.el(edit_match_container, 'div'); @@ -1230,6 +1273,7 @@ function render_edit(form, match) { title: 'Date in ISO8601 format, e.g. 2020-05-30', size: 6, value: setup.scheduled_date || '', + style: 'width: 238px;', }); uiu.el(start, 'span', 'match_label', ci18n('Time:')); @@ -1240,6 +1284,7 @@ function render_edit(form, match) { title: 'Time in 24 hour format, e.g. 09:23', size: 3, value: setup.scheduled_time_str || '', + style: 'width: 268px;', }); const player_table = uiu.el(edit_match_container, 'table'); @@ -1252,6 +1297,7 @@ function render_edit(form, match) { size: 3, name: 'team0player0nationality', value: player_names.team0player0.nationality || '', + disabled: 'disabled', }); uiu.el(t0p0td, 'input', { type: 'text', @@ -1260,6 +1306,7 @@ function render_edit(form, match) { required: 'required', value: player_names.team0player0.firstname, tabindex: 20, + disabled: 'disabled', }); uiu.el(t0p0td, 'input', { type: 'text', @@ -1268,6 +1315,7 @@ function render_edit(form, match) { required: 'required', value: player_names.team0player0.lastname, tabindex: 20, + disabled: 'disabled', }); const t0p1td = uiu.el(tr1, 'td'); uiu.el(t0p1td, 'input', { @@ -1275,6 +1323,7 @@ function render_edit(form, match) { size: 3, name: 'team0player1nationality', value: player_names.team0player1.nationality || '', + disabled: 'disabled', }); uiu.el(t0p1td, 'input', { type: 'text', @@ -1282,6 +1331,7 @@ function render_edit(form, match) { name: 'team0player1firstname', value: player_names.team0player1.firstname, tabindex: 21, + disabled: 'disabled', }); uiu.el(t0p1td, 'input', { type: 'text', @@ -1290,6 +1340,7 @@ function render_edit(form, match) { placeholder: ci18n('(Singles)'), value: player_names.team0player1.lastname, tabindex: 21, + disabled: 'disabled', }); uiu.el(tr0, 'td', { @@ -1303,6 +1354,7 @@ function render_edit(form, match) { size: 3, name: 'team1player0nationality', value: player_names.team1player0.nationality || '', + disabled: 'disabled', }); uiu.el(t1p0td, 'input', { type: 'text', @@ -1311,6 +1363,7 @@ function render_edit(form, match) { required: 'required', value: player_names.team1player0.firstname, tabindex: 30, + disabled: 'disabled', }); uiu.el(t1p0td, 'input', { type: 'text', @@ -1319,6 +1372,7 @@ function render_edit(form, match) { required: 'required', value: player_names.team1player0.lastname, tabindex: 30, + disabled: 'disabled', }); const t1p1td = uiu.el(tr1, 'td'); uiu.el(t1p1td, 'input', { @@ -1326,6 +1380,7 @@ function render_edit(form, match) { size: 3, name: 'team1player1nationality', value: player_names.team1player1.nationality || '', + disabled: 'disabled', }); uiu.el(t1p1td, 'input', { type: 'text', @@ -1333,6 +1388,7 @@ function render_edit(form, match) { name: 'team1player1firstname', value: player_names.team1player1.firstname, tabindex: 31, + disabled: 'disabled', }); uiu.el(t1p1td, 'input', { type: 'text', @@ -1341,6 +1397,7 @@ function render_edit(form, match) { placeholder: ci18n('(Singles)'), value: player_names.team1player1.lastname, tabindex: 31, + disabled: 'disabled', }); if (curt.is_team) { diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 8a8d345..c1882c1 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -159,6 +159,9 @@ var ctournament = (function() { } function update_match(c) { const cval = c.val; + + console.log(cval); + const match_id = cval.match__id; // Find the match @@ -168,12 +171,16 @@ var ctournament = (function() { return; } const old_section = cmatch.calc_section(m); - m.network_score = cval.match.network_score; - m.presses = cval.match.presses; - m.team1_won = cval.match.team1_won; - m.shuttle_count = cval.match.shuttle_count; - m.setup = cval.match.setup; - m.btp_winner = cval.match.btp_winner; + if (cval.match) { + if('network_score' in cval.match){ + m.network_score = cval.match.network_score; + } + m.presses = cval.match.presses; + m.team1_won = cval.match.team1_won; + m.shuttle_count = cval.match.shuttle_count; + m.setup = cval.match.setup; + m.btp_winner = cval.match.btp_winner; + } const new_section = cmatch.calc_section(m); cmatch.update_match(m, old_section, new_section); } From 531c0e3bc2d1b75709d2aec82f2061d6717e82f5 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 23 Aug 2024 00:05:12 +0200 Subject: [PATCH 122/224] Feature 47: Make it possible to switch the positions of tabletoperator in the tabletoperators list. --- bts/admin.js | 77 ++++++++++++++++++++++++++++++++++ bts/match_utils.js | 2 +- static/css/ctabletoperator.css | 29 +++++++++++++ static/icons/move_down.svg | 69 ++++++++++++++++++++++++++++++ static/icons/move_up.svg | 69 ++++++++++++++++++++++++++++++ static/js/change.js | 6 +++ static/js/ci18n_de.js | 2 + static/js/ci18n_en.js | 2 + static/js/ctabletoperator.js | 66 +++++++++++++++++++++++++++-- static/js/ctournament.js | 18 ++++++++ 10 files changed, 335 insertions(+), 5 deletions(-) create mode 100644 static/icons/move_down.svg create mode 100644 static/icons/move_up.svg diff --git a/bts/admin.js b/bts/admin.js index f750367..9d56810 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -311,6 +311,81 @@ function handle_normalization_remove(app, ws, msg) { return; }); } +function handle_tabletoperator_move_up(app, ws, msg) { + if (!msg.tournament_key) { + return ws.respond(msg, { message: 'Missing tournament_key' }); + } + if (!msg.tabletoperator) { + return ws.respond(msg, { message: 'Missing tabletoperator' }); + } + const tournament_key = msg.tournament_key; + const tabletoperator = msg.tabletoperator + + const tabletoperator_querry = { 'tournament_key': msg.tournament_key, court: null }; + + + app.db.tabletoperators.find(tabletoperator_querry).sort({ 'start_ts': 1 }).exec((err, tabletoperators) => { + if (err) { + ws.respond(msg, err); + return; + } + + let start_ts_1 = 0; + let start_ts_2 = 0; + let index = 0; + + while (index < tabletoperators.length && tabletoperators[index]._id != tabletoperator._id) { + start_ts_2 = start_ts_1; + start_ts_1 = tabletoperators[index].start_ts; + index++; + } + app.db.tabletoperators.update({ _id: tabletoperator._id, tournament_key: tournament_key }, { $set: { start_ts: (start_ts_1 + start_ts_2)/2 } }, { returnUpdatedDocs: true}, function (err, numAffected, changed_tabletoperator) { + if (err) { + ws.respond(msg, err); + return; + } + notify_change(app, tournament_key, 'tabletoperator_moved_up', { tabletoperator: changed_tabletoperator }); + }); + }); +} + +function handle_tabletoperator_move_down(app, ws, msg) { + if (!msg.tournament_key) { + return ws.respond(msg, { message: 'Missing tournament_key' }); + } + if (!msg.tabletoperator) { + return ws.respond(msg, { message: 'Missing tabletoperator' }); + } + const tournament_key = msg.tournament_key; + const tabletoperator = msg.tabletoperator + + const tabletoperator_querry = { 'tournament_key': msg.tournament_key, court: null }; + + + app.db.tabletoperators.find(tabletoperator_querry).sort({ 'start_ts': -1 }).exec((err, tabletoperators) => { + if (err) { + ws.respond(msg, err); + return; + } + + let start_ts_1 = Date.now(); + let start_ts_2 = Date.now(); + let index = 0; + + while (index < tabletoperators.length && tabletoperators[index]._id != tabletoperator._id) { + start_ts_2 = start_ts_1; + start_ts_1 = tabletoperators[index].start_ts; + index++; + } + app.db.tabletoperators.update({ _id: tabletoperator._id, tournament_key: tournament_key }, { $set: { start_ts: (start_ts_1 + start_ts_2)/2 } }, { returnUpdatedDocs: true}, function (err, numAffected, changed_tabletoperator) { + if (err) { + ws.respond(msg, err); + return; + } + notify_change(app, tournament_key, 'tabletoperator_moved_up', { tabletoperator: changed_tabletoperator }); + }); + }); +} function handle_tabletoperator_remove(app, ws, msg) { if (!msg.tournament_key) { return ws.respond(msg, { message: 'Missing tournament_key' }); @@ -831,6 +906,8 @@ module.exports = { handle_normalization_add, handle_normalization_remove, handle_tabletoperator_add, + handle_tabletoperator_move_up, + handle_tabletoperator_move_down, handle_tabletoperator_remove, handle_fetch_allscoresheets_data, handle_create_tournament, diff --git a/bts/match_utils.js b/bts/match_utils.js index d326b86..e583f2d 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -687,7 +687,7 @@ function reset_tabletoperator_settings_at_player(app, tkey, tournament, player, function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, callback) { app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { if (err) { - return reject(err); + return callback(err); } if (cur_match.setup.umpire_name) { diff --git a/static/css/ctabletoperator.css b/static/css/ctabletoperator.css index 2f0de49..fe2df03 100644 --- a/static/css/ctabletoperator.css +++ b/static/css/ctabletoperator.css @@ -23,6 +23,33 @@ margin: 0 0.2em 0 0.2em; background-image: url(../icons/tabletoperator_add.svg); } +.tabletoperator_move_up_button { + display: inline-block; + min-width: 1em; + min-height: 1em; + background-size: 50%; + background-repeat: no-repeat; + background-position: center center; + height: 1.2em; + background-size: cover; + width: 0.8em; + margin: 0 -0.1em -0.2em 0.0em; + background-image: url(../icons/move_up.svg); +} +.tabletoperator_move_down_button { + display: inline-block; + min-width: 1em; + min-height: 1em; + background-size: 80%; + background-repeat: no-repeat; + background-position: center center; + height: 1.2em; + background-size: cover; + width: 0.8em; + margin: 0 0.0em -0.2em -0.1em; + background-image: url(../icons/move_down.svg); +} + .tabletoperator_remove_button { display: inline-block; min-width: 1em; @@ -38,6 +65,8 @@ } .tabletoperator_add_custom_button:hover, +.tabletoperator_move_up_button:hover, +.tabletoperator_move_down_button:hover, .tabletoperator_remove_button:hover, .tabletoperator_add_button:hover { opacity: 0.5; diff --git a/static/icons/move_down.svg b/static/icons/move_down.svg new file mode 100644 index 0000000..9d3024a --- /dev/null +++ b/static/icons/move_down.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/static/icons/move_up.svg b/static/icons/move_up.svg new file mode 100644 index 0000000..0b11a03 --- /dev/null +++ b/static/icons/move_up.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/static/js/change.js b/static/js/change.js index 21c3970..e60def8 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -83,6 +83,12 @@ function default_handler(rerender, special_funcs) { case 'tabletoperator_add': //nothing to do here break; + case 'tabletoperator_moved_up': + //nothing todo here + break; + case 'tabletoperator_moved_down': + //nothing todo here + break; case 'tabletoperator_removed': //nothing todo here break; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index b45b654..caf925c 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -172,6 +172,8 @@ var ci18n_de = { 'tabletoperator:unassigned': 'Nächste TabletbedienerInnen', 'tabletoperator:name': 'Name', 'tabletoperator:add': 'Als Tabletoperator planen', + 'tabletoperator:move_up': 'In Liste vorziehen', + 'tabletoperator:move_down': 'In Liste zurückstellen', 'tabletoperator:remove': 'Von Liste nehmen', 'csvexport:winners': 'CSV-Export für Siegerurkunden', }; diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 5c470aa..dfb652e 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -173,6 +173,8 @@ var ci18n_en = { 'tabletoperator:unassigned': 'Next Tabletoperators', 'tabletoperator:name': 'Name', 'tabletoperator:add': 'Schedule as Tabletoperator', + 'tabletoperator:move_up': 'Move up in list', + 'tabletoperator:move_down': 'Move down in list', 'tabletoperator:remove': 'Remove from list', 'csvexport:winners': 'CSV export (winner\'s certificates)', }; diff --git a/static/js/ctabletoperator.js b/static/js/ctabletoperator.js index 55c16d8..0d4a1f7 100644 --- a/static/js/ctabletoperator.js +++ b/static/js/ctabletoperator.js @@ -6,6 +6,15 @@ function render_unassigned(container) { uiu.empty(container); uiu.el(container, 'h3', {}, ci18n('tabletoperator:unassigned')); const unassigned_tabletoperators = curt.tabletoperators.filter(m => m.court == null); + console.log(unassigned_tabletoperators); + + unassigned_tabletoperators.sort((a, b) => { + return a.start_ts - b.start_ts; + }); + + + console.log(unassigned_tabletoperators); + const tableoperator_content = uiu.el(container, 'div', 'unassigned_tableoperators_content'); render_tabletoperator_table(tableoperator_content, unassigned_tabletoperators); render_tabletoperator_formular(container); @@ -17,10 +26,14 @@ function render_tabletoperator_table(container, tabletoperators) { //render_tabletoperator_table_header(table); const tbody = uiu.el(table, 'tbody'); - for (const t of tabletoperators) { + tabletoperators.forEach((t, index) => { + + console.log(tabletoperators.length); + console.log(index); + const tr = uiu.el(tbody, 'tr'); - render_tabletoperator_row(tr, t); - } + render_tabletoperator_row(tr, t, index === 0, index >= (tabletoperators.length - 1)); + }); } function render_tabletoperator_table_header(table) { @@ -28,7 +41,7 @@ function render_tabletoperator_table_header(table) { const title_tr = uiu.el(thead, 'tr'); uiu.el(title_tr, 'th', {}, ci18n('tabletoperator:name')); } -function render_tabletoperator_row(tr, tabletoperator) { +function render_tabletoperator_row(tr, tabletoperator, is_fist_entry, is_last_entry) { const to = tabletoperator.tabletoperator; const to_td = uiu.el(tr, 'td'); uiu.el(to_td, 'div', 'tablet', ''); @@ -41,11 +54,56 @@ function render_tabletoperator_row(tr, tabletoperator) { const court = curt.courts_by_id[tabletoperator.played_on_court]; uiu.el(tr, 'td', court ? 'court_history' : '', court ? court.num : ''); + if (tabletoperator.court == null) { + const buttonbar = uiu.el(tr, 'td'); + if(!is_fist_entry) { + create_tabletoperator_button(buttonbar, 'vlink tabletoperator_move_up_button', 'tabletoperator:move_up', on_move_up_button_click, tabletoperator._id); + } + } + + if (tabletoperator.court == null) { + const buttonbar = uiu.el(tr, 'td'); + if(! is_last_entry) { + create_tabletoperator_button(buttonbar, 'vlink tabletoperator_move_down_button', 'tabletoperator:move_down', on_move_down_button_click, tabletoperator._id); + } + } + if (tabletoperator.court == null) { const buttonbar = uiu.el(tr, 'td'); create_tabletoperator_button(buttonbar, 'vlink tabletoperator_remove_button', 'tabletoperator:remove', on_remove_from_list_button_click, tabletoperator._id); } } + +function on_move_up_button_click(e) { + const to = fetchTabletOperatorFromEvent(e); + if (to != null) { + send({ + type: 'tabletoperator_move_up', + tournament_key: curt.key, + tabletoperator: to, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +} + +function on_move_down_button_click(e) { + const to = fetchTabletOperatorFromEvent(e); + if (to != null) { + send({ + type: 'tabletoperator_move_down', + tournament_key: curt.key, + tabletoperator: to, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +} + function on_remove_from_list_button_click(e) { const to = fetchTabletOperatorFromEvent(e); if (to != null) { diff --git a/static/js/ctournament.js b/static/js/ctournament.js index c1882c1..fd9a89e 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -219,6 +219,22 @@ var ctournament = (function() { _show_render_tabletoperators(); } + function tabletoperator_moved_up(c) { + const changed_t = utils.find(curt.tabletoperators, m => m._id === c.val.tabletoperator._id); + if (changed_t) { + changed_t.start_ts = c.val.tabletoperator.start_ts; + } + _show_render_tabletoperators(); + } + + function tabletoperator_moved_down(c) { + const changed_t = utils.find(curt.tabletoperators, m => m._id === c.val.tabletoperator._id); + if (changed_t) { + changed_t.start_ts = c.val.tabletoperator.start_ts; + } + _show_render_tabletoperators(); + } + function tabletoperator_removed(c) { const changed_t = utils.find(curt.tabletoperators, m => m._id === c.val.tabletoperator._id); if (changed_t) { @@ -412,6 +428,8 @@ var ctournament = (function() { normalization_removed: remove_normalization, normalization_add: add_normalization, tabletoperator_add: tabletoperator_add, + tabletoperator_moved_up: tabletoperator_moved_up, + tabletoperator_moved_down: tabletoperator_moved_down, tabletoperator_removed: tabletoperator_removed, btp_status: btp_status_changed, ticker_status: ticker_status_changed, From 1a58ca036b9259d54c46d25a457a32be8e83eeeb Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 23 Aug 2024 14:25:10 +0200 Subject: [PATCH 123/224] Make it possible to move a game via drag and drop on a court --- bts/admin.js | 37 +++++++++++++++++++++++ static/css/cmatch.css | 33 ++++++++++++++++++++- static/js/cmatch.js | 57 ++++++++++++++++++++++++++++++++++-- static/js/ctabletoperator.js | 7 ----- static/js/ctournament.js | 2 -- 5 files changed, 124 insertions(+), 12 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 9d56810..1b0aa99 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -471,6 +471,42 @@ function handle_tabletoperator_add(app, ws, msg) { }); } +function handle_match_call_on_court(app, ws, msg) { + const match_utils = require('./match_utils'); + + if (!_require_msg(ws, msg, ['tournament_key', 'court_id', 'match_id'])) { + return; + } + + app.db.tournaments.findOne({ key: msg.tournament_key }, async (err, tournament) => { + if (err) { + return ws.respond(msg, err); + } + + app.db.matches.findOne({tournament_key: msg.tournament_key, _id: msg.match_id}, async (err, match) => { + if (err) { + return ws.respond(msg, err); + } + + match.setup.court_id = msg.court_id; + match.setup.now_on_court = true; + + match_utils.call_match(app, tournament, match, (err, updated_match) => { + if (err) { + ws.respond(msg, err); + return; + } + + update_btp_courts(app, msg.tournament_key, updated_match, (err) => { + ws.respond(msg, err); + return; + }); + }); + }); + }); + +} + function handle_match_edit(app, ws, msg) { const match_utils = require('./match_utils'); @@ -914,6 +950,7 @@ module.exports = { handle_courts_add, handle_match_add, handle_match_edit, + handle_match_call_on_court, handle_match_preparation_call, handle_ticker_pushall, handle_ticker_reset, diff --git a/static/css/cmatch.css b/static/css/cmatch.css index ce5b2ff..27246f9 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -89,7 +89,10 @@ .match_table { border-collapse: collapse; + width: 98%; + margin: 0 1% 0 1%; } + .match_table > thead > tr > th { text-align: left; } @@ -105,6 +108,29 @@ background-color: #00000030; } +.match_table > tbody > tr > td.droppable_active { + + background-color: #fbe44d44; +} + +.match_table > tbody > tr:hover > td.droppable_active { + background-color: #fbe44dbb; +} + +.match_table > tbody > tr:nth-of-type(even) > td.droppable_active { + + background-color: #fbe44d88; +} + +.match_table > tbody > tr:nth-of-type(even):hover > td.droppable_active { + background-color: #fbe44dbb; +} + + +.match_table > tbody > tr > #droppable { + min-width: 1000px; +} + .match_cancel_link { margin-top: 1em; } @@ -119,6 +145,7 @@ .match_vs { color: #999; padding: 0 0.5em; + width: 0.5em; } .match_team_won { font-weight: bold; @@ -154,7 +181,11 @@ } .match_timer { - min-width: 3.6em; + width: 3.6em; +} + +.call_td { + width: 5.5em; } .match_timer > div { diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 8e324c6..4bed64a 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -49,6 +49,8 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ if (style === 'default') { if(!court){ tr.setAttribute('draggable', 'true'); + tr.addEventListener("dragstart", drag); + tr.addEventListener("dragend", dragend); } } @@ -256,7 +258,6 @@ function create_match_button(targetEl, cssClass, title, listener, matchId,) { btn.addEventListener('click', listener); } function update_match_score(m) { - console.log('In Matchscore update'); uiu.qsEach('.match_score[data-match_id=' + JSON.stringify(m._id) + ']', function(score_el) { uiu.text(score_el, calc_score_str(m)); }); @@ -484,6 +485,8 @@ function update_player(match_id, player, now_on_court, show_player_status) { while (match_row_el.childElementCount > 1) { match_row_el.removeChild(match_row_el.lastChild); } + const c = {_id:m.setup.court_id}; + render_droppable_row(match_row_el, c, 'plain', true); }); break; } @@ -538,6 +541,9 @@ function update_match(m, old_section, new_section) { break; default: uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { + while(match_row_el.childElementCount > 1){ + match_row_el.removeChild(match_row_el.lastChild); + } const closest = match_row_el.closest('.main_upcoming'); if(Boolean(closest)) { render_match_row(match_row_el, m, null, 'public'); @@ -1102,6 +1108,7 @@ function render_match_table(container, matches, show_player_status, show_add_tab render_match_row(tr, m, null, 'default', show_player_status, show_add_tabletoperator); } } + } function render_unassigned(container) { @@ -1163,7 +1170,7 @@ function render_courts(container, style) { }, c.num); if (court_matches.length === 0) { - uiu.el(tr, 'td', {colspan: 11}, ''); + render_droppable_row(tr, c, style, true); } else { let i = 0; for (const cm of court_matches) { @@ -1175,6 +1182,52 @@ function render_courts(container, style) { } } +function render_droppable_row(tr, court, style, is_droppable) { + const target_td = uiu.el(tr, 'td', {class: "droppable", colspan: 11, "data-court_id":court._id}, ''); + + target_td.addEventListener("drop", drop); + target_td.addEventListener("dragover", allowDrop); +} + + +function allowDrop(ev) { + ev.preventDefault(); +} + +function drag(ev) { + ev.dataTransfer.setData('text', ev.target.getAttribute("data-match_id")); + + for (const dropp_row of document.getElementsByClassName ("droppable")) { + dropp_row.setAttribute("class", "droppable droppable_active"); + } +} + +function dragend(ev) { + for (const dropp_row of document.getElementsByClassName ("droppable")) { + dropp_row.setAttribute("class", "droppable"); + } +} + +function drop(ev) { + ev.preventDefault(); + let match_id = ev.dataTransfer.getData('text'); + + send({ + type: 'match_call_on_court', + court_id: ev.target.getAttribute('data-court_id'), + match_id: match_id, + tournament_key: curt.key, + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + + for (const dropp_row of document.getElementsByClassName ("droppable")) { + dropp_row.setAttribute("class", "droppable"); + } +} + function _make_player(d, team_idx, player_idx) { const firstname = d['team' + team_idx + 'player' + player_idx + 'firstname']; const lastname = d['team' + team_idx + 'player' + player_idx + 'lastname']; diff --git a/static/js/ctabletoperator.js b/static/js/ctabletoperator.js index 0d4a1f7..d2e7838 100644 --- a/static/js/ctabletoperator.js +++ b/static/js/ctabletoperator.js @@ -6,15 +6,11 @@ function render_unassigned(container) { uiu.empty(container); uiu.el(container, 'h3', {}, ci18n('tabletoperator:unassigned')); const unassigned_tabletoperators = curt.tabletoperators.filter(m => m.court == null); - console.log(unassigned_tabletoperators); unassigned_tabletoperators.sort((a, b) => { return a.start_ts - b.start_ts; }); - - console.log(unassigned_tabletoperators); - const tableoperator_content = uiu.el(container, 'div', 'unassigned_tableoperators_content'); render_tabletoperator_table(tableoperator_content, unassigned_tabletoperators); render_tabletoperator_formular(container); @@ -28,9 +24,6 @@ function render_tabletoperator_table(container, tabletoperators) { tabletoperators.forEach((t, index) => { - console.log(tabletoperators.length); - console.log(index); - const tr = uiu.el(tbody, 'tr'); render_tabletoperator_row(tr, t, index === 0, index >= (tabletoperators.length - 1)); }); diff --git a/static/js/ctournament.js b/static/js/ctournament.js index fd9a89e..701e582 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -160,8 +160,6 @@ var ctournament = (function() { function update_match(c) { const cval = c.val; - console.log(cval); - const match_id = cval.match__id; // Find the match From 87bd138cb509ea6a0c10945519ff647ba0873a83 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 23 Aug 2024 16:18:29 +0200 Subject: [PATCH 124/224] Feature Make it possible to check in Player in Check in per Game mode via Click in the admin gui --- bts/admin.js | 38 +++++++++++++++++++++++++++++++++++++- bts/btp_proto.js | 19 +++++++++++++++++++ bts/match_utils.js | 15 +++++++++++++++ static/js/cmatch.js | 21 +++++++++++++++++++++ 4 files changed, 92 insertions(+), 1 deletion(-) diff --git a/bts/admin.js b/bts/admin.js index 1b0aa99..f8b5738 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -543,7 +543,7 @@ function handle_match_edit(app, ws, msg) { update_btp_courts(app, tournament_key, msg.match, (err) => { ws.respond(msg, err); return; - }) + }); }); } else { @@ -662,6 +662,41 @@ function handle_match_preparation_call(app, ws, msg) { }); }); } + +function handle_match_player_check_in (app, ws, msg) { + const match_utils = require('./match_utils'); + + if (!_require_msg(ws, msg, ['tournament_key', 'player_id', 'match_id', 'checked_in'])) { + return; + } + + app.db.tournaments.findOne({ key: msg.tournament_key }, async (err, tournament) => { + if (err) { + return ws.respond(msg, err); + } + + app.db.matches.findOne({tournament_key: msg.tournament_key, _id: msg.match_id}, async (err, match) => { + if (err) { + return ws.respond(msg, err); + } + + for(const team of match.setup.teams) { + for(const player of team.players) { + if(player.btp_id == msg.player_id) { + player.checked_in = msg.checked_in; + } + } + } + + match_utils.match_update(app, match, (err) => { + ws.respond(msg, err); + return; + }); + }); + }); +} + + function handle_begin_to_play_call(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { return; @@ -952,6 +987,7 @@ module.exports = { handle_match_edit, handle_match_call_on_court, handle_match_preparation_call, + handle_match_player_check_in, handle_ticker_pushall, handle_ticker_reset, handle_free_announce, diff --git a/bts/btp_proto.js b/bts/btp_proto.js index c957b8f..9e50752 100644 --- a/bts/btp_proto.js +++ b/bts/btp_proto.js @@ -142,6 +142,25 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud m.Shuttles = shuttle_count; } + + if(match.setup.teams.length > 1) { + if(match.setup.teams[0].players[0].checked_in) { + m.Status = m.Status | 0b0001; + } + + if(match.setup.teams[0].players.length > 1 && match.setup.teams[0].players[1].checked_in) { + m.Status = m.Status | 0b0010; + } + + if(match.setup.teams[1].players[0].checked_in) { + m.Status = m.Status | 0b0100; + } + + if(match.setup.teams[1].players.length > 1 && match.setup.teams[1].players[1].checked_in) { + m.Status = m.Status | 0b1000; + } + } + matches.push({Match: m}); } diff --git a/bts/match_utils.js b/bts/match_utils.js index e583f2d..361a4cb 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -3,6 +3,20 @@ const assert = require('assert'); const async = require('async'); + +async function match_update(app, match, callback) { + async.waterfall([ + (wcb) => update_match_btp(app, match, wcb), + (wcb) => update_match_db(app, match, wcb), + (wcb) => notify_change_match_edit(app, match, wcb), + (wcb) => notify_bupws(app, match, wcb), + ], + (err) => { + return callback(err); + } + ); +} + async function uncall_match(app, tournament, match, callback) { // Imports @@ -717,6 +731,7 @@ function update_umpire(app, tkey, umpire_name, status, last_time_on_court_ts, co module.exports ={ add_player_to_tabletoperator_list, call_match, + match_update, uncall_match, remove_player_on_court, remove_tablet_on_court, diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 4bed64a..3ad522a 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -394,6 +394,27 @@ function render_player_el(parentNode, player, match_id, now_on_court, show_playe 'data-btp_id' : player.btp_id, 'data-match_id': match_id, }, player.name.replace(' ', '\xa0')); + + + player_element.addEventListener("click", (ev) => { + if(curt.btp_settings.check_in_per_match) { + + send({ + type: 'match_player_check_in', + match_id: ev.target.getAttribute("data-match_id"), + player_id: ev.target.getAttribute("data-btp_id"), + checked_in: (ev.target.getAttribute("class") == "player not_checked_in" ? true : false), + tournament_key: curt.key + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + } + }, false); + + + if (player.now_playing_on_court && player_status != "now_on_court") { let parts = player.now_playing_on_court.split("_"); let court_number = parts[parts.length - 1]; From 9667d12fac7223e85647d47c62cffff02b5e5514 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 8 Sep 2024 10:05:32 +0200 Subject: [PATCH 125/224] #56: Server.Core: NullpointerException crashes Server during updating a Match in BTP --- bts/btp_proto.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/bts/btp_proto.js b/bts/btp_proto.js index 9e50752..fb81543 100644 --- a/bts/btp_proto.js +++ b/bts/btp_proto.js @@ -1,13 +1,9 @@ 'use strict'; - const assert = require('assert'); const zlib = require('zlib'); - const xmldom = require('xmldom'); - const serror = require('./serror'); - function get_info_request(password) { const res = { Header: { @@ -53,7 +49,6 @@ function login_request(password) { function update_request(match, key_unicode, password, umpire_btp_id, service_judge_btp_id, court_btp_id) { assert(key_unicode); const matches = []; - const courts = []; const res = { Header: { Version: { @@ -144,7 +139,7 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud if(match.setup.teams.length > 1) { - if(match.setup.teams[0].players[0].checked_in) { + if (match.setup.teams[0].players.length > 0 && match.setup.teams[0].players[0].checked_in) { m.Status = m.Status | 0b0001; } @@ -152,7 +147,7 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud m.Status = m.Status | 0b0010; } - if(match.setup.teams[1].players[0].checked_in) { + if (match.setup.teams[1].players.length > 0 && match.setup.teams[1].players[0].checked_in) { m.Status = m.Status | 0b0100; } From 31e46dd6232236d84587c8c3a9efb68acd4bab5b Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 8 Sep 2024 10:09:02 +0200 Subject: [PATCH 126/224] #57: Merge corrupts the assignement of tabletoperators to matches in preparation --- bts/admin.js | 3 ++- bts/match_utils.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bts/admin.js b/bts/admin.js index f8b5738..e465a76 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -623,7 +623,8 @@ function handle_match_preparation_call(app, ws, msg) { if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { if (!setup.tabletoperators || setup.tabletoperators == null) { const admin = require('./admin'); - setup.tabletoperators = await btp_sync.fetch_tabletoperator(admin, app, tournament_key, "prep_call"); + const match_utils = require('./match_utils'); + setup.tabletoperators = await match_utils.fetch_tabletoperator(admin, app, tournament_key, "prep_call"); } } } diff --git a/bts/match_utils.js b/bts/match_utils.js index 361a4cb..0319eb7 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -733,6 +733,7 @@ module.exports ={ call_match, match_update, uncall_match, + fetch_tabletoperator, remove_player_on_court, remove_tablet_on_court, remove_umpire_on_court From 72d1d4220d745db819ee83d2920f10019a9694a7 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 8 Sep 2024 10:40:07 +0200 Subject: [PATCH 127/224] #58 Merge corrupts Umpires and Service Judges Filed Management in BTS --- bts/match_utils.js | 47 +++++++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/bts/match_utils.js b/bts/match_utils.js index 0319eb7..079ea6e 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -45,20 +45,21 @@ async function call_match(app, tournament, match, callback) { } async.waterfall([ (wcb) => add_called_timestamp(match, wcb), - (wcb) => add_tabletoperators(app, tournament, match, wcb), - (wcb) => remove_highlight_preperation(match, wcb), - (wcb) => update_match_btp(app, match, wcb), - (wcb) => update_match_db(app, match, wcb), - (wcb) => update_court_db(app, match, wcb), - (wcb) => notify_change_match_edit(app, match, wcb), - (wcb) => notify_change_match_called_on_court(app, match, wcb), - (wcb) => notify_bupws(app, match, wcb), - (wcb) => set_player_on_court(app, tournament.key, match.setup, wcb), - (wcb) => set_player_on_tablet(app, tournament.key, match.setup, wcb)], - (err) => { - return callback(err, match); - } - ); + (wcb) => add_tabletoperators(app, tournament, match, wcb), + (wcb) => set_umpires_on_court(app, tournament, match, wcb), + (wcb) => remove_highlight_preperation(match, wcb), + (wcb) => update_match_btp(app, match, wcb), + (wcb) => update_match_db(app, match, wcb), + (wcb) => update_court_db(app, match, wcb), + (wcb) => notify_change_match_edit(app, match, wcb), + (wcb) => notify_change_match_called_on_court(app, match, wcb), + (wcb) => notify_bupws(app, match, wcb), + (wcb) => set_player_on_court(app, tournament.key, match.setup, wcb), + (wcb) => set_player_on_tablet(app, tournament.key, match.setup, wcb)], + (err) => { + return callback(err, match); + } + ); } function add_called_timestamp(match, callback) { @@ -119,7 +120,23 @@ async function add_tabletoperators(app, tournament, match, callback) { } btp_manager.update_players(app, tournament.key, setup.tabletoperators); } - + return callback(null); +} + +async function set_umpires_on_court(app, tournament, match, callback) { + const setup = match.setup; + const court_id = setup.court_id; + if (!court_id) { + return; // TODO in async we would assert both to be true + } + + if (setup.umpire_name) { + update_umpire(app, tournament.key, setup.umpire_name, 'oncourt', setup.called_timestamp, court_id); + } + + if (setup.service_judge_name) { + update_umpire(app, tournament.key, setup.service_judge_name, 'oncourt', setup.called_timestamp, court_id); + } return callback(null); } From a90af48ca8d13d2f88c719be3d8097ef296ad897 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 8 Sep 2024 11:01:08 +0200 Subject: [PATCH 128/224] #59 Incomplete Matches can be called and crashes Server --- bts/match_utils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bts/match_utils.js b/bts/match_utils.js index 079ea6e..7c4e063 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -44,6 +44,10 @@ async function call_match(app, tournament, match, callback) { return; // TODO in async we would assert both to be true } + if (match.setup.teams[0].players.length == 0 || match.setup.teams[1].players.length == 0) { + return callback("Match cannot be called one or more Teams are not set."); + } + async.waterfall([ (wcb) => add_called_timestamp(match, wcb), (wcb) => add_tabletoperators(app, tournament, match, wcb), (wcb) => set_umpires_on_court(app, tournament, match, wcb), From 071b83907c54d21021e8e9e51d8c671b1b4bc586 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 8 Sep 2024 11:26:28 +0200 Subject: [PATCH 129/224] #60: Incomplete Matches can be called in preparation and crashes Server --- bts/admin.js | 17 ++++++++--------- bts/match_utils.js | 27 +++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index e465a76..0dc87e5 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -609,9 +609,16 @@ function update_btp_courts(app, tournament_key, match, callback) { function handle_match_preparation_call(app, ws, msg) { + + const match_utils = require('./match_utils'); + if (!_require_msg(ws, msg, ['tournament_key', 'id', 'setup'])) { return; } + if (match_utils.match_completly_initialized(msg.setup) == false) { + return ws.respond("Match cannot be called one or more Teams are not set."); + } + const tournament_key = msg.tournament_key; app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { @@ -628,15 +635,7 @@ function handle_match_preparation_call(app, ws, msg) { } } } - - - if (setup.umpire_name) { - btp_sync.update_umpire(app, tournament_key, setup.umpire_name, 'standby', null, null); - } - - if (setup.service_judge_name) { - btp_sync.update_umpire(app, tournament_key, setup.service_judge_name, 'standby', null, null); - } + match_utils.set_umpire_to_standby(app, tournament_key, setup); app.db.matches.update({ _id: msg.id, tournament_key }, { $set: { setup } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_match) { if (err) { diff --git a/bts/match_utils.js b/bts/match_utils.js index 7c4e063..bdaa554 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -43,11 +43,9 @@ async function call_match(app, tournament, match, callback) { if (!match.setup.court_id || !match._id) { return; // TODO in async we would assert both to be true } - - if (match.setup.teams[0].players.length == 0 || match.setup.teams[1].players.length == 0) { + if (match_completly_initialized(match.setup) == false) { return callback("Match cannot be called one or more Teams are not set."); } - async.waterfall([ (wcb) => add_called_timestamp(match, wcb), (wcb) => add_tabletoperators(app, tournament, match, wcb), (wcb) => set_umpires_on_court(app, tournament, match, wcb), @@ -66,6 +64,13 @@ async function call_match(app, tournament, match, callback) { ); } +function match_completly_initialized(setup) { + if (setup.teams[0].players.length == 0 || setup.teams[1].players.length == 0) { + return false; + } + return true; +} + function add_called_timestamp(match, callback) { const setup = match.setup; const called_timestamp = Date.now(); @@ -738,6 +743,18 @@ function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, callb }); } +function set_umpire_to_standby(app, tournament_key, setup) { + if (setup.umpire_name) { + update_umpire(app, tournament_key, setup.umpire_name, 'standby', null, null); + } + + if (setup.service_judge_name) { + update_umpire(app, tournament_key, setup.service_judge_name, 'standby', null, null); + } +} + + + function update_umpire(app, tkey, umpire_name, status, last_time_on_court_ts, court_id, callback) { app.db.umpires.update({ tournament_key: tkey, name: umpire_name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { if (err) { @@ -755,7 +772,9 @@ module.exports ={ match_update, uncall_match, fetch_tabletoperator, + match_completly_initialized, remove_player_on_court, remove_tablet_on_court, - remove_umpire_on_court + remove_umpire_on_court, + set_umpire_to_standby, }; \ No newline at end of file From bba440b34361c21b9f248300d96962aac397604b Mon Sep 17 00:00:00 2001 From: tengmel Date: Thu, 3 Oct 2024 13:08:10 +0200 Subject: [PATCH 130/224] #59 Incomplete Matches can be called and crashes Server --- bts/admin.js | 32 ++++++++++++++---------------- static/js/cmatch.js | 48 +++++++++++++++++++++++++++++++-------------- 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 0dc87e5..bceb135 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -473,35 +473,33 @@ function handle_tabletoperator_add(app, ws, msg) { function handle_match_call_on_court(app, ws, msg) { const match_utils = require('./match_utils'); - if (!_require_msg(ws, msg, ['tournament_key', 'court_id', 'match_id'])) { return; } - app.db.tournaments.findOne({ key: msg.tournament_key }, async (err, tournament) => { if (err) { return ws.respond(msg, err); } - app.db.matches.findOne({tournament_key: msg.tournament_key, _id: msg.match_id}, async (err, match) => { if (err) { return ws.respond(msg, err); } - - match.setup.court_id = msg.court_id; - match.setup.now_on_court = true; - - match_utils.call_match(app, tournament, match, (err, updated_match) => { - if (err) { - ws.respond(msg, err); - return; - } - - update_btp_courts(app, msg.tournament_key, updated_match, (err) => { - ws.respond(msg, err); - return; + if (match != null) { + match.setup.court_id = msg.court_id; + match.setup.now_on_court = true; + match_utils.call_match(app, tournament, match, (err, updated_match) => { + if (err) { + ws.respond(msg, err); + return; + } + update_btp_courts(app, msg.tournament_key, updated_match, (err) => { + ws.respond(msg, err); + return; + }); }); - }); + }else { + return ws.respond(msg, "Match cannot be fetched from DB " + msg.match_id); + } }); }); diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 3ad522a..730042e 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -1215,11 +1215,28 @@ function allowDrop(ev) { ev.preventDefault(); } +function validate_match_complete(match_id) { + const m = utils.find(curt.matches, m => m._id === match_id); + if (!m) { + cerror.silent('Cannot find match to call on court. ID: ' + JSON.stringify(match_id)); + return false; + } + + if (m.setup.teams[0].players.length == 0 || m.setup.teams[1].players.length == 0) { + cerror.silent("Match cannot be called one or more Teams are not set.") + return false; + } + return true; +} + function drag(ev) { - ev.dataTransfer.setData('text', ev.target.getAttribute("data-match_id")); + let match_id = ev.target.getAttribute("data-match_id"); + if (validate_match_complete(match_id)) { + ev.dataTransfer.setData('text', ev.target.getAttribute("data-match_id")); - for (const dropp_row of document.getElementsByClassName ("droppable")) { - dropp_row.setAttribute("class", "droppable droppable_active"); + for (const dropp_row of document.getElementsByClassName ("droppable")) { + dropp_row.setAttribute("class", "droppable droppable_active"); + } } } @@ -1232,20 +1249,21 @@ function dragend(ev) { function drop(ev) { ev.preventDefault(); let match_id = ev.dataTransfer.getData('text'); - - send({ - type: 'match_call_on_court', - court_id: ev.target.getAttribute('data-court_id'), - match_id: match_id, - tournament_key: curt.key, + if (validate_match_complete(match_id)) { + send({ + type: 'match_call_on_court', + court_id: ev.target.getAttribute('data-court_id'), + match_id: match_id, + tournament_key: curt.key, }, function (err) { - if (err) { - return cerror.net(err); - } - }); + if (err) { + return cerror.net(err); + } + }); - for (const dropp_row of document.getElementsByClassName ("droppable")) { - dropp_row.setAttribute("class", "droppable"); + for (const dropp_row of document.getElementsByClassName("droppable")) { + dropp_row.setAttribute("class", "droppable"); + } } } From bac7b72effac59d52259d1bb0790f2986110f6a0 Mon Sep 17 00:00:00 2001 From: tengmel Date: Thu, 3 Oct 2024 13:39:39 +0200 Subject: [PATCH 131/224] #61: Announcement: Making the announcement of match numbers, rounds and events configurable --- bts/admin.js | 1 + static/js/announcements.js | 126 ++++++++++++++++++++----------------- static/js/change.js | 1 + static/js/ci18n_de.js | 3 + static/js/ci18n_en.js | 3 + static/js/ctournament.js | 6 ++ 6 files changed, 83 insertions(+), 57 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index bceb135..a2b4fb6 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -63,6 +63,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'tabletoperator_set_break_after_tabletservice','tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', + 'annoncement_include_event', 'annoncement_include_round','annoncement_include_matchnumber', 'preparation_meetingpoint_enabled','preparation_tabletoperator_setup_enabled', 'logo_background_color', 'logo_foreground_color']); diff --git a/static/js/announcements.js b/static/js/announcements.js index d008689..2d16f6d 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -145,86 +145,98 @@ function normalizeNames(name) { } function createRoundAnnouncement(matchSetup) { - var round = matchSetup.match_name; - if (round == "R16") { - round = ci18n('announcements:round_16'); - } else if (round == "VF") { - round = ci18n('announcements:quaterfinal'); - } else if (round == "HF") { - round = ci18n('announcements:semifinal'); - } else if (round == "Finale") { - round = ci18n('announcements:final'); - } else if (round.indexOf('/') !== -1) { - var roundParts = round.split("/") - var diff = roundParts[1] - roundParts[0]; - if (diff > 1) { + if (curt.annoncement_include_round) { + var round = matchSetup.match_name; + if (round == "R16") { + round = ci18n('announcements:round_16'); + } else if (round == "VF") { + round = ci18n('announcements:quaterfinal'); + } else if (round == "HF") { + round = ci18n('announcements:semifinal'); + } else if (round == "Finale") { + round = ci18n('announcements:final'); + } else if (round.indexOf('/') !== -1) { + var roundParts = round.split("/") + var diff = roundParts[1] - roundParts[0]; + if (diff > 1) { + round = ci18n('announcements:intermediate_round'); + } else { + round = ci18n('announcements:game_for_place') + roundParts[0] + ci18n('announcements:and') + roundParts[1]; + } + } else if (round.indexOf('-') !== -1) { round = ci18n('announcements:intermediate_round'); } else { - round = ci18n('announcements:game_for_place') + roundParts[0] + ci18n('announcements:and') + roundParts[1]; + round = ""; } - } else if (round.indexOf('-') !== -1) { - round = ci18n('announcements:intermediate_round'); + return round; } else { - round = ""; + return null; } - return round; } function createEventAnnouncement(matchSetup) { - var eventParts = matchSetup.event_name.replaceAll("-"," ").split(" "); - var eventName = ""; - if (eventParts[0] == 'JE') { - eventName = ci18n('announcements:boys_singles'); - } else if (eventParts[0] == 'JD') { - eventName = ci18n('announcements:boys_doubles'); - } else if (eventParts[0] == 'ME') { - eventName = ci18n('announcements:girls_singles'); - } else if (eventParts[0] == 'MD') { - eventName = ci18n('announcements:girls_doubles') - } else if (eventParts[0] == 'GD' || eventParts[0] == 'MX') { - eventName = ci18n('announcements:mixed_doubles') - } else if (eventParts[0] == 'HE') { - eventName = ci18n('announcements:men_singles'); - } else if (eventParts[0] == 'HD') { - eventName = ci18n('announcements:men_doubles'); - } else if (eventParts[0] == 'DE') { - eventName = ci18n('announcements:women_singles'); - } else if (eventParts[0] == 'DD') { - eventName = ci18n('announcements:women_doubles'); - } - if (eventName == "") { - if (eventParts[1] == 'JE') { + if (curt.annoncement_include_event) { + var eventParts = matchSetup.event_name.replaceAll("-", " ").split(" "); + var eventName = ""; + if (eventParts[0] == 'JE') { eventName = ci18n('announcements:boys_singles'); - } else if (eventParts[1] == 'JD') { + } else if (eventParts[0] == 'JD') { eventName = ci18n('announcements:boys_doubles'); - } else if (eventParts[1] == 'ME') { + } else if (eventParts[0] == 'ME') { eventName = ci18n('announcements:girls_singles'); - } else if (eventParts[1] == 'MD') { + } else if (eventParts[0] == 'MD') { eventName = ci18n('announcements:girls_doubles') - } else if (eventParts[1] == 'GD' || eventParts[1] == 'MX') { + } else if (eventParts[0] == 'GD' || eventParts[0] == 'MX') { eventName = ci18n('announcements:mixed_doubles') - } else if (eventParts[1] == 'HE') { + } else if (eventParts[0] == 'HE') { eventName = ci18n('announcements:men_singles'); - } else if (eventParts[1] == 'HD') { + } else if (eventParts[0] == 'HD') { eventName = ci18n('announcements:men_doubles'); - } else if (eventParts[1] == 'DE') { + } else if (eventParts[0] == 'DE') { eventName = ci18n('announcements:women_singles'); - } else if (eventParts[1] == 'DD') { + } else if (eventParts[0] == 'DD') { eventName = ci18n('announcements:women_doubles'); } - if (eventParts[0]) { - eventName = eventName + " " + eventParts[0]; + if (eventName == "") { + if (eventParts[1] == 'JE') { + eventName = ci18n('announcements:boys_singles'); + } else if (eventParts[1] == 'JD') { + eventName = ci18n('announcements:boys_doubles'); + } else if (eventParts[1] == 'ME') { + eventName = ci18n('announcements:girls_singles'); + } else if (eventParts[1] == 'MD') { + eventName = ci18n('announcements:girls_doubles') + } else if (eventParts[1] == 'GD' || eventParts[1] == 'MX') { + eventName = ci18n('announcements:mixed_doubles') + } else if (eventParts[1] == 'HE') { + eventName = ci18n('announcements:men_singles'); + } else if (eventParts[1] == 'HD') { + eventName = ci18n('announcements:men_doubles'); + } else if (eventParts[1] == 'DE') { + eventName = ci18n('announcements:women_singles'); + } else if (eventParts[1] == 'DD') { + eventName = ci18n('announcements:women_doubles'); + } + if (eventParts[0]) { + eventName = eventName + " " + eventParts[0]; + } + } else { + if (eventParts[1]) { + eventName = eventName + " " + eventParts[1]; + } } + return eventName; } else { - if (eventParts[1]) { - eventName = eventName + " " + eventParts[1]; - } + return null; } - return eventName; } function createMatchNumberAnnouncement(matchSetup) { - var number = matchSetup.match_num; - return ci18n('announcements:match_number') + number + "!"; + if (curt.annoncement_include_matchnumber) { + var number = matchSetup.match_num; + return ci18n('announcements:match_number') + number + "!"; + } else { + return null; + } } function createFieldAnnouncement(matchSetup) { diff --git a/static/js/change.js b/static/js/change.js index e60def8..cef52ba 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -63,6 +63,7 @@ function default_handler(rerender, special_funcs) { 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds','announcement_speed', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', + 'annoncement_include_event','annoncement_include_round','annoncement_include_matchnumber', 'preparation_meetingpoint_enabled','preparation_tabletoperator_setup_enabled']; for (const cb_name of CHECKBOXES) { uiu.qsEach('input[name="' + cb_name + '"]', function(el) { diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index caf925c..17f3fc7 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -109,6 +109,9 @@ var ci18n_de = { 'tournament:edit:tabletoperator_break_seconds': 'Pausenzeit nach Tabletbedienung (sec)', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', + 'tournament:edit:annoncement_include_event': 'Disziplin ansagen', + 'tournament:edit:annoncement_include_round': 'Turnierrunden ansagen', + 'tournament:edit:annoncement_include_matchnumber': 'Spielnummer ansagen', 'tournament:edit:normalizations': 'Ausspracheoptimierung', 'tournament:edit:normalizations:origin': 'Original', 'tournament:edit:normalizations:replace': 'Ersetzung', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index dfb652e..d025bc8 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -113,6 +113,9 @@ var ci18n_en = { 'tournament:edit:tabletoperator_with_state_enabled': 'Call up national association instead of player', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', + 'tournament:edit:annoncement_include_event': 'Announce event', + 'tournament:edit:annoncement_include_round': 'Announce round of tournament', + 'tournament:edit:annoncement_include_matchnumber': 'Announce number of match', 'tournament:edit:announcement_speed': 'Announcementspeed (0.8-1.3)', 'tournament:edit:preparation_meetingpoint_enabled': 'Use Meetingpoint for preparation', 'tournament:edit:preparation_tabletoperator_setup_enabled': 'Call up tablet operator in preparation', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 701e582..43a548c 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -843,6 +843,9 @@ var ctournament = (function() { create_checkbox(curt, tablet_fieldset, 'tabletoperator_split_doubles'); create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_state_enabled'); create_checkbox(curt, tablet_fieldset, 'tabletoperator_set_break_after_tabletservice'); + create_checkbox(curt, tablet_fieldset, 'annoncement_include_event'); + create_checkbox(curt, tablet_fieldset, 'annoncement_include_round'); + create_checkbox(curt, tablet_fieldset, 'annoncement_include_matchnumber'); create_checkbox(curt, tablet_fieldset, 'preparation_meetingpoint_enabled'); create_checkbox(curt, tablet_fieldset, 'preparation_tabletoperator_setup_enabled'); if (!curt.tabletoperator_break_seconds) { @@ -883,6 +886,9 @@ var ctournament = (function() { tabletoperator_with_state_enabled: (!!data.tabletoperator_with_state_enabled), tabletoperator_set_break_after_tabletservice: (!!data.tabletoperator_set_break_after_tabletservice), tabletoperator_use_manual_counting_boards_enabled: (!!data.tabletoperator_use_manual_counting_boards_enabled), + annoncement_include_event: (!!data.annoncement_include_event), + annoncement_include_round: (!!data.annoncement_include_round), + annoncement_include_matchnumber: (!!data.annoncement_include_matchnumber), tabletoperator_enabled: (!!data.tabletoperator_enabled), tabletoperator_break_seconds: data.tabletoperator_break_seconds, announcement_speed: data.announcement_speed, From f7103b71c2abe194aa18f19f1d44acff0a3946d4 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 4 Oct 2024 03:01:58 +0200 Subject: [PATCH 132/224] #62: Improve handling of incomplete matches in Main Gui MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The handling of the main GUI of games in which not all participants have been determined is to be improved so that the errors that are intercepted in tickets #60 and #59 can no longer occur. To achieve this, the following changes will be made: - Matches in which not all participants have been determined are no longer draggable. - For matches in which not all participants have been determined, the ‘Now on the field’ checkbox will be deactivated in the match editing dialogue. - The preparation button will not be displayed for matches in which not all participants have been determined. - The match sheet button is not displayed for matches in which not all participants have been determined. --- static/css/cmatch.css | 10 ++++++++++ static/js/cmatch.js | 23 +++++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 27246f9..70bffba 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -423,3 +423,13 @@ match_manual_call_button, display: none; } } + +.incomplete { + color: #aaaaaa; + font-style: italic; +} + +.player { + color: #000000; + font-style: normal; +} \ No newline at end of file diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 730042e..aa725d8 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -46,14 +46,21 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ court = curt.courts_by_id[match.setup.court_id]; } + const completeMatch = (match.setup.teams[0].players.length >= 1 && match.setup.teams[1].players.length >= 1); + if (style === 'default') { - if(!court){ + if(!court && completeMatch){ tr.setAttribute('draggable', 'true'); tr.addEventListener("dragstart", drag); tr.addEventListener("dragend", dragend); + tr.classList.add('complete'); } } + if(! completeMatch) { + tr.classList.add('incomplete'); + } + const waitForMatchStart = match.setup.called_timestamp && ( match.network_score == undefined || ( match.network_score[0] && @@ -65,7 +72,9 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ if (style === 'default' || style === 'plain') { const actions_td = uiu.el(tr, 'td'); create_match_button(actions_td, 'vlink match_edit_button', 'match:edit', on_edit_button_click, match._id); - create_match_button(actions_td, 'vlink match_scoresheet_button', 'match:scoresheet', on_scoresheet_button_click, match._id); + if(completeMatch) { + create_match_button(actions_td, 'vlink match_scoresheet_button', 'match:scoresheet', on_scoresheet_button_click, match._id); + } uiu.el(actions_td, 'a', { 'class': 'match_rawinfo', 'title': ci18n('match:rawinfo'), @@ -217,8 +226,9 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ const call_td = uiu.el(tr, 'td', 'call_td'); if (!court) { - create_match_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); - + if(completeMatch) { + create_match_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); + } } else { create_match_button(call_td, 'vlink match_manual_call_button', 'match:manualcall', on_announce_match_manually_button_click, match._id); create_match_button(call_td, 'vlink match_begin_to_play_button', 'match:begintoplay', on_begin_to_play_button_click, match._id); @@ -1366,6 +1376,7 @@ function render_edit(form, match) { size: 6, value: setup.scheduled_date || '', style: 'width: 238px;', + disabled: 'disabled', }); uiu.el(start, 'span', 'match_label', ci18n('Time:')); @@ -1377,6 +1388,7 @@ function render_edit(form, match) { size: 3, value: setup.scheduled_time_str || '', style: 'width: 268px;', + disabled: 'disabled', }); const player_table = uiu.el(edit_match_container, 'table'); @@ -1551,6 +1563,9 @@ function render_edit(form, match) { if (setup.now_on_court) { now_on_court_attrs.checked = 'checked'; } + if (setup.teams[0].players.length < 1 && setup.teams[1].players.length < 1) { + now_on_court_attrs.disabled = true; + } uiu.el(now_on_court_label, 'input', now_on_court_attrs); uiu.el(now_on_court_label, 'span', 'match_label', ci18n('match:edit:now_on_court')); From 4cba8e8c1de9a8ce5a8333c7fe1f48b6b21a5a29 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Mon, 7 Oct 2024 01:06:30 +0200 Subject: [PATCH 133/224] #63: Main GUI: Try to make the rows for the individual matches more static. --- static/css/cmatch.css | 94 +++++++++++++++++++++++++----- static/css/court.css | 6 +- static/js/cmatch.js | 131 ++++++++++++++++++++++++------------------ 3 files changed, 158 insertions(+), 73 deletions(-) diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 70bffba..0d99054 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -180,14 +180,6 @@ /*cursor: none;*/ } -.match_timer { - width: 3.6em; -} - -.call_td { - width: 5.5em; -} - .match_timer > div { font-size: 1.2em; font-weight: 900; @@ -201,19 +193,27 @@ .match_shuttle_count { white-space: pre; text-align: right; - padding-left: 0.4em; } + +.match_shuttle_count_display_active { + width: 40px; + background-color: yellow; +} + .match_shuttle_count span::after { content: ''; display: inline-block; - width: 1em; height: 1em; } -.match_shuttle_count span.match_shuttle_count_display_active::after { +.match_shuttle_image{ background-image: url("../../bup/icons/shuttle.svg"); - background-size: contain; - background-position: center; + height: 1.0em; + min-height: 1em; + width: 0.8em; + margin: 0.0em 0.0em -0.1em 0.0em; + background-size: 100%; background-repeat: no-repeat; + background-position: center center; } .match_no_umpire { @@ -343,6 +343,13 @@ match_manual_call_button, line-height: 100%; } +.umpire, +.service_judge, +.no_umpire, +.match_no_umpire { + text-wrap: nowrap; +} + .umpire { background-image: url(../icons/umpire.svg); } @@ -432,4 +439,65 @@ match_manual_call_button, .player { color: #000000; font-style: normal; +} + +td.actions { + width: 80px; +} + +td.court_number { + width: 60px; +} + +td.match_num { + width: 35px; + padding-right: 15px; +} + +td.match_properties { + width: 280px; +} + +.finished_container > table > tbody > tr > td.score { + width: 170px; +} + +.courts_container > table > tbody > tr > td.score { + width: 270px; +} + +td.umpire_and_tablet{ + width: 50px; + padding-right: 10px; +} + +td.match_timer { + width: 3.6em; +} + +td.call_td { + width: 5.5em; +} + +.name { + text-wrap: nowrap; +} + +.person { + text-wrap: nowrap; +} + +th.match_num { + text-align: center !important; +} + +th.players { + text-align: center !important; +} + +h3.section { + margin-left: 1%; + margin-right: 1%; + margin-top: 20px; + text-align: center; } \ No newline at end of file diff --git a/static/css/court.css b/static/css/court.css index eb7a800..30bd62c 100644 --- a/static/css/court.css +++ b/static/css/court.css @@ -19,9 +19,10 @@ 1px -1px 3px #000, -1px 1px 3px #000, 1px 1px 3px #000; - margin: -0.1em 0.3em 0.1em 0.3em; + margin: -0.0em 0.0em 0.05em 0.0em; line-height: 100%; } +.main_upcoming > div > .match_table > tbody > .court_row > .court_number > .court_num , .main_upcoming > div > .match_table > tbody > .court_row > .court_num { display: inline-block; background-size: cover; @@ -32,7 +33,6 @@ height: 1.0em; min-height: 1.0em; width: 1.3em; - min-width: 1.3em; font-weight: 900; font-size: 1.8em; -webkit-text-stroke-width: 1px; @@ -43,7 +43,7 @@ 1px -1px 3px #000, -1px 1px 3px #000, 1px 1px 3px #000; - margin: -0.05em 0.3em 0.05em 0.3em; + margin: -0.0em 0.0em 0.05em 0.0em; line-height: 100%; padding: 0.1em 0.2em 0em 0.2em; } diff --git a/static/js/cmatch.js b/static/js/cmatch.js index aa725d8..f9bde0d 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -27,9 +27,10 @@ function render_match_table_header(table) { const title_tr = uiu.el(thead, 'tr'); uiu.el(title_tr, 'th'); uiu.el(title_tr, 'th', {}, ci18n('Court')); - uiu.el(title_tr, 'th', {}, '#'); + uiu.el(title_tr, 'th', 'match_num', '#'); uiu.el(title_tr, 'th', {}, ci18n('Match')); uiu.el(title_tr, 'th', { + class: ('players'), colspan: 3, }, ci18n('Players')); uiu.el(title_tr, 'th', {}, ci18n('Umpire')); @@ -70,7 +71,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ const activeMatch = court && match.btp_winner != undefined; const setup = match.setup; if (style === 'default' || style === 'plain') { - const actions_td = uiu.el(tr, 'td'); + const actions_td = uiu.el(tr, 'td', 'actions'); create_match_button(actions_td, 'vlink match_edit_button', 'match:edit', on_edit_button_click, match._id); if(completeMatch) { create_match_button(actions_td, 'vlink match_scoresheet_button', 'match:scoresheet', on_scoresheet_button_click, match._id); @@ -83,13 +84,22 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } if (style === 'default') { - uiu.el(tr, 'td', court ? 'court_history' : '', court ? court.num : ''); + const court_number_td = uiu.el(tr, 'td','court_number'); + if(court) { + uiu.el(court_number_td, 'span', 'court_history', court.num); + } + //uiu.el(tr, 'td', court ? 'court_history' : 'empty_court', court ? court.num : ''); + } + + if (style === 'plain') { + const court_number_td = uiu.el(tr, "td", 'court_number'); + uiu.el(court_number_td, "div", 'court_num', court.num); } if (style === 'default' || style === 'plain') { const match_str = (setup.scheduled_time_str ? (setup.scheduled_time_str + ' ') : '') + (setup.match_name ? (setup.match_name + ' ') : '') + setup.event_name; uiu.el(tr, 'td', 'match_num', setup.match_num); - uiu.el(tr, 'td', {}, match_str); + uiu.el(tr, 'td', 'match_properties', match_str); } else if (style === 'upcoming') { uiu.el(tr, 'td', { style: 'min-width: 0.8em;' @@ -134,19 +144,22 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ - const to_td = uiu.el(tr, 'td'); + const to_td = uiu.el(tr, 'td', 'umpire_and_tablet'); if (style === 'default' || style === 'plain') { if (setup.umpire_name) { - uiu.el(to_td, 'div', 'umpire', ''); - uiu.el(to_td, 'span', {}, setup.umpire_name); + const umpire_span = uiu.el(to_td, 'span', 'person'); + uiu.el(umpire_span, 'div', 'umpire', ''); + uiu.el(umpire_span, 'span', 'name', setup.umpire_name); if (style === 'default' || style === 'plain') { - create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcallumpire', on_second_call_umpire_button_click, match._id); + create_match_button(umpire_span, 'vlink match_second_call_button', 'match:secondcallumpire', on_second_call_umpire_button_click, match._id); } if (setup.service_judge_name) { - uiu.el(to_td, 'div', 'service_judge', ''); - uiu.el(to_td, 'span', {}, setup.service_judge_name); + + const service_judge_span = uiu.el(to_td, 'span', 'person'); + uiu.el(service_judge_span, 'div', 'service_judge', ''); + uiu.el(service_judge_span, 'span', 'name', setup.service_judge_name); if (style === 'default' || style === 'plain') { - create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcallservicejudge', on_second_call_servicejudge_button_click, match._id); + create_match_button(service_judge_span, 'vlink match_second_call_button', 'match:secondcallservicejudge', on_second_call_servicejudge_button_click, match._id); } } } @@ -163,8 +176,9 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } if (!setup.umpire_name && (!setup.tabletoperators || setup.tabletoperators.length == 0)) { - uiu.el(to_td, 'div', 'no_umpire', ''); - uiu.el(to_td, 'span', 'match_no_umpire', ci18n('No umpire')); + const no_umpire_span = uiu.el(to_td, 'span', 'person'); + uiu.el(no_umpire_span, 'div', 'no_umpire', ''); + uiu.el(no_umpire_span, 'span', 'match_no_umpire', ci18n('No umpire')); } } else if(style === 'upcoming' && setup.highlight == 6) { @@ -173,11 +187,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ - const score_td = uiu.el(tr, 'td'); - if (court && (court.match_id !== match._id) && (typeof match.team1_won !== 'boolean') && setup.umpire_name) { - const ready_text = (style === 'public') ? ci18n('Ready') : ci18n(' Ready to start '); - uiu.el(score_td, 'span', {}, ready_text); - } + const score_td = uiu.el(tr, 'td', 'score'); const score_span = uiu.el(score_td, 'span', { 'class': ('match_score' + ((match.setup.now_on_court === true) ? ' match_score_current' : '')), 'data-match_id': match._id, @@ -203,16 +213,27 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ if ((style === 'default' || style === 'plain') && match.setup.now_on_court != undefined) { const shuttle_td = uiu.el(tr, 'td', 'match_shuttle_count'); + if(match.shuttle_count) { + shuttle_td.classList.add('match_shuttle_count_display_active'); + } + uiu.el(shuttle_td, 'span', { 'class': ( - 'match_shuttle_count_display' + - (match.shuttle_count ? ' match_shuttle_count_display_active' : '') + 'match_shuttle_count_number' ), 'data-match_id': match._id, }, match.shuttle_count || ''); + + const shutle_image = uiu.el(shuttle_td, 'div', 'match_shuttle_image'); + if(!match.shuttle_count) { + shutle_image.style.display = 'none'; + } + else { + shutle_image.style.display = 'inline-block' + } } - if (style === 'default' || style === 'plain') { + if ((style === 'default' || style === 'plain') && match.setup.now_on_court != false){ const timer_td = uiu.el(tr, 'td', {'class': 'match_timer', 'data-match_id': match._id}); var timer_state = _extract_match_timer_state(match); @@ -222,7 +243,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } } - if (style === 'default' || style === 'plain') { + if ((style === 'default' || style === 'plain') && match.setup.now_on_court != false) { const call_td = uiu.el(tr, 'td', 'call_td'); if (!court) { @@ -513,10 +534,11 @@ function update_player(match_id, player, now_on_court, show_player_status) { break; default: uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { - while (match_row_el.childElementCount > 1) { - match_row_el.removeChild(match_row_el.lastChild); - } - const c = {_id:m.setup.court_id}; + const court_number = match_row_el.getElementsByClassName('court_number')[0].children[0].innerHTML; + const c = { _id:m.setup.court_id, + num: court_number}; + + match_row_el.innerHTML = ""; render_droppable_row(match_row_el, c, 'plain', true); }); break; @@ -541,21 +563,9 @@ function update_match(m, old_section, new_section) { break; } } else { - switch (old_section) { - case 'finished': - case 'unassigned': - uiu.qsEach('.match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { - match_row_el.innerHTML = ''; - }); - break; - default: - uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { - while(match_row_el.childElementCount > 1){ - match_row_el.removeChild(match_row_el.lastChild); - } - }); - break; - } + uiu.qsEach('.match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { + match_row_el.innerHTML = ''; + }); } switch (new_section) { @@ -572,9 +582,7 @@ function update_match(m, old_section, new_section) { break; default: uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { - while(match_row_el.childElementCount > 1){ - match_row_el.removeChild(match_row_el.lastChild); - } + match_row_el.innerHTML = ""; const closest = match_row_el.closest('.main_upcoming'); if(Boolean(closest)) { render_match_row(match_row_el, m, null, 'public'); @@ -1005,10 +1013,7 @@ function ui_edit(match_id) { form_utils.onsubmit(form, function(d) { const setup = _make_setup(d); - console.log(setup); - console.log(match.setup); match.setup = _update_setup(match.setup, d); - console.log(match.setup); btn.setAttribute('disabled', 'disabled'); send({ type: 'match_edit', @@ -1144,7 +1149,7 @@ function render_match_table(container, matches, show_player_status, show_add_tab function render_unassigned(container) { uiu.empty(container); - uiu.el(container, 'h3', {}, ci18n('Unassigned Matches')); + uiu.el(container, 'h3', 'section', ci18n('Unassigned Matches')); const unassigned_matches = curt.matches.filter(m => calc_section(m) === 'unassigned'); render_match_table(container, unassigned_matches, true, true); @@ -1177,7 +1182,7 @@ function render_upcoming_matches(container) { function render_finished(container) { uiu.empty(container); - uiu.el(container, 'h3', {}, ci18n('Finished Matches')); + uiu.el(container, 'h3', 'section', ci18n('Finished Matches')); const matches = curt.matches.filter(m => calc_section(m) === 'finished').sort((a, b) => {return b.end_ts - a.end_ts}); render_match_table(container, matches, false, true); @@ -1194,11 +1199,15 @@ function render_courts(container, style) { const tr = uiu.el(tbody, 'tr', {class:"court_row", "data-court_id":c._id} ); const rowspan = Math.max(1, court_matches.length); - uiu.el(tr, 'th', { - 'class': 'court_num', - rowspan, - title: c._id, - }, c.num); + //uiu.el(tr, 'th', { + // 'class': 'court_num', + // rowspan, + // title: c._id, + //}, c.num); + + //const court_number_td = uiu.el(tr, "td", 'court_number'); + //uiu.el(court_number_td, "div", "court_num", c.num); + if (court_matches.length === 0) { render_droppable_row(tr, c, style, true); @@ -1214,7 +1223,15 @@ function render_courts(container, style) { } function render_droppable_row(tr, court, style, is_droppable) { + const lead_target_td = uiu.el(tr, 'td', {class: "droppable", colspan: 1, "data-court_id":court._id}, ''); + + const court_number_td = uiu.el(tr, "td", {'class':('court_number', 'droppable'), "data-court_id":court._id}); + uiu.el(court_number_td, "div", 'court_num', court.num); + const target_td = uiu.el(tr, 'td', {class: "droppable", colspan: 11, "data-court_id":court._id}, ''); + + lead_target_td.addEventListener("drop", drop); + lead_target_td.addEventListener("dragover", allowDrop); target_td.addEventListener("drop", drop); target_td.addEventListener("dragover", allowDrop); @@ -1271,9 +1288,9 @@ function drop(ev) { } }); - for (const dropp_row of document.getElementsByClassName("droppable")) { - dropp_row.setAttribute("class", "droppable"); - } + //for (const dropp_row of document.getElementsByClassName("droppable")) { + // dropp_row.setAttribute("class", "droppable"); + //} } } From fe3a5d8a263e2792c43446f112bdaa6e5d73d38e Mon Sep 17 00:00:00 2001 From: tengmel Date: Mon, 7 Oct 2024 20:42:14 +0200 Subject: [PATCH 134/224] #64: Server.Core: Call Matchen in preparation after call a new match on court #65: Server.Core: Automatic call of a match which is in preparation when a match is finished --- bts/admin.js | 103 ++--------------- bts/btp_sync.js | 31 +++++- bts/http_api.js | 79 +++++++------ bts/match_utils.js | 232 +++++++++++++++++++++++++++++++++++---- static/js/change.js | 3 +- static/js/ci18n_de.js | 6 + static/js/ci18n_en.js | 4 +- static/js/cmatch.js | 28 ++++- static/js/ctournament.js | 9 +- 9 files changed, 346 insertions(+), 149 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index a2b4fb6..f5296be 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -64,7 +64,8 @@ function handle_tournament_edit_props(app, ws, msg) { 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', 'annoncement_include_event', 'annoncement_include_round','annoncement_include_matchnumber', - 'preparation_meetingpoint_enabled','preparation_tabletoperator_setup_enabled', + 'preparation_meetingpoint_enabled', 'preparation_tabletoperator_setup_enabled', + 'call_preparation_matches_automatically_enabled', 'call_next_possible_scheduled_match_in_preparation' , 'logo_background_color', 'logo_foreground_color']); if (msg.props.btp_timezone) { @@ -239,6 +240,7 @@ function _extract_setup(msg_setup) { 'scheduled_time_str', 'scheduled_date', 'called_timestamp', + 'preparation_call_timestamp', 'teams', 'team_competition', 'tabletoperators', @@ -489,14 +491,8 @@ function handle_match_call_on_court(app, ws, msg) { match.setup.court_id = msg.court_id; match.setup.now_on_court = true; match_utils.call_match(app, tournament, match, (err, updated_match) => { - if (err) { - ws.respond(msg, err); - return; - } - update_btp_courts(app, msg.tournament_key, updated_match, (err) => { - ws.respond(msg, err); - return; - }); + ws.respond(msg, err); + return; }); }else { return ws.respond(msg, "Match cannot be fetched from DB " + msg.match_id); @@ -522,27 +518,13 @@ function handle_match_edit(app, ws, msg) { if(setup.now_on_court && !setup.called_timestamp) { match_utils.call_match(app, tournament, msg.match, (err, match) => { - if (err) { - ws.respond(msg, err); - return; - } - - update_btp_courts(app, tournament_key, msg.match, (err) => { - ws.respond(msg, err); - return; - }) + ws.respond(msg, err); + return; }); } else if (!setup.now_on_court && setup.called_timestamp) { match_utils.uncall_match(app, tournament, msg.match, (err) => { - if (err) { - ws.respond(msg, err); - return; - } - - update_btp_courts(app, tournament_key, msg.match, (err) => { - ws.respond(msg, err); - return; - }); + ws.respond(msg, err); + return; }); } else { @@ -573,38 +555,7 @@ function handle_match_edit(app, ws, msg) { }); } -function update_btp_courts(app, tournament_key, match, callback) { - stournament.get_courts(app.db, tournament_key, (err, all_courts) => { - if (err) { - callback(err); - return; - } - const courts = []; - - all_courts.forEach((element, index) => { - if(match.setup.court_id === element._id && match.setup.now_on_court) { - const court = { - btp_id: element.btp_id, - btp_match_id: match.btp_match_ids[0].id, - } - - courts.push(court); - } else if (element.match_id && element.match_id == ("btp_" + match.btp_id) && !match.setup.now_on_court) { - const court = { - btp_id: element.btp_id - } - - courts.push(court); - } - }); - - btp_manager.update_courts(app, tournament_key, courts); - - callback(null); - return; - }); -} function handle_match_preparation_call(app, ws, msg) { @@ -618,46 +569,16 @@ function handle_match_preparation_call(app, ws, msg) { return ws.respond("Match cannot be called one or more Teams are not set."); } - const tournament_key = msg.tournament_key; app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { if (err) { return ws.respond(err); } - const setup = _extract_setup(msg.setup); - if (tournament.preparation_tabletoperator_setup_enabled) { - if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { - if (!setup.tabletoperators || setup.tabletoperators == null) { - const admin = require('./admin'); - const match_utils = require('./match_utils'); - setup.tabletoperators = await match_utils.fetch_tabletoperator(admin, app, tournament_key, "prep_call"); - } - } - } - match_utils.set_umpire_to_standby(app, tournament_key, setup); - - app.db.matches.update({ _id: msg.id, tournament_key }, { $set: { setup } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_match) { - if (err) { - ws.respond(msg, err); - return; - } - if (numAffected !== 1) { - ws.respond(msg, new Error('Cannot find match ' + msg.id + ' of tournament ' + tournament_key + ' in database')); - return; - } - if (changed_match._id !== msg.id) { - const errmsg = 'Match ' + changed_match._id + ' changed by accident, intended to change ' + msg.id + ' (old nedb version?)'; - serror.silent(errmsg); - ws.respond(msg, new Error(errmsg)); - return; - } - - //notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, setup}); - notify_change(app, tournament_key, 'match_preparation_call', { match__id: msg.id, match: changed_match }); - - btp_manager.update_highlight(app, changed_match); + const setup = _extract_setup(msg.setup); + match_utils.call_match_in_preparation(app, tournament, msg.id, setup, (err) => { ws.respond(msg, err); + return; }); }); } diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 0e7c143..d83c6da 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -434,10 +434,23 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { craft_match(app, tkey, btp_id, court_map, event, draw, btp_state.links, officials, bm, match_ids_on_court).then(match => { if (cur_match) { + match.setup.state = 'unscheduled'; + if (match.setup.scheduled_date && match.setup.scheduled_time_str) { + match.setup.state = 'scheduled'; + } + + if (match.setup.incomplete == true) { + match.setup.state = 'incomplete'; + } + if (cur_match.team1_won === null) { cur_match.team1_won = undefined; } + if (cur_match.btp_winner) { + match.setup.state = 'finished'; + } + if (!match.network_score && cur_match.network_score) { match.network_score = cur_match.network_score; } @@ -446,6 +459,17 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. match.setup.called_timestamp = cur_match.setup.called_timestamp; } + if (cur_match.setup.called_timestamp) { + // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. + match.setup.called_timestamp = cur_match.setup.called_timestamp; + } + + + if (cur_match.setup.preparation_call_timestamp) { + // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. + match.setup.preparation_call_timestamp = cur_match.setup.preparation_call_timestamp; + match.setup.state = 'preparartion'; + } if (cur_match.setup.tabletoperators) { // tabletoperators is not from btp so we have to coppy it to the match generated by btp. @@ -504,6 +528,10 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { } } + if (match.setup.now_on_court === true) { + match.setup.state = 'oncourt'; + } + for (let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { if ('tablet_break_active' in cur_match.setup.teams[team_index].players[player_index]) { @@ -945,8 +973,7 @@ async function integrate_now_on_court(app, tkey, callback) { const court_id = match.setup.court_id; const match_id = match._id; - //const called_timestamp = Date.now(); - + if (!court_id || !match_id) { return; // TODO in async we would assert both to be true } diff --git a/bts/http_api.js b/bts/http_api.js index bf824ae..2acb911 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -242,35 +242,39 @@ function matchinfo_handler(req, res) { }); } -function score_handler(req, res) { +async function score_handler(req, res) { if (!_require_params(req, res, ['duration_ms', 'end_ts', 'network_score', 'team1_won', 'presses'])) return; - + const match_utils = require('./match_utils'); const tournament_key = req.params.tournament_key; const match_id = req.params.match_id; - const query = { - _id: match_id, - tournament_key, - }; + + + var match = await match_utils.fetch_match(req.app, tournament_key, match_id); + if (match == null || match.setup.now_on_court == false) { + res.json({ + status: 'error', + message: "Match not found or not on court actualy.", + }); + return; + } const update = { network_score: req.body.network_score, network_team1_left: req.body.network_team1_left, network_team1_serving: req.body.network_team1_serving, network_teams_player1_even: req.body.network_teams_player1_even, - team1_won: req.body.team1_won, presses: req.body.presses, duration_ms: req.body.duration_ms, end_ts: req.body.end_ts, 'setup.now_on_court': true, }; - if (update.team1_won !== undefined && update.team1_won != null) { + const finish_confirmed = req.body.finish_confirmed ? req.body.finish_confirmed : false; + if (finish_confirmed) { + update.team1_won = req.body.team1_won, update.btp_winner = (update.team1_won === true) ? 1 : 2; update.btp_needsync = true; - update['setup.now_on_court'] = false; - } - - if (update.team1_won != undefined && update.team1_won != null) { + update["setup.now_on_court"] = false; reset_player_tabletoperator(req.app, tournament_key, match_id, update.end_ts); } @@ -278,16 +282,21 @@ function score_handler(req, res) { update.shuttle_count = req.body.shuttle_count; } + const match_query = { + _id: match_id, + tournament_key, + }; + const court_q = { tournament_key, _id: req.body.court_id, }; const db = req.app.db; - + async.waterfall([ - cb => db.matches.update(query, {$set: update}, {returnUpdatedDocs: true}, (err, _, match) => cb(err, match)), + cb => db.matches.update(match_query, { $set: update }, { returnUpdatedDocs: true }, (err, _, match) => cb(err, match)), (match, cb) => { - if (match) { + if (match) { bupws.handle_score_change(req.app, tournament_key, match.setup.court_id); admin.notify_change(req.app, tournament_key, 'score', { match_id, @@ -297,23 +306,17 @@ function score_handler(req, res) { presses: match.presses, }); } - cb(null,match); - }, - (match, cb) => { - if (!match) { - return cb(new Error('Cannot find match ' + JSON.stringify(match))); - } - return cb(null, match); + cb(null, match); }, (match, cb) => db.courts.findOne(court_q, (err, court) => cb(err, match, court)), (match, court, cb) => { - if (!match) { + if (!match) { if (court.match_id === match_id) { cb(null, match, court, false); return; } - db.courts.update(court_q, {$set: {match_id: match_id}}, {}, (err) => { + db.courts.update(court_q, { $set: { match_id: match_id } }, {}, (err) => { cb(err, match, court, true); }); } @@ -337,12 +340,12 @@ function score_handler(req, res) { (match, changed_court, cb) => { if (match && match.setup.highlight && match.setup.highlight == 6 && - match.network_score && - match.network_score.length > 0 && - match.network_score[0].length > 1 && + match.network_score && + match.network_score.length > 0 && + match.network_score[0].length > 1 && (match.network_score[0][0] > 0 || match.network_score[0][1] > 0)) { - match.setup.highlight = 0; - btp_manager.update_highlight(req.app, match); + match.setup.highlight = 0; + btp_manager.update_highlight(req.app, match); } cb(null, match, changed_court); }, @@ -354,9 +357,20 @@ function score_handler(req, res) { ticker_manager.update_score(req.app, match); } } - cb(null, match); + cb(null, match, changed_court); }, - ], function(err) { + (match, changed_court, cb) => { + if (!match) { + return cb(new Error('Cannot find match ' + JSON.stringify(match))); + } + if (finish_confirmed && match.team1_won != undefined && match.team1_won != null) { + match_utils.call_preparation_match_on_court(req.app, tournament_key, match.setup.court_id, (err) => { + + }); + } + return cb(null, match, changed_court); + }, + ], function (err) { if (err) { res.json({ status: 'error', @@ -365,8 +379,9 @@ function score_handler(req, res) { return; } - res.json({status: 'ok'}); + res.json({ status: 'ok' }); }); + } function reset_player_tabletoperator(app, tournament_key, match_id, end_ts) { diff --git a/bts/match_utils.js b/bts/match_utils.js index bdaa554..d684c4d 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -3,7 +3,6 @@ const assert = require('assert'); const async = require('async'); - async function match_update(app, match, callback) { async.waterfall([ (wcb) => update_match_btp(app, match, wcb), @@ -23,19 +22,19 @@ async function uncall_match(app, tournament, match, callback) { // Requrements async.waterfall([ (wcb) => remove_called_timestamp(match, wcb), - (wcb) => remove_tablet_on_court(app, tournament.key, match._id, null, wcb), - (wcb) => remove_tablet_operator_to_list(app, tournament.key, match, wcb), - (wcb) => update_match_btp(app, match, wcb), - (wcb) => update_match_db(app, match, wcb), - (wcb) => update_court_db(app, match, wcb), - (wcb) => notify_change_match_edit(app, match, wcb), - (wcb) => notify_bupws(app, match, wcb), - (wcb) => remove_player_on_court(app, tournament.key, match._id, null, wcb) - ], - (err) => { - return callback(err); - } - ); + (wcb) => remove_tablet_on_court(app, tournament.key, match._id, null, wcb), + (wcb) => remove_tablet_operator_to_list(app, tournament.key, match, wcb), + (wcb) => update_match_btp(app, match, wcb), + (wcb) => update_match_db(app, match, wcb), + (wcb) => update_court_db(app, match, wcb), + (wcb) => notify_change_match_edit(app, match, wcb), + (wcb) => notify_bupws(app, match, wcb), + (wcb) => remove_player_on_court(app, tournament.key, match._id, null, wcb), + (wcb) => update_btp_courts(app, tournament.key, match, wcb)], + (err) => { + return callback(err); + } + ); } @@ -57,7 +56,9 @@ async function call_match(app, tournament, match, callback) { (wcb) => notify_change_match_called_on_court(app, match, wcb), (wcb) => notify_bupws(app, match, wcb), (wcb) => set_player_on_court(app, tournament.key, match.setup, wcb), - (wcb) => set_player_on_tablet(app, tournament.key, match.setup, wcb)], + (wcb) => set_player_on_tablet(app, tournament.key, match.setup, wcb), + (wcb) => update_btp_courts(app, tournament.key, match, wcb), + (wcb) => call_next_possible_match_for_preparation(app, tournament.key, wcb)], (err) => { return callback(err, match); } @@ -74,20 +75,28 @@ function match_completly_initialized(setup) { function add_called_timestamp(match, callback) { const setup = match.setup; const called_timestamp = Date.now(); - setup.called_timestamp = called_timestamp; - + setup.state = 'oncourt'; + remove_preparation_call_timestamp(setup); return callback(null); } function remove_called_timestamp(match, callback) { const setup = match.setup; - setup.called_timestamp = undefined; - + setup.state = 'scheduled'; return callback(null); } +function add_preparation_call_timestamp(setup) { + setup.highlight = 6; + setup.preparation_call_timestamp = Date.now(); + setup.state = 'preparartion'; +} + +function remove_preparation_call_timestamp(setup) { + setup.preparation_call_timestamp = undefined; +} function remove_tablet_operator_to_list(app, tkey, match, callback) { add_tabletoperator_to_tabletoperator_list_by_match(app, tkey, match); @@ -356,24 +365,28 @@ function set_player_on_court (app, tkey, match_on_court_setup, callback) { if (match.setup.teams[0].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { match.setup.teams[0].players[0].now_playing_on_court = match_on_court_setup.court_id; match.setup.teams[0].players[0].tablet_break_active = false; + match.setup.state = 'blocked'; change = true; } if (match.setup.teams[0].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { match.setup.teams[0].players[1].now_playing_on_court = match_on_court_setup.court_id; match.setup.teams[0].players[1].tablet_break_active = false; + match.setup.state = 'blocked'; change = true; } if (match.setup.teams[1].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { match.setup.teams[1].players[0].now_playing_on_court = match_on_court_setup.court_id; match.setup.teams[1].players[0].tablet_break_active = false; + match.setup.state = 'blocked'; change = true; } if (match.setup.teams[1].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { match.setup.teams[1].players[1].now_playing_on_court = match_on_court_setup.court_id; match.setup.teams[1].players[1].tablet_break_active = false; + match.setup.state = 'blocked'; change = true; } if (change) { @@ -484,6 +497,20 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ }); } } +function fetch_match(app, tournament_key, match_id) { + return new Promise((resolve, reject) => { + app.db.matches.findOne({ tournament_key: tournament_key, _id: match_id }, async (err, match) => { + if (err) { + return reject(err); + } + if (match != null) { + return resolve(match) + } else { + return reject("Match cannot be fetched from DB " + match_id); + } + }); + }); +} function create_team_from_player_state(player) { return { @@ -766,15 +793,182 @@ function update_umpire(app, tkey, umpire_name, status, last_time_on_court_ts, co }); } +async function call_preparation_match_on_court(app, tournament_key, court_id, callback) { + app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { + if (err) { + return callback("No tournament found for "); + } + if (tournament.call_preparation_matches_automatically_enabled) { + const match_querry = { 'tournament_key': tournament_key, 'setup.highlight': 6 }; + app.db.matches.find(match_querry).sort({ 'setup.preparation_call_timestamp': 1 }).exec((err, matches) => { + if (err) { + return callback(msg, err); + } + if (matches && matches.length > 0) { + const next_match = matches[0]; + next_match.setup.court_id = court_id; + next_match.setup.now_on_court = true; + call_match(app, tournament, next_match, callback); + + } else { + return callback("No match found to call on court."); + } + }); + } else { + return callback(null); + } + }); +} + +async function call_next_possible_match_for_preparation(app, tournament_key, callback) { + app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { + if (err) { + return callback("No tournament found for "); + } + if (tournament.call_next_possible_scheduled_match_in_preparation) { + const match_querry = { 'tournament_key': tournament_key, 'setup.state': 'scheduled' }; + app.db.matches.find(match_querry).sort({ 'setup.scheduled_date': 1, 'setup.scheduled_time_str': 1, 'setup.match_number': 1 }).exec((err, matches) => { + if (err) { + return callback(msg, err); + } + if (matches && matches.length > 0) { + const now = new Date(); + for (var i = 0; i < matches.length; ++i) { + var match = matches[i]; + var possible = true; + for (let team_index = 0; team_index < Math.min(match.setup.teams.length, match.setup.teams.length); team_index++) { + for (let player_index = 0; player_index < Math.min(match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { + if (possible == true) { + const player = match.setup.teams[team_index].players[player_index]; + if (player.now_playing_on_court != undefined) { + if (player.now_playing_on_court === false) { + possible = true; + } else { + possible = false; + } + } + if (possible) { + if (player.now_tablet_on_court != undefined) { + if (player.now_tablet_on_court === false) { + possible = true; + } else { + possible = false; + } + } + if (possible) { + if (player.last_time_on_court_ts) { + const last_time_on_court = new Date(player.last_time_on_court_ts); + if ((now - last_time_on_court) < tournament.btp_settings.pause_duration_ms) { + possible = false; + } else { + possible = true; + } + } + } + } + } + } + } + if (possible) { + call_match_in_preparation(app, tournament,match._id, match.setup, callback); + break; + } + } + } else { + return callback("No match found to call on court."); + } + }); + } else { + return callback(null); + } + }); +} + + +async function call_match_in_preparation(app, tournament, match_id, setup, callback) { + add_preparation_call_timestamp(setup); + const tournament_key = tournament.key; + const admin = require('./admin'); + + if (tournament.preparation_tabletoperator_setup_enabled) { + if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { + if (!setup.tabletoperators || setup.tabletoperators == null) { + setup.tabletoperators = await fetch_tabletoperator(admin, app, tournament_key, "prep_call"); + } + } + } + set_umpire_to_standby(app, tournament_key, setup); + + app.db.matches.update({ _id: match_id, tournament_key }, { $set: { setup } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_match) { + if (err) { + return callback(err); + } + if (numAffected !== 1) { + return callback(new Error('Cannot find match ' + match_id + ' of tournament ' + tournament_key + ' in database')); + } + if (changed_match._id !== match_id) { + const errmsg = 'Match ' + changed_match._id + ' changed by accident, intended to change ' + match_id + ' (old nedb version?)'; + serror.silent(errmsg); + + return callback(new Error(errmsg)); + } + admin.notify_change(app, tournament_key, 'match_preparation_call', { match__id: match_id, match: changed_match }); + const btp_manager = require('./btp_manager'); + btp_manager.update_highlight(app, changed_match); + return callback (null); + }); +} + +function update_btp_courts(app, tournament_key, match, callback) { + const stournament = require('./stournament'); + const btp_manager = require('./btp_manager'); + stournament.get_courts(app.db, tournament_key, (err, all_courts) => { + if (err) { + callback(err); + return; + } + + const courts = []; + + all_courts.forEach((element, index) => { + if (match.setup.court_id === element._id && match.setup.now_on_court) { + const court = { + btp_id: element.btp_id, + btp_match_id: match.btp_match_ids[0].id, + } + + courts.push(court); + } else if (element.match_id && element.match_id == ("btp_" + match.btp_id) && !match.setup.now_on_court) { + const court = { + btp_id: element.btp_id + } + + courts.push(court); + } + }); + + btp_manager.update_courts(app, tournament_key, courts); + + callback(null); + return; + }); +} + module.exports ={ add_player_to_tabletoperator_list, call_match, match_update, uncall_match, + fetch_match, fetch_tabletoperator, match_completly_initialized, remove_player_on_court, remove_tablet_on_court, remove_umpire_on_court, set_umpire_to_standby, + add_preparation_call_timestamp, + remove_preparation_call_timestamp, + call_preparation_match_on_court, + call_next_possible_match_for_preparation, + call_match_in_preparation }; \ No newline at end of file diff --git a/static/js/change.js b/static/js/change.js index cef52ba..bfc7af6 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -63,7 +63,8 @@ function default_handler(rerender, special_funcs) { 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds','announcement_speed', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', - 'annoncement_include_event','annoncement_include_round','annoncement_include_matchnumber', + 'annoncement_include_event', 'annoncement_include_round', 'annoncement_include_matchnumber', + 'call_preparation_matches_automatically_enabled','call_next_possible_scheduled_match_in_preparation', 'preparation_meetingpoint_enabled','preparation_tabletoperator_setup_enabled']; for (const cb_name of CHECKBOXES) { uiu.qsEach('input[name="' + cb_name + '"]', function(el) { diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 17f3fc7..0f5ad9b 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -131,6 +131,12 @@ var ci18n_de = { 'tournament:edit:ticker': 'Ticker Einstellungen:', 'tournament:edit:btp': 'Badminton Turnier Planer Einstellungen:', + 'tournament:edit:bts': 'Badminton Turnier Server Einstellungen:', + 'tournament:edit:call_preparation_matches_automatically_enabled': 'Spiele in Vorbereitung automatisch auf freien Felden aufrufen', + 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Nächst mögliches Spiel automatisch in Vorbereitung aufrufen', + + + 'to_stats:header': 'Statistik der Technischen Offiziellen', 'to_stats:name': 'Name', 'to_stats:umpire': 'SR', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index d025bc8..2ce7432 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -131,7 +131,9 @@ var ci18n_en = { 'tournament:edit:tablets': 'Tablets Settings:', 'tournament:edit:ticker': 'Ticker Settings:', 'tournament:edit:btp': 'Badminton Tournament Planer Settings:', - + 'tournament:edit:bts': 'Badminton Tournament Server Settings:', + 'tournament:edit:call_preparation_matches_automatically_enabled': 'Call games in preparation on free fields automatically', + 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Automatically call up next possible game in preparation', 'to_stats:header': 'Technical Officials Statistics', 'to_stats:name': 'Name', 'to_stats:umpire': 'U', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index f9bde0d..16b69e9 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -240,6 +240,12 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ var timer = create_timer(timer_state, timer_td, "#cccccc", "#ff0000"); if (timer) { active_timers.matches[match._id] = timer; + } else { + var preparation_timer_state = _extract_preparation_timer_state(match); + var preparation_timer = create_timer(preparation_timer_state, timer_td, "#cccccc", "#ff0000"); + if (preparation_timer) { + active_timers.matches[match._id] = preparation_timer; + } } } @@ -302,6 +308,12 @@ function update_match_score(m) { var timer = create_timer(timer_state, timer_td, "#cccccc", "#ff0000"); if (timer) { active_timers.matches[m._id] = timer; + } else { + var preparation_timer_state = _extract_preparation_timer_state(m); + var preparation_timer = create_timer(preparation_timer_state, timer_td, "#cccccc", "#ff0000"); + if (preparation_timer) { + active_timers.matches[match._id] = preparation_timer; + } } }); @@ -657,6 +669,20 @@ function _extract_player_timer_state(player) { return s; } +function _extract_preparation_timer_state(match) { + let s = {}; + s.settings = {}; + s.settings.negative_timers = false; + s.lang = "de"; + s.timer = {}; + //s.timer.duration = curt.btp_settings.pause_duration_ms; + s.timer.start = (match.setup.preparation_call_timestamp ? match.setup.preparation_call_timestamp : false); + s.timer.upwards = true; + s.timer.exigent = false; + s.bgColor = "#C56BFF"; + return s; +} + function _extract_match_timer_state(match) { var presses = match.presses; @@ -720,8 +746,6 @@ function on_scoresheet_button_click(e) { function on_announce_preparation_matchbutton_click(e) { const match = fetchMatchFromEvent(e); if (match != null) { - match.setup.highlight = 6; //its magenta - send({ type: 'match_preparation_call', id: match._id, diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 43a548c..90629aa 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -727,6 +727,11 @@ var ctournament = (function() { } }; + const bts_fieldset = uiu.el(form, 'fieldset'); + uiu.el(bts_fieldset, 'h2', {}, ci18n('tournament:edit:bts')); + create_checkbox(curt, bts_fieldset, 'call_preparation_matches_automatically_enabled'); + create_checkbox(curt, bts_fieldset, 'call_next_possible_scheduled_match_in_preparation'); + // BTP const btp_fieldset = uiu.el(form, 'fieldset'); uiu.el(btp_fieldset, 'h2', {}, ci18n('tournament:edit:btp')); @@ -893,7 +898,9 @@ var ctournament = (function() { tabletoperator_break_seconds: data.tabletoperator_break_seconds, announcement_speed: data.announcement_speed, preparation_meetingpoint_enabled: (!!data.preparation_meetingpoint_enabled), - preparation_tabletoperator_setup_enabled: (!!data.preparation_tabletoperator_setup_enabled) + preparation_tabletoperator_setup_enabled: (!!data.preparation_tabletoperator_setup_enabled), + call_preparation_matches_automatically_enabled: (!!data.call_preparation_matches_automatically_enabled), + call_next_possible_scheduled_match_in_preparation: (!!data.call_next_possible_scheduled_match_in_preparation) }; send({ type: 'tournament_edit_props', From e1b368f84698b3fddd76c670d89ca75513baa3cb Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 8 Oct 2024 00:39:08 +0200 Subject: [PATCH 135/224] #66: BUPWS / Settings GUI: Show hostname of displays --- bts/bupws.js | 65 ++++++++++++++++++++++++++++++++++++---- static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/ctournament.js | 2 ++ 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index cf16f28..9ccf77b 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -4,6 +4,9 @@ const { forEach } = require('async'); const serror = require('./serror'); const utils = require('./utils'); const admin = require('./admin'); +const dns = require('dns'); +const cp = require('child_process'); +const os = require('os'); const all_panels = []; @@ -27,12 +30,13 @@ async function notify_admin_display_status_changed(app, ws, ws_online) { err = { message: 'No tournament ' + default_tournament_key }; } const client_id = determine_client_id(ws); + const hostname = await determine_client_hostname(ws); var display_court_displaysetting = await get_display_court_displaysettings(app, client_id); if (display_court_displaysetting == null) { - display_court_displaysetting = create_display_court_displaysettings(client_id, null, generate_default_displaysettings_id(tournament)); + display_court_displaysetting = create_display_court_displaysettings(client_id, hostname, null, generate_default_displaysettings_id(tournament)); } display_court_displaysetting.online = ws_online; - admin.notify_change(app, default_tournament_key, 'display_status_changed', { 'display_court_displaysetting': display_court_displaysetting }); + admin.notify_change(app, default_tournament_key, 'display_status_changed', {'display_court_displaysetting': display_court_displaysetting }); }); } @@ -90,11 +94,12 @@ async function handle_persist_display_settings(app, ws, msg) { var setting = msg.panel_settings; const client_id = determine_client_id(ws); + const hostname = await determine_client_hostname(ws); var client_court_displaysetting = await get_display_court_displaysettings(app, client_id); if (client_court_displaysetting == null) { setting.id = tournament_key + "_" + court_id + " _" + Date.now(); setting = await persist_displaysetting(app, setting); - client_court_displaysetting = create_display_court_displaysettings(client_id, court_id, setting.id); + client_court_displaysetting = create_display_court_displaysettings(client_id, hostname, court_id, setting.id); client_court_displaysetting = await persist_client_court_displaysetting(app, client_court_displaysetting); } else { setting.id = tournament_key + "_" + court_id + " _" + Date.now(); @@ -107,9 +112,10 @@ async function handle_persist_display_settings(app, ws, msg) { } } -function create_display_court_displaysettings(client_id, court_id, displaysetting_id) { +function create_display_court_displaysettings(client_id, hostname, court_id, displaysetting_id) { return { client_id: client_id, + hostname: hostname, court_id: court_id, displaysetting_id: displaysetting_id, } @@ -133,6 +139,7 @@ async function handle_init(app, ws, msg) { async function initialize_client(ws, app, tournament_key, court_id, displaysetting) { const client_id = determine_client_id(ws); + const hostname = await determine_client_hostname(ws); if (client_id) { let display_setting = await get_display_setting(app, tournament_key, client_id, court_id, displaysetting) if (display_setting != null) { @@ -145,10 +152,55 @@ async function initialize_client(ws, app, tournament_key, court_id, displaysetti matches_handler(app, ws, tournament_key, ws.court_id); } +function getComputerName() { + switch (process.platform) { + case "win32": + return process.env.COMPUTERNAME; + case "darwin": + return cp.execSync("scutil --get ComputerName").toString().trim(); + case "linux": + const prettyname = cp.execSync("hostnamectl --pretty").toString().trim(); + return prettyname === "" ? os.hostname() : prettyname; + default: + return os.hostname(); + } + } + +async function determine_client_hostname(ws) { + return new Promise((resolve, reject)=> { + if(!ws.hostname) { + const remote_adress_seqments_v6 = ws._socket.remoteAddress.split(':'); + const ip_v4 = remote_adress_seqments_v6[remote_adress_seqments_v6.length - 1]; + + // catch ip for localhost + if(ip_v4 == '127.0.0.1') { + ws.hostname = getComputerName(); + resolve(ws.hostname); + return; + } + + dns.reverse(ip_v4, (err, hostnames) => { + if(err) { + reject(err); + } + if(hostnames.length >= 1) { + ws.hostname = hostnames[0].split('.')[0]; + } + resolve(ws.hostname); + }); + + } + else { + resolve(ws.hostname); + } + }); +} + function determine_client_id(ws) { if (!ws.client_id) { const remote_adress_seqments = ws._socket.remoteAddress.split('.'); ws.client_id = remote_adress_seqments[remote_adress_seqments.length - 1]; + } return ws.client_id; } @@ -510,7 +562,7 @@ function reinitialize_panel(app, tournament_key, client_id, new_court_id, displa return false;; } -function add_display_status(app, tournament, displays) { +async function add_display_status(app, tournament, displays) { for (const d of displays) { d.online = false; for (const panel_ws of all_panels) { @@ -523,6 +575,7 @@ function add_display_status(app, tournament, displays) { for (const panel_ws of all_panels) { var found = false; const ws_client_id = determine_client_id(panel_ws); + const ws_hostname = await determine_client_hostname(panel_ws); for (const d of displays) { if (d.client_id == ws_client_id) { @@ -530,7 +583,7 @@ function add_display_status(app, tournament, displays) { } } if (!found) { - const client_court_displaysetting = create_display_court_displaysettings(ws_client_id, panel_ws.court_id, generate_default_displaysettings_id(tournament)); + const client_court_displaysetting = create_display_court_displaysettings(ws_client_id, ws_hostname, panel_ws.court_id, generate_default_displaysettings_id(tournament)); client_court_displaysetting.online = true; displays[displays.length] = client_court_displaysetting; } diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 0f5ad9b..ffbf2c9 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -123,6 +123,7 @@ var ci18n_de = { 'tournament:edit:ticker_url': 'Ticker-Adresse:', 'tournament:edit:ticker_password': 'Ticker-Passwort:', 'tournament:edit:displays': 'Anzeigen administrieren:', + 'tournament:edit:displays:hostname': 'Hostname', 'tournament:edit:displays:num': 'Monitor', 'tournament:edit:displays:court': 'Feld', 'tournament:edit:displays:setting': 'Einstellung', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 2ce7432..42c0fa4 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -124,6 +124,7 @@ var ci18n_en = { 'tournament:edit:normalizations:replace': 'Replacement', 'tournament:edit:normalizations:language': 'Language', 'tournament:edit:displays': 'Manage Displays:', + 'tournament:edit:displays:hostname': 'Hostname', 'tournament:edit:displays:num': 'Displaynumber', 'tournament:edit:displays:court': 'Ar Court', 'tournament:edit:displays:setting': 'Setting', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 90629aa..e49831c 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -1051,6 +1051,7 @@ var ctournament = (function() { const display_tbody = uiu.el(display_table, 'tbody'); const tr = uiu.el(display_tbody, 'tr'); uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:num')); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:hostname')); uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:court')); uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:setting')); uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:onlinestatus')); @@ -1058,6 +1059,7 @@ var ctournament = (function() { for (const c of curt.displays) { const tr = uiu.el(display_tbody, 'tr'); uiu.el(tr, 'th', {}, c.client_id); + uiu.el(tr, 'th', {}, c.hostname); createCourtSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.court_id); createDisplaySettingsSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.displaysetting_id); uiu.el(tr, 'td', {}, (!c.online) ? 'offline' : 'online'); From 262d44406cb3f53bbd1ac756d234bcd3d86bf630 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 12 Oct 2024 11:21:14 +0200 Subject: [PATCH 136/224] #67: Announcement: Inserting a configurable pause between two announcements --- bts/admin.js | 3 ++- static/js/announcements.js | 19 +++++++++++++++---- static/js/change.js | 3 ++- static/js/ci18n_de.js | 3 ++- static/js/ci18n_en.js | 3 ++- static/js/ctournament.js | 6 ++++-- 6 files changed, 27 insertions(+), 10 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index f5296be..4a41267 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -59,7 +59,8 @@ function handle_tournament_edit_props(app, ws, msg) { 'warmup', 'warmup_ready', 'warmup_start', 'ticker_enabled', 'ticker_url', 'ticker_password', 'language', 'dm_style', 'displaysettings_general', - 'tabletoperator_enabled', 'tabletoperator_break_seconds','announcement_speed', + 'tabletoperator_enabled', 'tabletoperator_break_seconds', + 'announcement_speed','announcement_pause_time_ms', 'tabletoperator_set_break_after_tabletservice','tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', diff --git a/static/js/announcements.js b/static/js/announcements.js index 2d16f6d..e791319 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -285,16 +285,27 @@ function announce(callArray) { break; } } - callArray.forEach(function (part) { - if (part && part != null) { + for (var i = 0; i < callArray.length; i++) { + const part = callArray[i]; + if (part && part != null) { var words = new SpeechSynthesisUtterance(part); words.lang = ci18n('announcements:lang'); - words.rate = curt.announcement_speed ? curt.announcement_speed: 1.05; + words.rate = curt.announcement_speed ? curt.announcement_speed : 1.05; words.pitch = 0; words.volume = 1; words.voice = voice; + if (i == 0) { + if (window.speechSynthesis.speaking) { + words.onstart = function () { + window.speechSynthesis.pause(); + setTimeout(() => { + window.speechSynthesis.resume(); + }, curt.announcement_pause_time_ms ? curt.announcement_pause_time_ms * 1000 : 2000); + } + } + } window.speechSynthesis.speak(words); } - }); + } }); } \ No newline at end of file diff --git a/static/js/change.js b/static/js/change.js index bfc7af6..9a96849 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -61,7 +61,8 @@ function default_handler(rerender, special_funcs) { 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', - 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds','announcement_speed', + 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds', + 'announcement_speed','announcement_pause_time_ms', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', 'annoncement_include_event', 'annoncement_include_round', 'annoncement_include_matchnumber', 'call_preparation_matches_automatically_enabled','call_next_possible_scheduled_match_in_preparation', diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index ffbf2c9..c378f18 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -116,7 +116,8 @@ var ci18n_de = { 'tournament:edit:normalizations:origin': 'Original', 'tournament:edit:normalizations:replace': 'Ersetzung', 'tournament:edit:normalizations:language': 'Sprache', - 'tournament:edit:announcement_speed': 'Ansagegeschwindigkeit (0.8-1.3)', + 'tournament:edit:announcement_speed': 'Ansagegeschwindigkeit (0.8-1.3): ', + 'tournament:edit:announcement_pause_time_ms': 'Pause zwischen Ansagen (sek): ', 'tournament:edit:preparation_meetingpoint_enabled': 'Meetingpoint für Vorbereitung nutzen', 'tournament:edit:preparation_tabletoperator_setup_enabled': 'Tabletbediener in Vorbereitung aufrufen', 'tournament:edit:ticker_enabled': 'Ticker aktivieren', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 42c0fa4..b6abfa5 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -116,7 +116,8 @@ var ci18n_en = { 'tournament:edit:annoncement_include_event': 'Announce event', 'tournament:edit:annoncement_include_round': 'Announce round of tournament', 'tournament:edit:annoncement_include_matchnumber': 'Announce number of match', - 'tournament:edit:announcement_speed': 'Announcementspeed (0.8-1.3)', + 'tournament:edit:announcement_speed': 'Announcementspeed (0.8-1.3): ', + 'tournament:edit:announcement_pause_time_ms': 'Pause between announcements (sec): ', 'tournament:edit:preparation_meetingpoint_enabled': 'Use Meetingpoint for preparation', 'tournament:edit:preparation_tabletoperator_setup_enabled': 'Call up tablet operator in preparation', 'tournament:edit:normalizations': 'Pronunciation optimization', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index e49831c..e505865 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -857,8 +857,9 @@ var ctournament = (function() { curt.tabletoperator_break_seconds = 300; } create_input(curt, "number", tablet_fieldset, 'tabletoperator_break_seconds') - create_numeric_input(curt, tablet_fieldset, 'announcement_speed', 0.8, 1.3, 1.05,0.01); - + create_numeric_input(curt, tablet_fieldset, 'announcement_speed', 0.8, 1.3, 1.05, 0.01); + create_numeric_input(curt, tablet_fieldset, 'announcement_pause_time_ms', 0.0, 5.0, 2.0, 0.1); + uiu.el(form, 'button', { @@ -897,6 +898,7 @@ var ctournament = (function() { tabletoperator_enabled: (!!data.tabletoperator_enabled), tabletoperator_break_seconds: data.tabletoperator_break_seconds, announcement_speed: data.announcement_speed, + announcement_pause_time_ms: data.announcement_pause_time_ms, preparation_meetingpoint_enabled: (!!data.preparation_meetingpoint_enabled), preparation_tabletoperator_setup_enabled: (!!data.preparation_tabletoperator_setup_enabled), call_preparation_matches_automatically_enabled: (!!data.call_preparation_matches_automatically_enabled), From 0edaf0c40c703974e3c25b6c7de688974139c806 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 12 Oct 2024 11:53:35 +0200 Subject: [PATCH 137/224] #64: Server.Core: Call Matchen in preparation after call a new match on court --- bts/match_utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bts/match_utils.js b/bts/match_utils.js index d684c4d..c7cdc17 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -827,7 +827,7 @@ async function call_next_possible_match_for_preparation(app, tournament_key, cal } if (tournament.call_next_possible_scheduled_match_in_preparation) { const match_querry = { 'tournament_key': tournament_key, 'setup.state': 'scheduled' }; - app.db.matches.find(match_querry).sort({ 'setup.scheduled_date': 1, 'setup.scheduled_time_str': 1, 'setup.match_number': 1 }).exec((err, matches) => { + app.db.matches.find(match_querry).sort({ 'setup.scheduled_date': 1, 'setup.scheduled_time_str': 1, 'match_order': 1 }).exec((err, matches) => { if (err) { return callback(msg, err); } From c9da8cd982442c0cbeab7d481f9e29ae5cb064e0 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 12 Oct 2024 12:08:45 +0200 Subject: [PATCH 138/224] #66: BUPWS / Settings GUI: Show hostname of displays --- bts/bupws.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index 9ccf77b..519eec2 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -178,17 +178,15 @@ async function determine_client_hostname(ws) { resolve(ws.hostname); return; } - dns.reverse(ip_v4, (err, hostnames) => { - if(err) { - reject(err); + if (err) { + resolve("N/N") } - if(hostnames.length >= 1) { + if (hostnames && hostnames.length >= 1) { ws.hostname = hostnames[0].split('.')[0]; } resolve(ws.hostname); }); - } else { resolve(ws.hostname); From 7253d793dc812d9b1188e718375aefd3153f4cff Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 13 Oct 2024 16:52:44 +0200 Subject: [PATCH 139/224] #52: Server.Core: Operate server under HTTPS --- bts/bts.js | 44 +++++++++++++++++++++++++++++++++++--------- config.json.default | 6 +++++- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/bts/bts.js b/bts/bts.js index e197362..17ad564 100644 --- a/bts/bts.js +++ b/bts/bts.js @@ -80,14 +80,10 @@ function cadmin_router() { } function create_app(config, db) { - const server = require('http').createServer(); + const app = express(); - const wss = new ws_module.Server({server: server}); - app.config = config; app.db = db; - app.wss = wss; - app.use('/bup/', express.static(config.bup_location, {index: config.bup_index})); app.use('/bupdev/', express.static(path.join(utils.root_dir(), 'static/bup/dev/'))); app.use('/static/', express.static('static/', {})); @@ -106,6 +102,19 @@ function create_app(config, db) { app.get('/h/:tournament_key/m/:match_id/info', http_api.matchinfo_handler); app.get('/h/:tournament_key/logo/:logo_id', http_api.logo_handler); + var server = null; + if (config.enable_https) { + const options = { + key: fs.readFileSync(config.https_key), + cert: fs.readFileSync(config.https_cert) + } + server = require('https').createServer(options, app); + } else { + server = require('http').createServer(app); + } + + const wss = new ws_module.Server({ server: server }); + app.wss = wss; wss.on('connection', function connection(ws, req) { const location = url.parse(req.url, true); if (location.path === '/ws/admin') { @@ -121,10 +130,27 @@ function create_app(config, db) { } }); - server.on('request', app); - server.listen(config.port, function () { - // console.log('Listening on ' + server.address().port); - }); + if (config.enable_https) { + server.listen(config.https_port, function () { + console.log("HTTPS server listening on port " + config.https_port); + }); + const httpApp = express(); + httpApp.get("*", function (req, res, next) { + var host = req.headers.host.split(":")[0] + if (config.https_port != 443) { + host = host + ":" + config.https_port + } + res.redirect("https://" + host + req.path); + }); + + require('http').createServer(httpApp).listen(config.port, function () { + console.log("HTTP server listening on port " + config.port+" ==> permanently redirected to https."); + }); + } else { + server.listen(config.port, function () { + console.log("HTTP server listening on port " + config.port); + }); + } return app; } diff --git a/config.json.default b/config.json.default index ab82eb5..0761365 100644 --- a/config.json.default +++ b/config.json.default @@ -2,5 +2,9 @@ "port": 4000, "bup_location": "static/bup/downloaded/", "bup_index": "index.html", - "report_errors": true + "report_errors": false, + "enable_https": false, + "https_port": 4433, + "https_key": "${PATH_TO}/key.pem", + "https_cert": "${PATH_TO}/cert.pem" } \ No newline at end of file From e2cff0e19f25ae23ddc7e1f58860f4c975338c43 Mon Sep 17 00:00:00 2001 From: tengmel Date: Mon, 14 Oct 2024 20:17:30 +0200 Subject: [PATCH 140/224] #51 Admin: Edit tournament: Display battery charge levels for the tablets --- bts/bupws.js | 61 +++++++++++++++++++++-- bts/http_api.js | 14 ++++++ bts/stournament.js | 7 +-- static/css/admin.css | 26 +++++++++- static/icons/charging.svg | 101 ++++++++++++++++++++++++++++++++++++++ static/js/change.js | 1 + static/js/ci18n_de.js | 3 ++ static/js/ci18n_en.js | 3 ++ static/js/ctournament.js | 38 ++++++++++++-- 9 files changed, 241 insertions(+), 13 deletions(-) create mode 100644 static/icons/charging.svg diff --git a/bts/bupws.js b/bts/bupws.js index 519eec2..bb041e9 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -112,6 +112,45 @@ async function handle_persist_display_settings(app, ws, msg) { } } +async function handle_device_info(app, ws, msg) { + const tournament_key = msg.tournament_key; + const device_info = msg.device; + if (device_info) { + device_info.client_ip = ws._socket.remoteAddress; + update_device_info(app, tournament_key, device_info); + } +} +async function update_device_info(app, tournament_key, device_info) { + app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { + if (!err || !tournament) { + err = { message: 'No tournament ' + default_tournament_key }; + } + const client_id = determine_client_id_from_ip(device_info.client_ip); + const panel = fetch_panel(client_id); + if (panel != null) { + const hostname = "N/N"; + var display_court_displaysetting = await get_display_court_displaysettings(app, client_id); + if (display_court_displaysetting == null) { + display_court_displaysetting = create_display_court_displaysettings(client_id, hostname, null, generate_default_displaysettings_id(tournament)); + } + panel.battery = device_info.battery + display_court_displaysetting.battery = device_info.battery + display_court_displaysetting.online = true; + admin.notify_change(app, default_tournament_key, 'display_status_changed', { 'display_court_displaysetting': display_court_displaysetting }); + } + }); +} + +function fetch_panel(client_id) { + for (const panel_ws of all_panels) { + if (client_id == panel_ws.client_id) { + return panel_ws; + } + } + return null; +} + + function create_display_court_displaysettings(client_id, hostname, court_id, displaysetting_id) { return { client_id: client_id, @@ -196,13 +235,20 @@ async function determine_client_hostname(ws) { function determine_client_id(ws) { if (!ws.client_id) { - const remote_adress_seqments = ws._socket.remoteAddress.split('.'); - ws.client_id = remote_adress_seqments[remote_adress_seqments.length - 1]; - + ws.client_id = determine_client_id_from_ip (ws._socket.remoteAddress); } return ws.client_id; } +function determine_client_id_from_ip(ip_adress) { + if (ip_adress) { + const remote_adress_seqments = ip_adress.split('.'); + return remote_adress_seqments[remote_adress_seqments.length - 1]; + } else { + return "UNDEFINED"; + } +} + function persist_client_court_displaysetting(app, client_court_displaysetting) { return new Promise((resolve, reject) => { app.db.display_court_displaysettings.insert(client_court_displaysetting, function (err, inserted_t) { @@ -560,13 +606,14 @@ function reinitialize_panel(app, tournament_key, client_id, new_court_id, displa return false;; } -async function add_display_status(app, tournament, displays) { +async function add_display_status(app, tournament, displays, callback) { for (const d of displays) { d.online = false; for (const panel_ws of all_panels) { const ws_client_id = determine_client_id(panel_ws); if (d.client_id == ws_client_id) { d.online = true; + d.battery = panel_ws.battery; } } } @@ -575,7 +622,6 @@ async function add_display_status(app, tournament, displays) { const ws_client_id = determine_client_id(panel_ws); const ws_hostname = await determine_client_hostname(panel_ws); for (const d of displays) { - if (d.client_id == ws_client_id) { found = true; } @@ -583,9 +629,12 @@ async function add_display_status(app, tournament, displays) { if (!found) { const client_court_displaysetting = create_display_court_displaysettings(ws_client_id, ws_hostname, panel_ws.court_id, generate_default_displaysettings_id(tournament)); client_court_displaysetting.online = true; + client_court_displaysetting.battery = panel_ws.battery; displays[displays.length] = client_court_displaysetting; + } } + return callback(displays); } module.exports = { @@ -596,6 +645,8 @@ module.exports = { handle_score_change, handle_persist_display_settings, handle_reset_display_settings, + handle_device_info, + update_device_info, restart_panel, change_display_mode, change_default_display_mode, diff --git a/bts/http_api.js b/bts/http_api.js index 2acb911..953e1fc 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -269,6 +269,13 @@ async function score_handler(req, res) { 'setup.now_on_court': true, }; + const device_info = req.body.device; + if (device_info) { + const client_ip = req.socket.remoteAddress + device_info.client_ip = client_ip; + } + + const finish_confirmed = req.body.finish_confirmed ? req.body.finish_confirmed : false; if (finish_confirmed) { update.team1_won = req.body.team1_won, @@ -370,6 +377,13 @@ async function score_handler(req, res) { } return cb(null, match, changed_court); }, + (match, changed_court, cb) => { + if (!device_info) { + return cb(null, match, changed_court); + } + bupws.update_device_info(req.app, tournament_key, device_info); + return cb(null, match, changed_court); + }, ], function (err) { if (err) { res.json({ diff --git a/bts/stournament.js b/bts/stournament.js index 3af1a45..0fa4869 100644 --- a/bts/stournament.js +++ b/bts/stournament.js @@ -48,9 +48,10 @@ function get_displays(app, tournament, callback) { }); const bupws = require('./bupws'); - bupws.add_display_status(app, tournament, display_court_displaysettings); - display_court_displaysettings = display_court_displaysettings.sort(utils.cmp_key('client_id')); - return callback(err, display_court_displaysettings); + bupws.add_display_status(app, tournament, display_court_displaysettings, function (display_court_displaysettings) { + display_court_displaysettings = display_court_displaysettings.sort(utils.cmp_key('client_id')); + return callback(err, display_court_displaysettings); + }); }); } diff --git a/static/css/admin.css b/static/css/admin.css index 7994d41..4653334 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -1,5 +1,27 @@ -html, -body { +td.battery-status-green { + background: linear-gradient(90deg, hsl(92, 89%, 46%) 15%, hsl(92, 90%, 68%) 100%); +} +td.battery-status-yellow { + background: linear-gradient(90deg, hsl(54, 89%, 46%) 15%, hsl(92, 90%, 45%) 100%); +} +td.battery-status-orange { + background: linear-gradient(90deg, hsl(22, 89%, 46%) 15%, hsl(54, 90%, 45%) 100%); + color: white; +} +td.battery-status-red { + background: linear-gradient(90deg, hsl(7, 89%, 46%) 15%, hsl(11, 93%, 68%) 100%); + color: white; +} + +td.battery-status-charging { + background-image: url(../icons/charging.svg); + background-repeat: no-repeat; + background-position: center center; + background-color: forestgreen; + color: white; + font-weight: bold; +} +html, body { margin: 0; padding: 0; font-size: 20px; diff --git a/static/icons/charging.svg b/static/icons/charging.svg new file mode 100644 index 0000000..15ced64 --- /dev/null +++ b/static/icons/charging.svg @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/static/js/change.js b/static/js/change.js index 9a96849..3c42fda 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -208,6 +208,7 @@ function default_handler(rerender, special_funcs) { d.court_id = display_setting.court_id; d.displaysetting_id = display_setting.displaysetting_id; d.online = display_setting.online; + d.battery = display_setting.battery; } if (laststatus != d.online) { cerror.silent('Display ' + display_setting.client_id + ' is ' + (display_setting.online ? 'online' : 'offline')); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index c378f18..a413e9f 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -125,6 +125,9 @@ var ci18n_de = { 'tournament:edit:ticker_password': 'Ticker-Passwort:', 'tournament:edit:displays': 'Anzeigen administrieren:', 'tournament:edit:displays:hostname': 'Hostname', + 'tournament:edit:displays:batterylevel': 'Akku', + 'tournament:edit:displays:battery_charging_time': 'Lädt: {battery_charging_time} Minuten Ladzeit.', + 'tournament:edit:displays:battery_duscharging_time': '{battery_discharging_time} Minuten Restlaufzeit.', 'tournament:edit:displays:num': 'Monitor', 'tournament:edit:displays:court': 'Feld', 'tournament:edit:displays:setting': 'Einstellung', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index b6abfa5..66c87f6 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -126,6 +126,9 @@ var ci18n_en = { 'tournament:edit:normalizations:language': 'Language', 'tournament:edit:displays': 'Manage Displays:', 'tournament:edit:displays:hostname': 'Hostname', + 'tournament:edit:displays:batterylevel': 'Battery', + 'tournament:edit:displays:battery_charging_time': 'Charging: {battery_charging_time} minutes remaining.', + 'tournament:edit:displays:battery_duscharging_time': '{battery_discharging_time} minutes left.', 'tournament:edit:displays:num': 'Displaynumber', 'tournament:edit:displays:court': 'Ar Court', 'tournament:edit:displays:setting': 'Setting', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index e505865..5974d30 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -1045,6 +1045,34 @@ var ctournament = (function() { } } + function set_battery_state(battery, node) { + if (battery && battery != null) { + node.removeAttribute("class"); + let level = Math.floor(battery.level * 100); + node.innerHTML = level + '%'; + if (battery.charging) { + node.classList.add('battery-status-charging'); + + node.title = ci18n('tournament:edit:displays:battery_charging_time', { + battery_charging_time : Math.floor(battery.chargingTime / 60) + }); + } else { + node.title = ci18n('tournament:edit:displays:battery_duscharging_time', { + battery_discharging_time: Math.floor(battery.dischargingTime / 60) + }); + + if (level <= 10) { + node.classList.add('battery-status-red'); + } else if (level <= 20) { + node.classList.add('battery-status-orange'); + } else if (level <= 40) { + node.classList.add('battery-status-yellow'); + } else { + node.classList.add('battery-status-green'); + } + } + } + } function render_displaysettings(main) { uiu.el(main, 'h2', {}, ci18n('tournament:edit:displays')); @@ -1054,14 +1082,18 @@ var ctournament = (function() { const tr = uiu.el(display_tbody, 'tr'); uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:num')); uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:hostname')); - uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:court')); - uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:setting')); - uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:onlinestatus')); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:batterylevel')); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:court')); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:setting')); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:onlinestatus')); + for (const c of curt.displays) { const tr = uiu.el(display_tbody, 'tr'); uiu.el(tr, 'th', {}, c.client_id); uiu.el(tr, 'th', {}, c.hostname); + var battery_node = uiu.el(tr, 'td', {}, 'N/N"'); + set_battery_state(c.battery, battery_node); createCourtSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.court_id); createDisplaySettingsSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.displaysetting_id); uiu.el(tr, 'td', {}, (!c.online) ? 'offline' : 'online'); From c5ce3d501c45347744814a5bb34517a3015ff500 Mon Sep 17 00:00:00 2001 From: tengmel Date: Wed, 16 Oct 2024 09:06:18 +0200 Subject: [PATCH 141/224] [49] Ticker: Revision of the display of all matches --- bts/utils.js | 8 ++++++ static/ticker/courts.mustache | 54 +++++++++++++++++++---------------- static/ticker/root.html | 43 +++++++++++++++------------- static/ticker/ticker.css | 17 +++++++---- ticker/tdata.js | 2 +- 5 files changed, 73 insertions(+), 51 deletions(-) diff --git a/bts/utils.js b/bts/utils.js index e05318e..7c86f14 100644 --- a/bts/utils.js +++ b/bts/utils.js @@ -202,6 +202,13 @@ function format_ts(ts) { ); } +function format_time_ts(ts) { + var d = new Date(ts); + return ( + pad(d.getHours(), 2) + ':' + pad(d.getMinutes(), 2) + ); +} + function has_key(obj, testfunc) { for (const k in obj) { if (testfunc(k)) return true; @@ -225,6 +232,7 @@ module.exports = { deep_equal, filter_map, format_ts, + format_time_ts, encode_html, gen_token, get_system_timezone, diff --git a/static/ticker/courts.mustache b/static/ticker/courts.mustache index c6ceb9f..f03fd07 100644 --- a/static/ticker/courts.mustache +++ b/static/ticker/courts.mustache @@ -1,25 +1,31 @@ -{{#courts_with_matches}} - - -{{^match}} - -{{/match}} - -{{#match}} - -{{#team0scores}} - -{{/team0scores}} - -{{/match}} - - - -{{#match}} - -{{#team1scores}} - -{{/team1scores}} -{{/match}} -
    {{num}}{{p0str}}{{.}}
    {{n}}
    {{p1str}}{{.}}
    +{{#courts_with_matches}} + + + + + + + + {{^match}} + + {{/match}} + {{#match}} + + {{#team0scores}} + + {{/team0scores}} + {{/match}} + + + {{#match}} + + {{#team1scores}} + + {{/team1scores}} + {{/match}} + + +
    + # {{#match}}{{n}}{{/match}} +
    {{num}}{{p0str}}{{.}}
    {{p1str}}{{.}}
    {{/courts_with_matches}} \ No newline at end of file diff --git a/static/ticker/root.html b/static/ticker/root.html index c45805a..6794513 100644 --- a/static/ticker/root.html +++ b/static/ticker/root.html @@ -1,29 +1,32 @@ - - - - -{{tournament_name_html}} - - + + + + + {{tournament_name_html}} + + -
    +
    -
    -

    {{tournament_name_html}}

    Last update: {{last_update_str}}
    -
    +
    +

    {{tournament_name_html}}

    +
    Stand: {{last_update_str}}
    +
    -
    {{prefix_html}}
    +
    {{prefix_html}}
    -
    {{courts_html}}
    +
    {{courts_html}}
    -
    {{note_html}}
    -
    -
    +
    + {{note_html}} +
    +
    +
    - - - - + + + + \ No newline at end of file diff --git a/static/ticker/ticker.css b/static/ticker/ticker.css index 79c38cc..0bda4c8 100644 --- a/static/ticker/ticker.css +++ b/static/ticker/ticker.css @@ -1,7 +1,6 @@ html, body { margin: 0; padding: 0; - font-size: 20px; font-family: sans-serif; } .main { @@ -19,15 +18,18 @@ h1 { padding: 0; font-size: inherit; font-weight: bold; - font-size: 150%; + font-size: 120%; } .last_update { position: absolute; - right: 0; - top: 0; + right: 10px; + top: 8px; + font-size: 10px; + font-weight: bold; } .note { margin-top: 1em; + font-size: 12px; } table.court { @@ -56,6 +58,8 @@ table.court + table.court { text-align: right; padding: 0 0.5em 0 0.1em; width: 1em; + font-size: 20px; + font-weight: bold; } .court_players { padding-left: 0.6em; @@ -63,14 +67,15 @@ table.court + table.court { } .court_score { width: 1.3em; - font-size: 40px; + font-size: 18px; border-left: 0.1vmin solid #ddd; text-align: center; } .court_event, .court_event_div { width: 2em; max-width: 2em; - font-size: 20px; + font-size: 12px; + font-weight: bold; } .court_event>div { width: 100%; diff --git a/ticker/tdata.js b/ticker/tdata.js index 4eadf93..a1c5739 100644 --- a/ticker/tdata.js +++ b/ticker/tdata.js @@ -56,7 +56,7 @@ function recalc(app, cb) { courts_with_matches: courts, }; if (tournament && tournament.last_update) { - td.last_update_str = utils.format_ts(tournament.last_update); + td.last_update_str = utils.format_time_ts(tournament.last_update); } app.ticker_data = td; From 221dc77ade40385cddf7b34cb097ef0ee171a714 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 20 Oct 2024 19:43:12 +0200 Subject: [PATCH 142/224] [#68] Server.Core: Conversion of communication between tablets and BTS to WebSocket --- bts/admin.js | 13 +- bts/btp_sync.js | 4 +- bts/bts.js | 6 +- bts/bupws.js | 169 +++++++++++++++++- bts/http_api.js | 416 +-------------------------------------------- bts/match_utils.js | 31 +++- 6 files changed, 201 insertions(+), 438 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 4a41267..fb185ec 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -11,7 +11,6 @@ const serror = require('./serror'); const stournament = require('./stournament'); const ticker_manager = require('./ticker_manager'); const utils = require('./utils'); -const btp_sync = require('./btp_sync'); /** @@ -42,6 +41,17 @@ function handle_tournament_list(app, ws, msg) { }); } +function handle_confirm_match_finished(app, ws, msg) { + if (!msg.tournament_key) { + return ws.respond(msg, { message: 'Missing tournament' }); + } + if (!msg.court_id) { + return ws.respond(msg, { message: 'Missing court' }); + } + const bupws = require('./bupws'); + bupws.send_finshed_confirmed(app, msg.tournament_key, msg.court_id); +} + function handle_tournament_edit_props(app, ws, msg) { if (! msg.key) { return ws.respond(msg, {message: 'Missing key'}); @@ -895,6 +905,7 @@ module.exports = { handle_begin_to_play_call, handle_announce_match_manually, handle_btp_fetch, + handle_confirm_match_finished, handle_normalization_add, handle_normalization_remove, handle_tabletoperator_add, diff --git a/bts/btp_sync.js b/bts/btp_sync.js index d83c6da..a891c40 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -566,8 +566,8 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { return callback(err); } if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { - const http_api = require('./http_api'); - http_api.reset_player_tabletoperator(app, tkey, match._id, match.end_ts); + const match_utils = require('./match_utils'); + match_utils.reset_player_tabletoperator(app, tkey, match._id, match.end_ts); } }); } diff --git a/bts/bts.js b/bts/bts.js index 17ad564..a28c72f 100644 --- a/bts/bts.js +++ b/bts/bts.js @@ -96,11 +96,7 @@ function create_app(config, db) { app.use('/u(:courtnum)?', shortcuts.umpire_handler); app.use(body_parser.json()); - app.get('/h/:tournament_key/courts', http_api.courts_handler); - app.get('/h/:tournament_key/matches', http_api.matches_handler); - app.post('/h/:tournament_key/m/:match_id/score', http_api.score_handler); - app.get('/h/:tournament_key/m/:match_id/info', http_api.matchinfo_handler); - app.get('/h/:tournament_key/logo/:logo_id', http_api.logo_handler); + app.get('/h/:tournament_key/C:logo_id', http_api.logo_handler); var server = null; if (config.enable_https) { diff --git a/bts/bupws.js b/bts/bupws.js index bb041e9..7e277b4 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -1,6 +1,5 @@ 'use strict'; - -const { forEach } = require('async'); +const async = require('async'); const serror = require('./serror'); const utils = require('./utils'); const admin = require('./admin'); @@ -8,6 +7,9 @@ const dns = require('dns'); const cp = require('child_process'); const os = require('os'); +const btp_manager = require('./btp_manager'); +const ticker_manager = require('./ticker_manager'); +const stournament = require('./stournament'); const all_panels = []; const default_tournament_key = 'default'; @@ -64,6 +66,18 @@ function notify_change_ws(ws, tournament_key, court_id, ctype, val) { } } } +function send_courts(app, ws, tournament_key) { + stournament.get_courts(app.db, tournament_key, function (err, courts) { + notify_change_ws(ws,tournament_key, ws.court_id, "courts-update", courts); + }); +} +function send_error(ws, tournament_key, msg) { + ws.sendmsg({ + type: 'error', + tournament_key, + msg + }); +} function all_matches_delivery() { for (const panel_ws of all_panels) { @@ -74,10 +88,6 @@ function all_matches_delivery() { } async function handle_reset_display_settings(app, ws, msg) { - const tournament_key = msg.tournament_key; - const court_id = msg.panel_settings.court_id; - var setting = msg.panel_settings; - const client_id = determine_client_id(ws); var client_court_displaysetting = await get_display_court_displaysettings(app, client_id); if (client_court_displaysetting != null) { @@ -111,7 +121,147 @@ async function handle_persist_display_settings(app, ws, msg) { client_court_displaysetting = await update_client_court_displaysetting(app, client_court_displaysetting.client_id, updatevalues); } } +async function handle_score_update(app, ws, msg) { + const match_utils = require('./match_utils'); + const tournament_key = msg.tournament_key; + const score_data = msg.score; + const match_id = score_data.match_id; + + var match = await match_utils.fetch_match(app, tournament_key, match_id); + if (match == null || match.setup.now_on_court == false) { + send_error(ws, tournament_key, "Match not found or not on court actualy."); + return; + } + const update = { + network_score: score_data.network_score, + network_team1_left:score_data.network_team1_left, + network_team1_serving:score_data.network_team1_serving, + network_teams_player1_even:score_data.network_teams_player1_even, + presses:score_data.presses, + duration_ms:score_data.duration_ms, + end_ts:score_data.end_ts, + 'setup.now_on_court': true, + }; + + const device_info = score_data.device; + if (device_info) { + const client_ip = ws._socket.remoteAddress; + device_info.client_ip = client_ip; + } + + const finish_confirmed = score_data.finish_confirmed ? score_data.finish_confirmed : false; + if (finish_confirmed) { + update.team1_won = score_data.team1_won, + update.btp_winner = (update.team1_won === true) ? 1 : 2; + update.btp_needsync = true; + update["setup.now_on_court"] = false; + match_utils.reset_player_tabletoperator(app, tournament_key, match_id, update.end_ts); + } + if (score_data.shuttle_count) { + update.shuttle_count = score_data.shuttle_count; + } + const match_query = { + _id: match_id, + tournament_key, + }; + + const court_q = { + tournament_key, + _id: score_data.court_id, + }; + const db = app.db; + async.waterfall([ + cb => db.matches.update(match_query, { $set: update }, { returnUpdatedDocs: true }, (err, _, match) => cb(err, match)), + (match, cb) => { + if (match) { + handle_score_change(app, tournament_key, match.setup.court_id); + admin.notify_change(app, tournament_key, 'score', { + match_id, + network_score: update.network_score, + team1_won: update.team1_won, + shuttle_count: update.shuttle_count, + presses: match.presses, + }); + } + cb(null, match); + }, + (match, cb) => db.courts.findOne(court_q, (err, court) => cb(err, match, court)), + (match, court, cb) => { + if (!match) { + if (court.match_id === match_id) { + cb(null, match, court, false); + return; + } + + db.courts.update(court_q, { $set: { match_id: match_id } }, {}, (err) => { + cb(err, match, court, true); + }); + } + cb(null, match, court, true); + }, + (match, court, changed_court, cb) => { + if (match && changed_court) { + admin.notify_change(app, tournament_key, 'court_current_match', { + match__id: match_id, + match: match, + }); + } + cb(null, match, changed_court); + }, + (match, changed_court, cb) => { + if (match) { + btp_manager.update_score(app, match); + } + cb(null, match, changed_court); + }, + (match, changed_court, cb) => { + if (match && match.setup.highlight && + match.setup.highlight == 6 && + match.network_score && + match.network_score.length > 0 && + match.network_score[0].length > 1 && + (match.network_score[0][0] > 0 || match.network_score[0][1] > 0)) { + match.setup.highlight = 0; + btp_manager.update_highlight(app, match); + } + cb(null, match, changed_court); + }, + (match, changed_court, cb) => { + if (changed_court) { + ticker_manager.pushall(app, tournament_key); + } else { + if (match) { + ticker_manager.update_score(app, match); + } + } + cb(null, match, changed_court); + }, + (match, changed_court, cb) => { + if (!match) { + return cb(new Error('Cannot find match ' + JSON.stringify(match))); + } + if (finish_confirmed && match.team1_won != undefined && match.team1_won != null) { + match_utils.call_preparation_match_on_court(app, tournament_key, match.setup.court_id, (err) => { + + }); + } + return cb(null, match, changed_court); + }, + (match, changed_court, cb) => { + if (!device_info) { + return cb(null, match, changed_court); + } + update_device_info(app, tournament_key, device_info); + return cb(null, match, changed_court); + }, + ], function (err) { + if (err) { + send_error(ws, tournament_key, err.message); + return; + } + }); +} async function handle_device_info(app, ws, msg) { const tournament_key = msg.tournament_key; const device_info = msg.device; @@ -174,6 +324,11 @@ async function handle_init(app, ws, msg) { } else { matches_handler(app, ws, tournament_key, ws.court_id); } + send_courts(app, ws, tournament_key); +} + +async function send_finshed_confirmed(app, tournament_key, court_id) { + notify_change(tournament_key, court_id, 'confirm-match-finished', {}); } async function initialize_client(ws, app, tournament_key, court_id, displaysetting) { @@ -645,9 +800,11 @@ module.exports = { handle_score_change, handle_persist_display_settings, handle_reset_display_settings, + handle_score_update, handle_device_info, update_device_info, restart_panel, + send_finshed_confirmed, change_display_mode, change_default_display_mode, add_display_status, diff --git a/bts/http_api.js b/bts/http_api.js index 953e1fc..d5183f2 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -1,418 +1,9 @@ 'use strict'; const assert = require('assert'); -const async = require('async'); const path = require('path'); - -const admin = require('./admin'); -const bupws = require('./bupws'); -const btp_manager = require('./btp_manager'); -const stournament = require('./stournament'); -const ticker_manager = require('./ticker_manager'); const utils = require('./utils'); -// Returns true iff all params are met -function _require_params(req, res, keys) { - for (const k of keys) { - if (! Object.prototype.hasOwnProperty.call(req.body, k)) { - res.json({ - status: 'error', - message: 'Missing field ' + k + ' in request', - }); - return false; - } - } - return true; -} - -function courts_handler(req, res) { - const tournament_key = req.params.tournament_key; - stournament.get_courts(req.app.db, tournament_key, function(err, courts) { - const reply = (err ? { - status: 'error', - message: err.message, - } : { - status: 'ok', - courts, - }); - - res.json(reply); - }); -} - -function create_match_representation(tournament, match) { - const setup = match.setup; - setup.match_id = 'bts_' + match._id; - setup.team_competition = tournament.is_team; - setup.nation_competition = tournament.is_nation_competition; - for (const t of setup.teams) { - if (!t.players) continue; - - for (const p of t.players) { - if (p.lastname) continue; - - const asian_m = /^([A-Z]+)\s+(.*)$/.exec(p.name); - if (asian_m) { - p.lastname = asian_m[1]; - p.firstname = asian_m[2]; - p._guess_info = 'bts_asian'; - continue; - } - - const m = /^(.*)\s+(\S+)$/.exec(p.name); - if (m) { - p.firstname = m[1]; - p.lastname = m[2]; - p._guess_info = 'bts_western'; - } else { - p.firstname = ''; - p.lastname = p.name; - p._guess_info = 'bts_single'; - } - } - } - - const res = { - setup, - network_score: match.network_score, - network_team1_left: match.network_team1_left, - network_team1_serving: match.network_team1_serving, - network_teams_player1_even: match.network_teams_player1_even, - }; - if (match.presses) { - res.presses_json = JSON.stringify(match.presses); - } - return res; -} - -function create_event_representation(tournament) { - const res = { - id: 'bts_' + tournament.key, - tournament_name: tournament.name, - }; - if (tournament.logo_id) { - res.tournament_logo_url = `/h/${encodeURIComponent(tournament.key)}/logo/${tournament.logo_id}`; - } - res.tournament_logo_background_color = tournament.logo_background_color || '#000000'; - res.tournament_logo_foreground_color = tournament.logo_foreground_color || '#aaaaaa'; - return res; -} - -// TODO might be removed due to refactoring work in bupws.js -function matches_handler(req, res) { - const tournament_key = req.params.tournament_key; - const now = Date.now(); - const show_still = now - 60000; - const query = { - tournament_key, - $or: [ - { - $and: [ - { - team1_won: { - $ne: true, - }, - }, - { - team1_won: { - $ne: false, - }, - }, - ], - }, - { - end_ts: { - $gt: show_still, - }, - }, - ], - }; - if (req.query.court) { - query['setup.court_id'] = req.query.court; - } else { - query['setup.court_id'] = {$exists: true}; - } - - req.app.db.fetch_all([{ - queryFunc: '_findOne', - collection: 'tournaments', - query: {key: tournament_key}, - }, { - collection: 'matches', - query, - }, { - collection: 'courts', - query: {tournament_key}, - }], function(err, tournament, db_matches, db_courts) { - if (err) { - res.json({ - status: 'error', - message: err.message, - }); - return; - } - - let matches = db_matches.map(dbm => create_match_representation(tournament, dbm)); - matches = matches.filter(m => m.setup.now_on_court); - - db_courts.sort(utils.cmp_key('num')); - const courts = db_courts.map(function(dc) { - var res = { - court_id: dc._id, - label: dc.num, - }; - if (dc.match_id) { - res.match_id = 'bts_' + dc.match_id; - } - if (dc.called_timestamp) { - res.called_timestamp = dc.called_timestamp; - } - return res; - }); - - const event = create_event_representation(tournament); - event.matches = matches; - event.courts = courts; - - const reply = { - status: 'ok', - event, - }; - res.json(reply); - }); -} - -function matchinfo_handler(req, res) { - const tournament_key = req.params.tournament_key; - const match_id = req.params.match_id; - - const query = { - tournament_key, - _id: match_id, - }; - - req.app.db.fetch_all([{ - collection: 'tournaments', - query: {key: tournament_key}, - }, { - collection: 'matches', - query, - }], function(err, tournaments, matches) { - if (err) { - res.json({ - status: 'error', - message: err.message, - }); - return; - } - - if (tournaments.length !== 1) { - res.json({ - status: 'error', - message: 'Cannot find tournament', - }); - return; - } - - if (matches.length !== 1) { - res.json({ - status: 'error', - message: 'Cannot find match', - }); - return; - } - - const [tournament] = tournaments; - const [match] = matches; - const event = create_event_representation(tournament); - const match_repr = create_match_representation(tournament, match); - if (match_repr.presses_json) { - // Parse JSON-in-JSON (for performance reasons) for nicer output - match_repr.presses = JSON.parse(match_repr.presses_json); - delete match_repr.presses_json; - } - event.matches = [match_repr]; - - const reply = { - status: 'ok', - event, - }; - res.header('Content-Type', 'application/json'); - res.send(JSON.stringify(reply, null, 4)); - }); -} - -async function score_handler(req, res) { - if (!_require_params(req, res, ['duration_ms', 'end_ts', 'network_score', 'team1_won', 'presses'])) return; - const match_utils = require('./match_utils'); - const tournament_key = req.params.tournament_key; - const match_id = req.params.match_id; - - - var match = await match_utils.fetch_match(req.app, tournament_key, match_id); - if (match == null || match.setup.now_on_court == false) { - res.json({ - status: 'error', - message: "Match not found or not on court actualy.", - }); - return; - } - - const update = { - network_score: req.body.network_score, - network_team1_left: req.body.network_team1_left, - network_team1_serving: req.body.network_team1_serving, - network_teams_player1_even: req.body.network_teams_player1_even, - presses: req.body.presses, - duration_ms: req.body.duration_ms, - end_ts: req.body.end_ts, - 'setup.now_on_court': true, - }; - - const device_info = req.body.device; - if (device_info) { - const client_ip = req.socket.remoteAddress - device_info.client_ip = client_ip; - } - - - const finish_confirmed = req.body.finish_confirmed ? req.body.finish_confirmed : false; - if (finish_confirmed) { - update.team1_won = req.body.team1_won, - update.btp_winner = (update.team1_won === true) ? 1 : 2; - update.btp_needsync = true; - update["setup.now_on_court"] = false; - reset_player_tabletoperator(req.app, tournament_key, match_id, update.end_ts); - } - - if (req.body.shuttle_count) { - update.shuttle_count = req.body.shuttle_count; - } - - const match_query = { - _id: match_id, - tournament_key, - }; - - const court_q = { - tournament_key, - _id: req.body.court_id, - }; - const db = req.app.db; - - async.waterfall([ - cb => db.matches.update(match_query, { $set: update }, { returnUpdatedDocs: true }, (err, _, match) => cb(err, match)), - (match, cb) => { - if (match) { - bupws.handle_score_change(req.app, tournament_key, match.setup.court_id); - admin.notify_change(req.app, tournament_key, 'score', { - match_id, - network_score: update.network_score, - team1_won: update.team1_won, - shuttle_count: update.shuttle_count, - presses: match.presses, - }); - } - cb(null, match); - }, - (match, cb) => db.courts.findOne(court_q, (err, court) => cb(err, match, court)), - (match, court, cb) => { - if (!match) { - if (court.match_id === match_id) { - cb(null, match, court, false); - return; - } - - db.courts.update(court_q, { $set: { match_id: match_id } }, {}, (err) => { - cb(err, match, court, true); - }); - } - cb(null, match, court, true); - }, - (match, court, changed_court, cb) => { - if (match && changed_court) { - admin.notify_change(req.app, tournament_key, 'court_current_match', { - match__id: match_id, - match: match, - }); - } - cb(null, match, changed_court); - }, - (match, changed_court, cb) => { - if (match) { - btp_manager.update_score(req.app, match); - } - cb(null, match, changed_court); - }, - (match, changed_court, cb) => { - if (match && match.setup.highlight && - match.setup.highlight == 6 && - match.network_score && - match.network_score.length > 0 && - match.network_score[0].length > 1 && - (match.network_score[0][0] > 0 || match.network_score[0][1] > 0)) { - match.setup.highlight = 0; - btp_manager.update_highlight(req.app, match); - } - cb(null, match, changed_court); - }, - (match, changed_court, cb) => { - if (changed_court) { - ticker_manager.pushall(req.app, tournament_key); - } else { - if (match) { - ticker_manager.update_score(req.app, match); - } - } - cb(null, match, changed_court); - }, - (match, changed_court, cb) => { - if (!match) { - return cb(new Error('Cannot find match ' + JSON.stringify(match))); - } - if (finish_confirmed && match.team1_won != undefined && match.team1_won != null) { - match_utils.call_preparation_match_on_court(req.app, tournament_key, match.setup.court_id, (err) => { - - }); - } - return cb(null, match, changed_court); - }, - (match, changed_court, cb) => { - if (!device_info) { - return cb(null, match, changed_court); - } - bupws.update_device_info(req.app, tournament_key, device_info); - return cb(null, match, changed_court); - }, - ], function (err) { - if (err) { - res.json({ - status: 'error', - message: err.message, - }); - return; - } - - res.json({ status: 'ok' }); - }); - -} - -function reset_player_tabletoperator(app, tournament_key, match_id, end_ts) { - const match_utils = require('./match_utils'); // avoid dependency cycle - async.waterfall([ - cb => match_utils.remove_player_on_court(app, tournament_key, match_id, end_ts, cb), - cb => match_utils.remove_tablet_on_court(app, tournament_key, match_id, end_ts, cb), - cb => match_utils.remove_umpire_on_court(app, tournament_key, match_id, end_ts,cb), - cb => match_utils.add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts, cb) - - ], function (err) { - if (err) { - return; - } - }); -} - function logo_handler(req, res) { const {tournament_key, logo_id} = req.params; assert(tournament_key); @@ -434,10 +25,5 @@ function logo_handler(req, res) { } module.exports = { - courts_handler, - logo_handler, - matches_handler, - matchinfo_handler, - score_handler, - reset_player_tabletoperator + logo_handler }; diff --git a/bts/match_utils.js b/bts/match_utils.js index c7cdc17..3e77a6a 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -5,15 +5,15 @@ const async = require('async'); async function match_update(app, match, callback) { async.waterfall([ - (wcb) => update_match_btp(app, match, wcb), - (wcb) => update_match_db(app, match, wcb), - (wcb) => notify_change_match_edit(app, match, wcb), - (wcb) => notify_bupws(app, match, wcb), - ], - (err) => { - return callback(err); - } - ); + (wcb) => update_match_btp(app, match, wcb), + (wcb) => update_match_db(app, match, wcb), + (wcb) => notify_change_match_edit(app, match, wcb), + (wcb) => notify_bupws(app, match, wcb), + ], + (err) => { + return callback(err); + } + ); } async function uncall_match(app, tournament, match, callback) { @@ -953,6 +953,18 @@ function update_btp_courts(app, tournament_key, match, callback) { return; }); } +function reset_player_tabletoperator(app, tournament_key, match_id, end_ts) { + async.waterfall([ + cb => remove_player_on_court(app, tournament_key, match_id, end_ts, cb), + cb => remove_tablet_on_court(app, tournament_key, match_id, end_ts, cb), + cb => remove_umpire_on_court(app, tournament_key, match_id, end_ts, cb), + cb => add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts, cb) + ], function (err) { + if (err) { + return; + } + }); +} module.exports ={ add_player_to_tabletoperator_list, @@ -968,6 +980,7 @@ module.exports ={ set_umpire_to_standby, add_preparation_call_timestamp, remove_preparation_call_timestamp, + reset_player_tabletoperator, call_preparation_match_on_court, call_next_possible_match_for_preparation, call_match_in_preparation From abb1e76f94c0de8981d2deee00ffadc25d60d27a Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 20 Oct 2024 19:46:01 +0200 Subject: [PATCH 143/224] [#69] AdminPanel: Provision of an option for games to also confirm termination from the admin panel --- static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/cmatch.js | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index a413e9f..09dafac 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -21,6 +21,7 @@ var ci18n_de = { 'Number:': '#:', 'Cancel': 'Abbrechen', 'Change': 'Ändern', + 'Confirm_Finish': 'Spielende bestätigen', 'No umpire': 'Kein Schiedsrichter', 'No service judge': 'Kein Aufschlagrichter', 'Edit match': 'Match bearbeiten', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 66c87f6..bee532a 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -21,6 +21,7 @@ var ci18n_en = { 'Number:': 'Number:', 'Cancel': 'Cancel', 'Change': 'Change', + 'Confirm_Finish': 'Confirm end of game', 'No umpire': 'No umpire', 'No service judge': 'No service judge', 'Edit match': 'Edit match', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 16b69e9..c2a3499 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -987,6 +987,23 @@ function _delete_match_btn_click(e) { _cancel_ui_edit(); }); } + function _finish_ui_edit(e) { + const match_id = e.target.getAttribute('data-match_id'); + const match = utils.find(curt.matches, m => m._id === match_id); + if (match) { + send({ + type: 'confirm_match_finished', + match_id: match_id, + tournament_key: match.tournament_key, + court_id: match.setup.court_id + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + } + _cancel_ui_edit(); + } function ui_edit(match_id) { const match = utils.find(curt.matches, m => m._id === match_id); @@ -1059,6 +1076,13 @@ function ui_edit(match_id) { 'data-match_id': match_id, }, ci18n('match:edit:delete')); delete_btn.addEventListener('click', _delete_match_btn_click); + + const finish_btn = uiu.el(buttons, 'span', { + 'class': 'match_cancel_link vlink', + 'data-match_id': match._id + }, ci18n('Confirm_Finish')); + finish_btn.addEventListener('click', _finish_ui_edit); + const cancel_btn = uiu.el(buttons, 'span', 'match_cancel_link vlink', ci18n('Cancel')); cancel_btn.addEventListener('click', _cancel_ui_edit); } From a6247103a211a641bad3621d17fdec39d373e4f1 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 20 Oct 2024 20:13:54 +0200 Subject: [PATCH 144/224] [#66] BUPWS / Settings GUI: Show hostname of displays --- bts/bupws.js | 5 ++++- static/js/ctournament.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index 7e277b4..bffd54b 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -769,19 +769,22 @@ async function add_display_status(app, tournament, displays, callback) { if (d.client_id == ws_client_id) { d.online = true; d.battery = panel_ws.battery; + d.hostname = await determine_client_hostname(panel_ws); + break; } } } for (const panel_ws of all_panels) { var found = false; const ws_client_id = determine_client_id(panel_ws); - const ws_hostname = await determine_client_hostname(panel_ws); for (const d of displays) { if (d.client_id == ws_client_id) { found = true; + break; } } if (!found) { + const ws_hostname = await determine_client_hostname(panel_ws); const client_court_displaysetting = create_display_court_displaysettings(ws_client_id, ws_hostname, panel_ws.court_id, generate_default_displaysettings_id(tournament)); client_court_displaysetting.online = true; client_court_displaysetting.battery = panel_ws.battery; diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 5974d30..92b3492 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -1092,7 +1092,7 @@ var ctournament = (function() { const tr = uiu.el(display_tbody, 'tr'); uiu.el(tr, 'th', {}, c.client_id); uiu.el(tr, 'th', {}, c.hostname); - var battery_node = uiu.el(tr, 'td', {}, 'N/N"'); + var battery_node = uiu.el(tr, 'td', {}, 'N/A'); set_battery_state(c.battery, battery_node); createCourtSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.court_id); createDisplaySettingsSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.displaysetting_id); From f4c41d096dd7528cba4b6a814e21eb4d9e461032 Mon Sep 17 00:00:00 2001 From: tengmel Date: Mon, 21 Oct 2024 21:19:02 +0200 Subject: [PATCH 145/224] [#51] Admin: Edit tournament: Display battery charge levels for the tablets --- bts/bupws.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index bffd54b..83c2092 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -278,10 +278,12 @@ async function update_device_info(app, tournament_key, device_info) { const client_id = determine_client_id_from_ip(device_info.client_ip); const panel = fetch_panel(client_id); if (panel != null) { - const hostname = "N/N"; + const hostname = await determine_client_hostname(panel); var display_court_displaysetting = await get_display_court_displaysettings(app, client_id); if (display_court_displaysetting == null) { - display_court_displaysetting = create_display_court_displaysettings(client_id, hostname, null, generate_default_displaysettings_id(tournament)); + display_court_displaysetting = create_display_court_displaysettings(client_id, hostname, panel.court_id, generate_default_displaysettings_id(tournament)); + } else { + display_court_displaysetting.hostname = hostname; } panel.battery = device_info.battery display_court_displaysetting.battery = device_info.battery From 2d873d9fbc2772378020c8293b9c10f9fe786387 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 23 Oct 2024 22:27:53 +0200 Subject: [PATCH 146/224] Fix wrong path of the http_api call for the logo sice commit 221dc77ade40385cddf7b34cb097ef0ee171a714 --- bts/bts.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bts/bts.js b/bts/bts.js index a28c72f..3e837da 100644 --- a/bts/bts.js +++ b/bts/bts.js @@ -96,7 +96,7 @@ function create_app(config, db) { app.use('/u(:courtnum)?', shortcuts.umpire_handler); app.use(body_parser.json()); - app.get('/h/:tournament_key/C:logo_id', http_api.logo_handler); + app.get('/h/:tournament_key/logo/:logo_id', http_api.logo_handler); var server = null; if (config.enable_https) { From 9e205bc2393f5dcb76dd9e5e6aff2544464c7cb0 Mon Sep 17 00:00:00 2001 From: tengmel Date: Thu, 24 Oct 2024 21:52:10 +0200 Subject: [PATCH 147/224] [#51] Admin: Edit tournament: Display battery charge levels for the tablets --- static/js/change.js | 1 + static/js/ctournament.js | 71 +++++++++++++++++++++++----------------- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/static/js/change.js b/static/js/change.js index 3c42fda..d9641b0 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -213,6 +213,7 @@ function default_handler(rerender, special_funcs) { if (laststatus != d.online) { cerror.silent('Display ' + display_setting.client_id + ' is ' + (display_setting.online ? 'online' : 'offline')); } + ctournament.update_display(d); break; default: cerror.silent('Unsupported change type ' + c.ctype); diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 92b3492..cd602f2 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -1088,39 +1088,49 @@ var ctournament = (function() { uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:onlinestatus')); - for (const c of curt.displays) { - const tr = uiu.el(display_tbody, 'tr'); - uiu.el(tr, 'th', {}, c.client_id); - uiu.el(tr, 'th', {}, c.hostname); - var battery_node = uiu.el(tr, 'td', {}, 'N/A'); - set_battery_state(c.battery, battery_node); - createCourtSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.court_id); - createDisplaySettingsSelectBox(uiu.el(tr, 'td', {}, ''), c.client_id, c.displaysetting_id); - uiu.el(tr, 'td', {}, (!c.online) ? 'offline' : 'online'); - const actions_td = uiu.el(tr, 'td', {}); - const reset_btn = uiu.el(actions_td, 'button', { - 'data-display-setting-id': c.client_id, - }, 'Restart'); - - if (!c.online) { - reset_btn.setAttribute('disabled', 'disabled'); - } - reset_btn.addEventListener('click', function (e) { - const del_btn = e.target; - const display_setting_id = del_btn.getAttribute('data-display-setting-id'); - send({ - type: 'reset_display', - tournament_key: curt.key, - display_setting_id: display_setting_id, - }, err => { - if (err) { - return cerror.net(err); - } - }); - }); + for (const display of curt.displays) { + const tr = uiu.el(display_tbody, 'tr', { 'data-display_id': display.client_id }); + render_display(tr, display); } } + function update_display(display) { + uiu.qsEach('[data-display_id=' + JSON.stringify(display.client_id) + ']', function (display_tr) { + display_tr.innerHTML = ''; + render_display(display_tr, display); + }); + } + + function render_display(tr, display) { + uiu.el(tr, 'th', {}, display.client_id); + uiu.el(tr, 'th', {}, display.hostname); + var battery_node = uiu.el(tr, 'td', {}, 'N/A'); + set_battery_state(display.battery, battery_node); + createCourtSelectBox(uiu.el(tr, 'td', {}, ''), display.client_id, display.court_id); + createDisplaySettingsSelectBox(uiu.el(tr, 'td', {}, ''), display.client_id, display.displaysetting_id); + uiu.el(tr, 'td', {}, (!display.online) ? 'offline' : 'online'); + const actions_td = uiu.el(tr, 'td', {}); + const reset_btn = uiu.el(actions_td, 'button', { + 'data-display-setting-id': display.client_id, + }, 'Restart'); + + if (!display.online) { + reset_btn.setAttribute('disabled', 'disabled'); + } + reset_btn.addEventListener('click', function (e) { + const del_btn = e.target; + const display_setting_id = del_btn.getAttribute('data-display-setting-id'); + send({ + type: 'reset_display', + tournament_key: curt.key, + display_setting_id: display_setting_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + } function render_courts(main) { uiu.el(main, 'h2', {}, ci18n('tournament:edit:courts')); @@ -1517,6 +1527,7 @@ var ctournament = (function() { ui_list, update_match, update_upcoming_match, + update_display, btp_status_changed, ticker_status_changed, bts_status_changed, From 732dfcda5f97c76282cea2d3b56ee6b5862a06ea Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 25 Oct 2024 19:49:17 +0200 Subject: [PATCH 148/224] Fix: The Match Info View is now workin again. --- bts/bts.js | 1 + bts/http_api.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/bts/bts.js b/bts/bts.js index 3e837da..c34f139 100644 --- a/bts/bts.js +++ b/bts/bts.js @@ -96,6 +96,7 @@ function create_app(config, db) { app.use('/u(:courtnum)?', shortcuts.umpire_handler); app.use(body_parser.json()); + app.get('/h/:tournament_key/m/:match_id/info', http_api.matchinfo_handler); app.get('/h/:tournament_key/logo/:logo_id', http_api.logo_handler); var server = null; diff --git a/bts/http_api.js b/bts/http_api.js index d5183f2..54cd31c 100644 --- a/bts/http_api.js +++ b/bts/http_api.js @@ -3,6 +3,7 @@ const assert = require('assert'); const path = require('path'); const utils = require('./utils'); +const bupws = require('./bupws'); function logo_handler(req, res) { const {tournament_key, logo_id} = req.params; @@ -24,6 +25,68 @@ function logo_handler(req, res) { res.sendFile(fn); } +function matchinfo_handler(req, res) { + const tournament_key = req.params.tournament_key; + const match_id = req.params.match_id; + + const query = { + tournament_key, + _id: match_id, + }; + + req.app.db.fetch_all([{ + collection: 'tournaments', + query: {key: tournament_key}, + }, { + collection: 'matches', + query, + }], function(err, tournaments, matches) { + if (err) { + res.json({ + status: 'error', + message: err.message, + }); + return; + } + + if (tournaments.length !== 1) { + res.json({ + status: 'error', + message: 'Cannot find tournament', + }); + return; + } + + if (matches.length !== 1) { + res.json({ + status: 'error', + message: 'Cannot find match', + }); + return; + } + + const [tournament] = tournaments; + const [match] = matches; + const event = bupws.create_event_representation(tournament); + const match_repr = bupws.create_match_representation(tournament, match); + if (match_repr.presses_json) { + // Parse JSON-in-JSON (for performance reasons) for nicer output + match_repr.presses = JSON.parse(match_repr.presses_json); + delete match_repr.presses_json; + } + event.matches = [match_repr]; + + const reply = { + status: 'ok', + event, + }; + res.header('Content-Type', 'application/json'); + res.send(JSON.stringify(reply, null, 4)); + }); +} + + module.exports = { - logo_handler + logo_handler, + matchinfo_handler }; From ad87323c6a8a4208e571f6a351e248006f3e3a52 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 22 Oct 2024 23:25:56 +0200 Subject: [PATCH 149/224] Improve Admin View and Upcomming Matches --- bts/admin.js | 31 +- bts/btp_conn.js | 8 +- bts/btp_sync.js | 24 +- bts/bupws.js | 9 +- bts/match_utils.js | 34 +- static/css/admin.css | 31 +- static/css/cmatch.css | 126 +++++++- static/css/court.css | 3 +- static/css/ctabletoperator.css | 11 +- static/icons/check_in.png | Bin 0 -> 962 bytes static/icons/check_in.svg | 67 ++++ static/icons/check_out.png | Bin 0 -> 1733 bytes static/icons/check_out.svg | 76 +++++ static/icons/path838.png | Bin 0 -> 1604 bytes static/js/announcements.js | 8 +- static/js/change.js | 17 +- static/js/ci18n_de.js | 2 + static/js/ci18n_en.js | 2 + static/js/cmatch.js | 561 +++++++++++++++++++++++++-------- static/js/ctabletoperator.js | 19 +- static/js/ctournament.js | 84 ++++- static/js/cumpires.js | 2 +- 22 files changed, 905 insertions(+), 210 deletions(-) create mode 100644 static/icons/check_in.png create mode 100644 static/icons/check_in.svg create mode 100644 static/icons/check_out.png create mode 100644 static/icons/check_out.svg create mode 100644 static/icons/path838.png diff --git a/bts/admin.js b/bts/admin.js index fb185ec..f7f5ac2 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -241,8 +241,9 @@ function _extract_setup(msg_setup) { 'match_name', 'match_num', 'now_on_court', - 'umpire_name', + 'umpire', 'service_judge_name', + 'service_judge', 'highlight', 'is_doubles', 'is_match', @@ -506,7 +507,7 @@ function handle_match_call_on_court(app, ws, msg) { return; }); }else { - return ws.respond(msg, "Match cannot be fetched from DB " + msg.match_id); + return ws.respond(msg, "Match cannot be fetched from DB 222 " + msg.match_id); } }); }); @@ -556,7 +557,7 @@ function handle_match_edit(app, ws, msg) { return; } - notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, changed_match}); + notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, match: changed_match}); if (msg.btp_update) { btp_manager.update_score(app, changed_match); } @@ -605,11 +606,13 @@ function handle_match_player_check_in (app, ws, msg) { if (err) { return ws.respond(msg, err); } + app.db.matches.findOne({tournament_key: msg.tournament_key, _id: msg.match_id}, async (err, match) => { if (err) { return ws.respond(msg, err); } + for(const team of match.setup.teams) { for(const player of team.players) { @@ -619,6 +622,8 @@ function handle_match_player_check_in (app, ws, msg) { } } + + match_utils.match_update(app, match, (err) => { ws.respond(msg, err); return; @@ -722,6 +727,24 @@ function handle_reset_display(app, ws, msg) { ws.respond("Angekommen: " + client_id); } +async function async_handle_delete_display_setting(app, ws, msg) { + const tournament_key = msg.tournament_key; + const setting_id = msg.setting_id; + const display = await app.db.display_court_displaysettings.findOne_async({displaysetting_id:setting_id}); + + if(display) { + ws.respond(msg, {message: `Could not delete displaysetting ${msg.setting_id} while in use`}); + return; + } + const query_remove = {id: setting_id}; + app.db.displaysettings.remove(query_remove, {}, (err) => { + notify_change(app, tournament_key, 'delete_display_setting', setting_id); + }); + + ws.respond("Angekommen: " + setting_id); +} + + function handle_relocate_display(app, ws, msg) { const tournament_key = msg.tournament_key; const client_id = msg.display_setting_id; @@ -736,6 +759,7 @@ function handle_change_display_mode(app, ws, msg) { const new_displaysettings_id = msg.new_displaysettings_id; const bupws = require('./bupws'); bupws.change_display_mode(app, tournament_key, client_id, new_displaysettings_id); + notify_change(app, tournament_key, 'update_general_displaysettings', {}); ws.respond("Angekommen: " + client_id); } @@ -900,6 +924,7 @@ async function async_handle_tournament_upload_logo(app, ws, msg) { } module.exports = { + async_handle_delete_display_setting, async_handle_match_delete, async_handle_tournament_upload_logo, handle_begin_to_play_call, diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 2e60d27..59ee5d2 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -216,12 +216,12 @@ class BTPConn { async.waterfall([ (cb) => { - if (!match.setup || !match.setup.umpire_name) { + if (!match.setup || !match.setup.umpire || !match.setup.umpire.name) { return cb(null, null, null); } this.app.db.umpires.findOne({ - name: match.setup.umpire_name, + name: match.setup.umpire.name, tournament_key: this.tkey, }, (err, umpire) => { if (err) { @@ -229,12 +229,12 @@ class BTPConn { } const umpire_btp_id = umpire ? umpire.btp_id : null; - if (!match.setup.service_judge_name) { + if (!match.setup.service_judge || !match.setup.service_judge.name) { return cb(null, umpire_btp_id, null); } this.app.db.umpires.findOne({ - name: match.setup.service_judge_name, + name: match.setup.service_judge.name, tournament_key: this.tkey, }, (err, service_judge) => { if (err) { diff --git a/bts/btp_sync.js b/bts/btp_sync.js index a891c40..2bb293c 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -117,14 +117,18 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, } if (tournament.btp_settings.check_in_per_match && teams.length > 1 && teams[0].players.length > 0) { teams[0].players[0].checked_in = (bm.Status & 0b0001) > 0; + teams[0].players[0].check_in_per_match = true; if (teams[0].players.length > 1) { teams[0].players[1].checked_in = (bm.Status & 0b0010) > 0; + teams[0].players[1].check_in_per_match = true; } if (teams[1].players.length > 0) { teams[1].players[0].checked_in = (bm.Status & 0b0100) > 0; + teams[1].players[0].check_in_per_match = true; if (teams[1].players.length > 1) { teams[1].players[1].checked_in = (bm.Status & 0b1000) > 0; + teams[1].players[1].check_in_per_match = true; } } } @@ -148,12 +152,13 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, if (bm.Official1ID) { const o = get_umpire(app, tkey, officials, bm.Official1ID[0]); assert(o); - setup.umpire_name = o.firstName + ' ' + o.surname; + setup.umpire = o; + } if (bm.Official2ID) { const o = get_umpire(app, tkey, officials, bm.Official2ID[0]); assert(o); - setup.service_judge_name = o.firstName + ' ' + o.surname; + setup.service_judge = o; } const btp_match_ids = [{ @@ -778,6 +783,7 @@ async function integrate_player_state(app, tkey, btp_state, callback) { const player = cur_match.setup.teams[team_nr].players[player_nr]; if (ids_to_change.indexOf(id) == -1) { player.checked_in = true; + player.check_in_per_match = false; player.tablet_break_active = false; ids_to_change.push(id); players_to_change.push(player); @@ -865,9 +871,9 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { var changed = false; async.each(officials, (o, cb) => { - const firstName = (o.FirstName ? o.FirstName[0] : ''); + const firstname = (o.FirstName ? o.FirstName[0] : ''); const surname = (o.Name ? o.Name[0] : ''); - const name = (firstName + " " + surname).trim(); + const name = (firstname + " " + surname).trim(); const country = (o.Country ? o.Country[0] : ''); const btp_id = o.ID[0]; if (!btp_id) { @@ -880,12 +886,12 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { if (cur) { if (cur.btp_id === btp_id && - cur.firstName == firstName && + cur.firstname == firstname && cur.surname == surname && cur.country === country) { return cb(); } else { - app.db.umpires.update({ tournament_key, btp_id }, { $set: { btp_id, firstName, surname, name, country } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { + app.db.umpires.update({ tournament_key, btp_id }, { $set: { btp_id, firstname, surname, name, country } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { if (err) { console.error(err); return; @@ -900,7 +906,7 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { const u = { _id: tournament_key + '_btp_' + btp_id, btp_id, - firstName, + firstname, surname, name, status: 'ready', @@ -943,8 +949,8 @@ function calculate_match_ids_on_court(btp_state) { -function update_umpire(app, tkey, umpire_name, status, last_time_on_court_ts,court_id) { - app.db.umpires.update({ tournament_key: tkey, name: umpire_name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { +function update_umpire(app, tkey, umpire, status, last_time_on_court_ts,court_id) { + app.db.umpires.update({ tournament_key: tkey, name: umpire.name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { if (err) { console.error(err); return; diff --git a/bts/bupws.js b/bts/bupws.js index 83c2092..351ec2d 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -108,12 +108,12 @@ async function handle_persist_display_settings(app, ws, msg) { var client_court_displaysetting = await get_display_court_displaysettings(app, client_id); if (client_court_displaysetting == null) { setting.id = tournament_key + "_" + court_id + " _" + Date.now(); - setting = await persist_displaysetting(app, setting); + setting = await persist_displaysetting(app, tournament_key, setting); client_court_displaysetting = create_display_court_displaysettings(client_id, hostname, court_id, setting.id); client_court_displaysetting = await persist_client_court_displaysetting(app, client_court_displaysetting); } else { setting.id = tournament_key + "_" + court_id + " _" + Date.now(); - setting = await persist_displaysetting(app, setting); + setting = await persist_displaysetting(app, tournament_key, setting); const updatevalues = { court_id: court_id, displaysetting_id: setting.id, @@ -430,13 +430,14 @@ function update_client_court_displaysetting(app, client_court_displaysetting_id, } -function persist_displaysetting(app, setting) { +function persist_displaysetting(app, tournament_key, setting) { setting._id = undefined; return new Promise((resolve, reject) => { app.db.displaysettings.insert(setting, function (err, inserted_t) { if (err) { reject(err); } + admin.notify_change(app, tournament_key, 'update_display_setting', {setting: inserted_t}); resolve(inserted_t); }); }); @@ -813,4 +814,6 @@ module.exports = { change_display_mode, change_default_display_mode, add_display_status, + create_match_representation, + create_event_representation, }; \ No newline at end of file diff --git a/bts/match_utils.js b/bts/match_utils.js index 3e77a6a..f26b733 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -123,7 +123,7 @@ async function add_tabletoperators(app, tournament, match, callback) { if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { if (!setup.tabletoperators || setup.tabletoperators == null) { const value = await fetch_tabletoperator(admin, app, tournament.key, court_id); - if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { + if (!setup.umpire || !setup.umpire.name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { setup.tabletoperators = value; } } @@ -148,12 +148,12 @@ async function set_umpires_on_court(app, tournament, match, callback) { return; // TODO in async we would assert both to be true } - if (setup.umpire_name) { - update_umpire(app, tournament.key, setup.umpire_name, 'oncourt', setup.called_timestamp, court_id); + if (setup.umpire) { + update_umpire(app, tournament.key, setup.umpire, 'oncourt', setup.called_timestamp, court_id); } - if (setup.service_judge_name) { - update_umpire(app, tournament.key, setup.service_judge_name, 'oncourt', setup.called_timestamp, court_id); + if (setup.service_judge) { + update_umpire(app, tournament.key, setup.service_judge, 'oncourt', setup.called_timestamp, court_id); } return callback(null); } @@ -506,7 +506,7 @@ function fetch_match(app, tournament_key, match_id) { if (match != null) { return resolve(match) } else { - return reject("Match cannot be fetched from DB " + match_id); + return reject("Match cannot be fetched from DB 111 " + match_id); } }); }); @@ -756,14 +756,14 @@ function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, callb if (err) { return callback(err); } - if (cur_match.setup.umpire_name) { + if (cur_match.setup.umpire) { - update_umpire(app, tournament_key, cur_match.setup.umpire_name, 'ready', end_ts, null); + update_umpire(app, tournament_key, cur_match.setup.umpire, 'ready', end_ts, null); } - if (cur_match.setup.service_judge_name) { + if (cur_match.setup.service_judge) { - update_umpire(app, tournament_key, cur_match.setup.service_judge_name, 'ready', end_ts, null); + update_umpire(app, tournament_key, cur_match.setup.service_judge, 'ready', end_ts, null); } return callback(null); @@ -771,19 +771,19 @@ function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, callb } function set_umpire_to_standby(app, tournament_key, setup) { - if (setup.umpire_name) { - update_umpire(app, tournament_key, setup.umpire_name, 'standby', null, null); + if (setup.umpire) { + update_umpire(app, tournament_key, setup.umpire, 'standby', null, null); } - if (setup.service_judge_name) { - update_umpire(app, tournament_key, setup.service_judge_name, 'standby', null, null); + if (setup.service_judge) { + update_umpire(app, tournament_key, setup.service_judge, 'standby', null, null); } } -function update_umpire(app, tkey, umpire_name, status, last_time_on_court_ts, court_id, callback) { - app.db.umpires.update({ tournament_key: tkey, name: umpire_name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { +function update_umpire(app, tkey, umpire, status, last_time_on_court_ts, court_id, callback) { + app.db.umpires.update({ tournament_key: tkey, name: umpire.name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { if (err) { console.error(err); return; @@ -891,7 +891,7 @@ async function call_match_in_preparation(app, tournament, match_id, setup, callb const admin = require('./admin'); if (tournament.preparation_tabletoperator_setup_enabled) { - if (!setup.umpire_name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { + if (!setup.umpire || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { if (!setup.tabletoperators || setup.tabletoperators == null) { setup.tabletoperators = await fetch_tabletoperator(admin, app, tournament_key, "prep_call"); } diff --git a/static/css/admin.css b/static/css/admin.css index 4653334..6450d2c 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -141,6 +141,9 @@ table.striped-table { } +.old { + position: fixed; left: 0; right: 0; top: 0; bottom: 0; +} .main_upcoming { background: #000; color: #fff; @@ -149,16 +152,31 @@ table.striped-table { font-size: 35px; cursor: none; } + +.end_container { + height: 400px; +} + +.help_me{ + position: fixed; + left: 0; right: 0; top: 0; bottom: 0; + height: 975px; + background-color: red; + width: 50px; +} + .upcoming_table { border-collapse: collapse; + width: 98%; + margin: 0 1% 0 1%; } .upcoming_table td { - padding-bottom: 0.3em; - vertical-align: top; + vertical-align: middle; } .upcoming_table td:not(:first-child) { padding-left: 0.3em; + padding-right: 0.3em; white-space: pre-wrap; } @@ -166,6 +184,7 @@ table.striped-table { background-color: #00000020; display: flex; flex-direction: row; + flex-wrap: wrap-reverse; justify-content: space-around; padding: 0.25em 0 0 0; margin: 0 0 0.75em 0; @@ -213,3 +232,11 @@ table.striped-table { background-image: url(../icons/signal_black.svg); } +h2 { + margin: 0; + padding-top: 15px; + padding-bottom: 5px; + font-size: 70px; + color: #fff; + text-align: center; +} \ No newline at end of file diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 0d99054..9ae403e 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -139,13 +139,35 @@ text-align: right; color: #666; } + .match_team2 { padding-right: 0.3em; } + +.match_team1_public, +.match_team2_public { + width: 50%; + text-wrap: nowrap; +} + +.match_team1_upcoming, +.match_team2_upcoming { + width:50%; + text-wrap: nowrap; + padding: 0px !important; + margin: 0px !important; +} + +.match_team1_upcoming > span, +.match_team2_upcoming > span{ + text-wrap: nowrap; +} + .match_vs { color: #999; - padding: 0 0.5em; - width: 0.5em; + width: 1.0em; + text-align: center; + margin: 0px; } .match_team_won { font-weight: bold; @@ -154,11 +176,40 @@ .match_score{ font-weight: bold; padding-left: 0.2em; + text-wrap: nowrap; } .match_score_current { font-weight: bold; font-size: 1.5em; padding-left: 0.3em; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + align-items: center; +} + +.no_status_public { + font-size: 60px; +} + +.operators_public { + font-size: 60px; + font-weight: 100; +} + +.umpire_name_public { + font-size: 60px; + font-weight: 100; +} + +.score_public { + width: 550px !important; + max-width: 550px !important; +} + +.score_public > .match_score_current { + font-size: 1.82em; } .match_score_current > .tablet, @@ -275,10 +326,18 @@ match_manual_call_button, background-color: #adffcb; } +.can_check_out { + cursor: url('../icons/check_out.png')15 15, auto; +} + .not_checked_in { background-color: #ffabab; } +.can_check_in { + cursor: url('../icons/check_in.png')15 15, auto; +} + .now_playing { background-color: #fbe44d; } @@ -311,7 +370,8 @@ match_manual_call_button, .service_judge, .no_umpire, .court, -.court_history { +.court_history, +.court_upcoming { margin: 0 0.2em -0.2em 0.2em; display: inline-block; width: 1.73em; @@ -332,6 +392,10 @@ match_manual_call_button, background-image: url(../icons/tablet.svg); } +.tablet { + margin: -1px 0.2em -0.2em 0.2em; +} + .tablet_inline { height: 1em; min-height: 1em; @@ -350,6 +414,12 @@ match_manual_call_button, text-wrap: nowrap; } +.tablet_operator { + display: flex; + flex-wrap: nowrap; + align-items: center; +} + .umpire { background-image: url(../icons/umpire.svg); } @@ -375,7 +445,8 @@ match_manual_call_button, } -.court_history { +.court_history, +.court_upcoming { background-image: url(../icons/court_history.svg); height: 1em; min-height: 1em; @@ -387,6 +458,11 @@ match_manual_call_button, line-height: 100%; } +.court_upcoming { + margin-left: 0; + margin-right: 0; +} + .highlight_0 { background-color: #ffffff; } @@ -417,6 +493,7 @@ match_manual_call_button, .preperation { font-weight: bold; + text-wrap: nowrap; } @media print { @@ -436,11 +513,6 @@ match_manual_call_button, font-style: italic; } -.player { - color: #000000; - font-style: normal; -} - td.actions { width: 80px; } @@ -466,6 +538,8 @@ td.match_properties { width: 270px; } + + td.umpire_and_tablet{ width: 50px; padding-right: 10px; @@ -500,4 +574,38 @@ h3.section { margin-right: 1%; margin-top: 20px; text-align: center; +} + +.main_upcoming > div > table > tbody > .court_row:nth-of-type(even), +.main_upcoming > div > table > tbody > tr:nth-of-type(even){ + background-color: #222222; +} + +.match_number_upcoming, +.match_scheduled_upcoming, +.match_event_upcoming { + color: #aaa; + text-wrap: nowrap; + width: 50px; +} + +.court_number_upcoming { + width: 0px; +} + +.upcoming_tbody{ + vertical-align: middle; +} + +.upcoming_tbody > tr { + height: 65px; +} + +.upcoming_tbody > tr > td { + height: 100%; + vertical-align: inherit; +} + +.complete { + cursor: grab; } \ No newline at end of file diff --git a/static/css/court.css b/static/css/court.css index 30bd62c..218ed28 100644 --- a/static/css/court.css +++ b/static/css/court.css @@ -32,7 +32,8 @@ background-image: url(../icons/court.svg); height: 1.0em; min-height: 1.0em; - width: 1.3em; + width: 1.2em; + min-width: 1.2em; font-weight: 900; font-size: 1.8em; -webkit-text-stroke-width: 1px; diff --git a/static/css/ctabletoperator.css b/static/css/ctabletoperator.css index fe2df03..52594bd 100644 --- a/static/css/ctabletoperator.css +++ b/static/css/ctabletoperator.css @@ -88,12 +88,17 @@ .unassigned_tableoperators_content { height: 130px; max-height: 130px; - width: 325px; - max-height: 325px; - overflow: auto; } .tabletoperator_add_custom_input { width: 290px; } +.tabletoperators_table { + border-collapse: collapse; + border: 0px solid; +} + +.tabletoperators_table > tbody > tr:nth-of-type(even) { + background-color: #ccc; +} diff --git a/static/icons/check_in.png b/static/icons/check_in.png new file mode 100644 index 0000000000000000000000000000000000000000..d8eba65daf9fbd4497e1a473341d4ffb558493a5 GIT binary patch literal 962 zcmV;z13mnSP)Vl&|00004b3#c}2nYxW zdt1aW_ax)Pu3b1UQhu zg@ZAXss}G1c;JLyJQ)%jO-M8mq9?$_L=qG%f3$`}FD+Q32BMKDe}=CN>%3%=dGGgLJ~Q)se@5VciLlaok7?a$`G^LITRk`;O^`S)Z~DVV z*g)J$(IRGOWvf3Zgh^Om&?R#utt-H4zu-8}Sby+tI8RcZ2O|Ek&1~WYd6N$#c@pw0 z5by_Xmpj;Ir#akIBq93&=?}b0?xZ_#o8D$cLJk2zzu>ocEi}$Ri(##G1&I0sx5?Mp z9+==_)X*hUK++%B;UzW)M!6U@>AL(ZJxlF~KN7GVJ^D)odo$_L>NLIO1KWidrsf4&s>5uVk3 z47w9F(U`D-7yxauo0mgdSSQP4zAw%&ZT181@^DD=kE+dkOav%zlWnwffVV@f02G0f zg&Rz&Z<-pbWECZZ(C##~(Lh_>Qri!mi0Dt+HIA(0lqDbl2ykVfmiS7d2xYJT^*hKct4d2Wc3ls3$4NUm^#kS?!R?= zA1#hDp$|0zuEkw`<(E2J?PaCO>eG}L+W&xq%ak7X{HR9?BaG>f8wGpeFl+j``q&YB zQyJ1~0H~jm29@X79iHU3sHt3=1H-_|MnhizFW2~UPTo@*ZmA!EsYc_%vo#@3k+i!I k7)F$2L~j6U51wU~`@K>XqsfYTW&i*H07*qoM6N<$f?c?_o&W#< literal 0 HcmV?d00001 diff --git a/static/icons/check_in.svg b/static/icons/check_in.svg new file mode 100644 index 0000000..5e734a9 --- /dev/null +++ b/static/icons/check_in.svg @@ -0,0 +1,67 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/static/icons/check_out.png b/static/icons/check_out.png new file mode 100644 index 0000000000000000000000000000000000000000..a607f3cb018bb6e939c4d112715ba965bc72e690 GIT binary patch literal 1733 zcmV;$20HnPP)Vl&|00004b3#c}2nYxW zd zY(f-L>Hu+3NUK(=Xd3mg%1foHQeUe)v^%$*IWwmZ^}54i8wcqBZT{co_np<5Z?wRSS~V@*o=BXkXRHDL2i0g=?Y9i0 zuT|611y2Tm7ERl_Q_~LaNhX`~QM4tS&-YFN*@p#A0j2GU#F6is=4OMCXISrt7r;uv%4@=Lp#}V4N+%?f7vhq_%0sc7dlp_x(cvf(HQ8_C(_F4^pX}Qp~KRGsC8 zjiNPI|2u&AI^?ao{^n~XB?l6c6hAaB`u=#2ZNGZK^G?YC@Ndqq-3I=-M3OSgC`}a- zB|^}|*qW&*svQb~?!|()YueAgUsCdVsVpmT|7s9SA9b9cob|jeVCJ?1z$E8G*)SY_ zl+sMOEGG*-C4@9Fw)R#OEgK1f_wNPX#Mo=!G0k68P-?{e@i4T0Z&^S8)OCMb@Z@;H zL>T_d5#m-QrQ1qnSu6MoA<`OEU2h4Y3`FTqSua?qui6p57Un4}WQM~LX{ zZ1z_tUH7NOHFv=QV2ty#b`;5Vs@j&6BwFwdNm8m65(A#tl+mOO7qS{;_VHZ zRvYi*3xVI{a_@cMI6s<;GbbDXMmX=g9YsqXSJfvqLS}Dli6pT{WqEThimrXh`9Q6v z)$GvqcQc7ZW4t~Ba4?(ge&2ES&pa+34gjN^pU4SO)x=nfLjL!%mdUbSCd*qUIiK95 zX>YU}#uM>60pJh0++Pmc_Fe$~eErFMftaM7hVjm;rujx}0O_xk?#20icMVwy_#PMMW|+O~iESI@gXSI`y$$87tp&mHI7VjC{@ zqQC6bi3 zqv+DLFdTYdcnz?8hiM+|NG99jz5sC8vMyH0a+xkkvwa?;G-XKA)+r$dZg4()-|%u^ z*$&e@^6g~u>0Y$LX^&;?%>vWIoS(3SsAyExRyDSWDk)7Fglw4x&QEeax@h<+pz^trlA|5T zs4t*>3(Sr zaJOyuf0WJc83Aq-$7WR6giXbp@EeL!Muo^&gq*p-`L(;?^}1gDoT7B@o&(%z+kgKg zo9!3|#^(Xg4lo!5oq-TfG%>a^zQJk~rA&^Z^mG*c?Pe5B%))Clt#*g19x47h0l-nm zxp3O|_YQJCb`Nkt;pHIc41gz_8Ec5|g%ygjlu1&0I*R&khT)`K%~<^lns%t&Fj|Yd z^f=Ci5A*pwS3Pge+q!`Gl<#+1K-)UTR>TLWp|qR;nVb-x$lDm}dBrq07FYK=&iUi{ z{7a*rH}W;X000{Z#(cj=5VEyVRjcE%Iz=fT4uWRckmWBJA=|4deWVaP?l=RzuDgFU zpTBxP;DYjiJK_7sAW2(Rt7=uj@Atg10pEXJzRvlm4g7-v+p86&^h3wF{IToqzvBCs z9|T;G4cL|>9Y+*xYfzMmvz|A0!gUYy=kq6LlUg+`y(gJG)u^hEKMZKG>KI$KJDI%W bFY + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/static/icons/path838.png b/static/icons/path838.png new file mode 100644 index 0000000000000000000000000000000000000000..5023a9ff50021d9b30c5e874ce7f84e7590fdf5c GIT binary patch literal 1604 zcmV-K2D|x*P)c&K@07Ce4bR&KfM01? zyOckjEyNn?5JJD}dCTvSa_bcYi=|R_+~;{uQXt|4Ae<1a%jfSkgs4A9(Ha2ea?u)q zy2YN?xzhKS4r+~}wV&}}V>AE=La@&Fzoo4Wy5sl<0PLj#3sT;+*l{*3k#Y_IB-ZM_ zG(E`#r6;E`);gK794Ca_kjcy~8dKksrmtNJutkV(-XrCv1=(y<)jOtCv@1z|!FrY2 z6B=`D8s`hAFqU%&A%`HSJaE?eo#50{)$zxAlJCUz?|NbP7HwmGl7*py|>fN&eXSwc-McM2tt9}0t zP6z>j*joGhFx=f;F0cF~NzTY+V+$PB`e0zp{t1+}HE^EKQR-anI4!ZYgNM`f!?E$V zWHL+c%4T2wS}r$-jCegt^}ig38+Jy~`qN4cj?MWT6af0PJ{Vj3PoL7Zt2m$NQz{x6 zYYl;c-ZcHF8rSN&>%Z=KznOsU?` z+JE_!wl!A3**eZ|%Mo(>THpWPRK_v@@L{R+->xux@{J_Et{QH>zA#?t|WQut>NJZ|5GX*98WFaJe^i`LaC#tfW6ZwUDm+4toj>)rY@dQd8URjwl8aTfjfHA(M@yxxMWCrkh-N+#+rXGV*L82tQ@TI{?;qE?|6I__QYnA4+V`KH={R*&vt+G( zAqcj;T6s~Z+T<80?<<$PipEU3#&PE4Mi%|Z^*CcHV=FAckBr&d3;+s3G~Xp<*LP}; z699&-wVgrm(#|Mai;)}R3uF(aX?M|>q@MGeCQ~{(QVopdZxo_EPwD)AO8+sW)M)@< zx4Q2A%X7Jni))S(sN51A#p0Hoar^+r@pL(&)O!P3|65WzfBMJ|1^}Yc^{fU;ms>#h zD)l~F>3N+ku6xF&iHy;kgJ4s496vZ7U{!ru9~{v7%@mk_t>ZM-uDeFgCl-WQk^)VX zj#-6~F?vf7JpY$Ce&|AgRqZL4_iP;+YJVvR{#dqWrspZ;GaaWuiM6B9&=}nj1iyVP zjvu}lU{wP^@eg6RvLgt7HDFAs_8SE_#Ym@vTBpA&7N6^m<3}zQHb(l@DEi@x#p2UF zX*y6f2_cMvQgT}Bv@-}cyq+YFT_)hD-b|8Tz7hoMJ}#9G0|2p>lLzwo1AR(${WXrC zy4=88H49N_b=}Pq7@LCY9Ouz139D5jF!d{rv-^L`taK`wS3Ir&0000 C&jvUE literal 0 HcmV?d00001 diff --git a/static/js/announcements.js b/static/js/announcements.js index e791319..5d7a6d0 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -111,15 +111,15 @@ function createTabletOperator(matchSetup) { } function createUmpire(matchSetup) { - if (matchSetup.umpire_name && matchSetup.umpire_name != null) { - return ci18n('announcements:umpire') + normalizeNames(matchSetup.umpire_name); + if (matchSetup.umpire && matchSetup.umpire.name && matchSetup.umpire.name != null) { + return ci18n('announcements:umpire') + normalizeNames(matchSetup.umpire.name); } return null; } function createServiceJudge(matchSetup) { - if (matchSetup.service_judge_name && matchSetup.service_judge_name != null) { - return ci18n('announcements:service_judge') + normalizeNames(matchSetup.service_judge_name); + if (matchSetup.serviceJudge && matchSetup.service_judge.name && matchSetup.service_judge.name != null) { + return ci18n('announcements:service_judge') + normalizeNames(matchSetup.service_judge.name); } return null; } diff --git a/static/js/change.js b/static/js/change.js index d9641b0..1ea2881 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -171,7 +171,7 @@ function default_handler(rerender, special_funcs) { const u = utils.find(curt.umpires, m => m._id === umpire._id); if (u) { u.last_time_on_court_ts = umpire.last_time_on_court_ts; - u.firstName = umpire.firstName; + u.firstname = umpire.firstname; u.surname = umpire.surname; u.name = umpire.name; u.country = umpire.country; @@ -195,6 +195,21 @@ function default_handler(rerender, special_funcs) { change_score(c.val); // Most dialogs don't show any matches, so do not rerender break; + case 'update_display_setting': + const updated_setting = c.val.setting; + const s = utils.find(curt.displaysettings, m => m.id === updated_setting.id); + if(!s) { + curt.displaysettings.push(updated_setting); + curt.displaysettings.sort(utils.cmp_key('id')); + } + ctournament.update_general_displaysettings(c); + break; + case 'delete_display_setting': + const removed_setting_id = c.val.setting_id; + const rs = utils.find(curt.displaysettings, m => m.id === removed_setting_id); + curt.displaysettings.splice(curt.displaysettings.indexOf(rs), 1); + ctournament.update_general_displaysettings(c); + break; case 'display_status_changed': const display_setting = c.val.display_court_displaysetting; const d = utils.find(curt.displays, m => m.client_id === display_setting.client_id); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 09dafac..1fba2f3 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -124,6 +124,7 @@ var ci18n_de = { 'tournament:edit:ticker_enabled': 'Ticker aktivieren', 'tournament:edit:ticker_url': 'Ticker-Adresse:', 'tournament:edit:ticker_password': 'Ticker-Passwort:', + 'tournament:edit:general_displaysettings': 'Verwaltung der Displayeinstellungen:', 'tournament:edit:displays': 'Anzeigen administrieren:', 'tournament:edit:displays:hostname': 'Hostname', 'tournament:edit:displays:batterylevel': 'Akku', @@ -132,6 +133,7 @@ var ci18n_de = { 'tournament:edit:displays:num': 'Monitor', 'tournament:edit:displays:court': 'Feld', 'tournament:edit:displays:setting': 'Einstellung', + 'tournament:edit:displays:description': 'Beschreibung', 'tournament:edit:displays:onlinestatus': 'Status', 'tournament:edit:tablets': 'Tablets Einstellungen:', 'tournament:edit:ticker': 'Ticker Einstellungen:', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index bee532a..55afe71 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -125,6 +125,7 @@ var ci18n_en = { 'tournament:edit:normalizations:origin': 'Origin', 'tournament:edit:normalizations:replace': 'Replacement', 'tournament:edit:normalizations:language': 'Language', + 'tournament:edit:general_displaysettings': 'Manaage Display Settings:', 'tournament:edit:displays': 'Manage Displays:', 'tournament:edit:displays:hostname': 'Hostname', 'tournament:edit:displays:batterylevel': 'Battery', @@ -133,6 +134,7 @@ var ci18n_en = { 'tournament:edit:displays:num': 'Displaynumber', 'tournament:edit:displays:court': 'Ar Court', 'tournament:edit:displays:setting': 'Setting', + 'tournament:edit:displays:description': 'Description', 'tournament:edit:displays:onlinestatus': 'Status', 'tournament:edit:tablets': 'Tablets Settings:', 'tournament:edit:ticker': 'Ticker Settings:', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index c2a3499..687a12d 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -2,6 +2,12 @@ var cmatch = (function() { +var has_resize_event = false; +var scroll_timer = setTimeout(auto_scroll, 10000); +var resize_timer = false; + +var scroll_down = true; + const OVERRIDE_COLORS_KEYS = ['', 'bg']; function calc_score_str(match) { @@ -22,6 +28,141 @@ function calc_section(m) { return 'unassigned'; } +function auto_scroll(){ + const scroll_object = document.querySelectorAll('.main_upcoming'); + let new_top = 0; + let height = 0; + let child_higth = 0; + scroll_object.forEach((item) =>{ + + let old_top = 0; + if(item.style.top) { + old_top = parseInt(item.style.top); + } + + if(scroll_down) { + item.style.top = (old_top - 2)+'px'; + } else { + item.style.top = (old_top + 2)+'px'; + } + + new_top = parseInt(item.style.top); + + for (const child of item.children) { + child_higth += child.offsetHeight; + } + + height = item.offsetHeight; + }); + + let scroll_interval = 1; + if(new_top >= 0) { + scroll_interval = 15000; + scroll_down = true; + } else if (height >= child_higth) { + scroll_interval = 15000; + scroll_down = false; + } + + scroll_timer = setTimeout(auto_scroll, scroll_interval); +} + +function resize_table(resizable_rows, table_width_factor) { + resizable_rows.forEach((row) => { + row.fixed_width_elements.forEach((item, index) => { + auto_size(item, row.fixed_width[index]); + }); + }); + + resizable_rows.forEach((row) => { + let fixed_size = 0; + + for (const child of row.tr.children) { + if(!row.variable_width_elements.includes(child)) { + fixed_size += child.offsetWidth; + } + } + + let width_factor_sum = 0; + for(const width_factor of row.variable_width_factor) { + width_factor_sum += width_factor; + } + + row.variable_width_elements.forEach((item, index) => { + resizable_auto_size(item, row.variable_width_factor[index] / width_factor_sum, fixed_size, table_width_factor); + }); + + }); +} + + + +function resizable_auto_size(parrent_el, factor, fixed_size, table_width_factor) { + parrent_el.classList.add("auto_size_parrent"); + parrent_el.style.width = (table_width_factor * window.innerWidth - fixed_size) * factor + 'px'; + parrent_el.setAttribute('resize_factor', factor); + parrent_el.setAttribute('fixed_size', fixed_size); + parrent_el.setAttribute('table_width_factor', table_width_factor); + + auto_size(parrent_el, (table_width_factor * window.innerWidth - fixed_size) * factor); + + if(!has_resize_event) { + window.addEventListener('resize', (ev) => { + const resize_parrents = document.querySelectorAll('.auto_size_parrent'); + resize_parrents.forEach((item) => { + + const factor = item.getAttribute('resize_factor'); + const fixed_size = item.getAttribute('fixed_size'); + const table_width_factor = item.getAttribute('table_width_factor'); + + item.style.width = (table_width_factor * window.innerWidth - fixed_size) * factor + 'px'; + auto_size(item, (table_width_factor * window.innerWidth - fixed_size) * factor); + }); + }); + + has_resize_event = true; + } + +} + + +function auto_size(parrent_el, parrent_width) { + if(!parrent_width) { + parrent_width = parrent_el.clientWidth; + } + + var child_width = 0; + for(const child of parrent_el.children) { + child_width += child.offsetWidth + 10; + } + + for(const child of parrent_el.children) { + var style = window.getComputedStyle(child, null).getPropertyValue('font-size'); + var fontSize = parseFloat(style); + if(!child.hasAttribute('original_font_size')){ + child.setAttribute('original_font_size', fontSize); + } + + const originalFontSize = child.getAttribute('original_font_size'); + + child.style.fontSize = Math.min(((fontSize + 1) * parrent_width/child_width), originalFontSize) + 'px'; + } + + /* + if(!has_resize_event) { + window.addEventListener('resize', (ev) => { + const resize_parrents = document.querySelectorAll('.auto_size_parrent'); + resize_parrents.forEach((item) => { + auto_size(item); + }); + }); + + has_resize_event = true; + } + */ +} + + function render_match_table_header(table) { const thead = uiu.el(table, 'thead'); const title_tr = uiu.el(thead, 'tr'); @@ -38,7 +179,13 @@ function render_match_table_header(table) { uiu.el(title_tr, 'th', {}, ''); } -function render_match_row(tr, match, court, style, show_player_status, show_add_tabletoperator) { +function render_match_row(tr, match, court, style, show_player_status, show_add_tabletoperator) { + var resizable_elements = { tr: tr, + variable_width_elements : [], + variable_width_factor: [], + fixed_width_elements: [], + fixed_width: []}; + if(!match.setup.is_match) { return; } @@ -49,8 +196,8 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ const completeMatch = (match.setup.teams[0].players.length >= 1 && match.setup.teams[1].players.length >= 1); - if (style === 'default') { - if(!court && completeMatch){ + if (style === 'unasigned') { + if(completeMatch){ tr.setAttribute('draggable', 'true'); tr.addEventListener("dragstart", drag); tr.addEventListener("dragend", dragend); @@ -58,9 +205,9 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } } - if(! completeMatch) { - tr.classList.add('incomplete'); - } + //if(! completeMatch) { + // tr.classList.add('incomplete'); + //} const waitForMatchStart = match.setup.called_timestamp && ( match.network_score == undefined || @@ -70,7 +217,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ ); const activeMatch = court && match.btp_winner != undefined; const setup = match.setup; - if (style === 'default' || style === 'plain') { + if (style === 'default' || style === 'plain' || style === 'unasigned') { const actions_td = uiu.el(tr, 'td', 'actions'); create_match_button(actions_td, 'vlink match_edit_button', 'match:edit', on_edit_button_click, match._id); if(completeMatch) { @@ -83,7 +230,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ }); } - if (style === 'default') { + if (style === 'default' || style === 'unasigned') { const court_number_td = uiu.el(tr, 'td','court_number'); if(court) { uiu.el(court_number_td, 'span', 'court_history', court.num); @@ -91,34 +238,38 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ //uiu.el(tr, 'td', court ? 'court_history' : 'empty_court', court ? court.num : ''); } - if (style === 'plain') { + if (style === 'plain' || style === 'public') { const court_number_td = uiu.el(tr, "td", 'court_number'); uiu.el(court_number_td, "div", 'court_num', court.num); } - if (style === 'default' || style === 'plain') { + if (style === 'default' || style === 'plain' || style === 'unasigned') { const match_str = (setup.scheduled_time_str ? (setup.scheduled_time_str + ' ') : '') + (setup.match_name ? (setup.match_name + ' ') : '') + setup.event_name; uiu.el(tr, 'td', 'match_num', setup.match_num); - uiu.el(tr, 'td', 'match_properties', match_str); + const match_properties_td = uiu.el(tr, 'td', 'match_properties', match_str); + if(! completeMatch) { + match_properties_td.classList.add('incomplete'); + } } else if (style === 'upcoming') { - uiu.el(tr, 'td', { - style: 'min-width: 0.8em;' - }, court ? court.num : ''); - uiu.el(tr, 'td', { - style: 'color: #aaa;', - }, `#${setup.match_num}`); - uiu.el(tr, 'td', { - style: 'color: #aaa;', - }, setup.scheduled_time_str || ''); - uiu.el(tr, 'td', { - style: 'color: #aaa;', - }, setup.event_name); + const court_number_td = uiu.el(tr, 'td','court_number_upcoming'); + if(court) { + uiu.el(court_number_td, 'span', 'court_upcoming', court.num); + } + uiu.el(tr, 'td', 'match_number_upcoming', `#${setup.match_num}`); + uiu.el(tr, 'td', 'match_scheduled_upcoming', setup.scheduled_time_str || ''); + const event_td = uiu.el(tr, 'td', 'match_event_upcoming'); + uiu.el(event_td, 'span', 'match_event_upcoming', setup.event_name); } const players0 = uiu.el(tr, 'td', { - 'class': ((match.team1_won === true) ? 'match_team_won' : ''), + 'class': ((match.team1_won === true) ? 'match_team_won' : 'match_team1'), style: 'text-align: right;', }); - if (style === 'default' || style === 'plain') { + + if(setup.teams[0].players.length < 1) { + players0.classList.add('incomplete'); + } + + if (style === 'default' || style === 'plain' || style === 'unasigned') { if (show_add_tabletoperator) { if (setup.teams[0].players.length > 0) { create_match_button(players0, 'vlink tabletoperator_add_button', 'tabletoperator:add', on_add_to_tabletoperators_team_one_button_click, match._id); @@ -128,11 +279,16 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } } - render_players_el(players0, setup, 0, match, show_player_status); + render_players_el(players0, setup, 0, match, show_player_status, style); uiu.el(tr, 'td', 'match_vs', 'v'); const players1 = uiu.el(tr, 'td', ((match.team1_won === false) ? 'match_team_won ' : '') + 'match_team2'); - render_players_el(players1, setup, 1, match, show_player_status); - if (style === 'default' || style === 'plain') { + + if(setup.teams[1].players.length < 1) { + players1.classList.add('incomplete'); + } + + render_players_el(players1, setup, 1, match, show_player_status, style); + if (style === 'default' || style === 'plain' || style === 'unasigned') { if (show_add_tabletoperator) { if (setup.teams[1].players.length > 0) { create_match_button(players1, 'vlink tabletoperator_add_button', 'tabletoperator:add', on_add_to_tabletoperators_team_two_button_click, match._id); @@ -142,71 +298,121 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } } - - - const to_td = uiu.el(tr, 'td', 'umpire_and_tablet'); - if (style === 'default' || style === 'plain') { - if (setup.umpire_name) { - const umpire_span = uiu.el(to_td, 'span', 'person'); - uiu.el(umpire_span, 'div', 'umpire', ''); - uiu.el(umpire_span, 'span', 'name', setup.umpire_name); - if (style === 'default' || style === 'plain') { - create_match_button(umpire_span, 'vlink match_second_call_button', 'match:secondcallumpire', on_second_call_umpire_button_click, match._id); - } - if (setup.service_judge_name) { + if(style === 'public' || style === 'upcoming') { + + if(style === 'upcoming') { + players0.classList.add('match_team1_upcoming'); + players1.classList.add('match_team2_upcoming'); + } else { + players0.classList.add('match_team1_public'); + players1.classList.add('match_team2_public'); + } + + resizable_elements.variable_width_elements.push(players0); + resizable_elements.variable_width_factor.push(1); - const service_judge_span = uiu.el(to_td, 'span', 'person'); - uiu.el(service_judge_span, 'div', 'service_judge', ''); - uiu.el(service_judge_span, 'span', 'name', setup.service_judge_name); - if (style === 'default' || style === 'plain') { - create_match_button(service_judge_span, 'vlink match_second_call_button', 'match:secondcallservicejudge', on_second_call_servicejudge_button_click, match._id); + resizable_elements.variable_width_elements.push(players1); + resizable_elements.variable_width_factor.push(1); + } + + if(style != 'public') { + const to_td = uiu.el(tr, 'td', 'umpire_and_tablet'); + if (style === 'default' || style === 'plain' || style === 'unasigned') { + if (setup.umpire && setup.umpire.name) { + const umpire_span = uiu.el(to_td, 'span', 'person'); + uiu.el(umpire_span, 'div', 'umpire', ''); + uiu.el(umpire_span, 'span', 'name', setup.umpire.name); + if (style === 'default' || style === 'plain' || style === 'unasigned') { + create_match_button(umpire_span, 'vlink match_second_call_button', 'match:secondcallumpire', on_second_call_umpire_button_click, match._id); + } + if (setup.service_judge && setup.service_judge.name) { + + const service_judge_span = uiu.el(to_td, 'span', 'person'); + uiu.el(service_judge_span, 'div', 'service_judge', ''); + uiu.el(service_judge_span, 'span', 'name', setup.service_judge.name); + if (style === 'default' || style === 'plain' || style === 'unasigned') { + create_match_button(service_judge_span, 'vlink match_second_call_button', 'match:secondcallservicejudge', on_second_call_servicejudge_button_click, match._id); + } } } - } - if (setup.tabletoperators && setup.tabletoperators.length > 0) { - uiu.el(to_td, 'div', 'tablet', ''); - uiu.el(to_td, 'span', 'match_no_umpire', setup.tabletoperators[0].name ); - if (setup.tabletoperators.length > 1) { - uiu.el(to_td, 'span', 'match_no_umpire', ' \u200B/ '); - uiu.el(to_td, 'span', 'match_no_umpire', setup.tabletoperators[1].name ); + if (setup.tabletoperators && setup.tabletoperators.length > 0) { + const tablet_div = uiu.el(to_td, 'div', 'tablet_operator', ''); + uiu.el(tablet_div, 'div', 'tablet', ''); + + const operators_div = uiu.el(tablet_div, 'div', 'operators'); + const person_div = uiu.el(operators_div, 'div', 'person'); + uiu.el(person_div, 'span', 'match_no_umpire', setup.tabletoperators[0].name ); + + if (setup.tabletoperators.length > 1) { + uiu.el(person_div, 'span', 'match_no_umpire', ' \u200B/ '); + const person2_div = uiu.el(operators_div, 'div', 'person'); + uiu.el(person2_div, 'span', 'match_no_umpire', setup.tabletoperators[1].name ); + } + + if (style === 'default' || style === 'plain' || style === 'unasigned') { + create_match_button(tablet_div, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); + } } - if (style === 'default' || style === 'plain') { - create_match_button(to_td, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); + + if (!setup.umpire && (!setup.tabletoperators || setup.tabletoperators.length == 0)) { + const no_umpire_span = uiu.el(to_td, 'span', 'person'); + uiu.el(no_umpire_span, 'div', 'no_umpire', ''); + uiu.el(no_umpire_span, 'span', 'match_no_umpire', ci18n('No umpire')); + } + } else if(style === 'upcoming' && setup.highlight == 6) { + uiu.el(to_td, 'span', 'preperation', 'Spiel in Vorbereitung!'); } - - if (!setup.umpire_name && (!setup.tabletoperators || setup.tabletoperators.length == 0)) { - const no_umpire_span = uiu.el(to_td, 'span', 'person'); - uiu.el(no_umpire_span, 'div', 'no_umpire', ''); - uiu.el(no_umpire_span, 'span', 'match_no_umpire', ci18n('No umpire')); + } + + if(style != 'upcoming') { + const score_td = uiu.el(tr, 'td', 'score'); + if(style === 'public') { + score_td.classList.add('score_public'); } - } else if(style === 'upcoming' && setup.highlight == 6) { - uiu.el(to_td, 'span', 'preperation', 'Spiel in Vorbereitung!'); - } + + const score_span = uiu.el(score_td, 'span', { + 'class': ('match_score' + ((match.setup.now_on_court === true) ? ' match_score_current' : '')), + 'data-match_id': match._id, + }, calc_score_str(match)); + if(style === 'public' && calc_score_str(match) === '') { + if (setup.umpire && setup.umpire.firstname && setup.umpire.surname) { + const umpire_icon = uiu.el(score_span, 'div', 'umpire', ''); + const umpire_name_div = uiu.el(score_span, 'div', 'umpire_name_public'); + uiu.el(umpire_name_div, 'span', {}, short_name(setup.umpire.firstname, setup.umpire.surname)); + if (setup.service_judge && setup.service_judge.firstname && setup.service_judge.surname) { + uiu.el(umpire_name_div, 'span', {}, ' \u200B+ '); + uiu.el(umpire_name_div, 'span', {}, short_name(setup.service_judge.firstname, setup.service_judge.surname)); + } + + let parrent_width = score_span.clientWidth; + parrent_width -= parseFloat(window.getComputedStyle(score_span, null).getPropertyValue('padding-left')); + parrent_width -= parseFloat(window.getComputedStyle(score_span, null).getPropertyValue('padding-right')); + + //auto_size(umpire_name_div, parrent_width - umpire_icon.offsetWidth - 20); + resizable_elements.fixed_width_elements.push(umpire_name_div); + resizable_elements.fixed_width.push(parrent_width - umpire_icon.offsetWidth - 20 + 'px'); + + } else if (setup.tabletoperators && setup.tabletoperators.length > 0){ + const tablet_icon = uiu.el(score_span, 'div', 'tablet', ''); + const operators_div = uiu.el(score_span, 'div', 'operators_public') + uiu.el(operators_div, 'span', 'match_no_umpire', short_name(setup.tabletoperators[0].firstname, setup.tabletoperators[0].lastname, setup.tabletoperators[0].name)); + if (setup.tabletoperators.length > 1) { + uiu.el(operators_div, 'span', 'match_no_umpire', ' \u200B/ '); + uiu.el(operators_div, 'span', 'match_no_umpire', short_name(setup.tabletoperators[1].firstname, setup.tabletoperators[1].lastname, setup.tabletoperators[1].name)); + } + + let parrent_width = score_span.clientWidth; + parrent_width -= parseFloat(window.getComputedStyle(score_span, null).getPropertyValue('padding-left')); + parrent_width -= parseFloat(window.getComputedStyle(score_span, null).getPropertyValue('padding-right')); + + //auto_size(operators_div, parrent_width - tablet_icon.offsetWidth - 20); - const score_td = uiu.el(tr, 'td', 'score'); - const score_span = uiu.el(score_td, 'span', { - 'class': ('match_score' + ((match.setup.now_on_court === true) ? ' match_score_current' : '')), - 'data-match_id': match._id, - }, calc_score_str(match)); - - if(style === 'public' && calc_score_str(match) === '') { - if (setup.umpire_name) { - uiu.el(score_span, 'div', 'umpire', ''); - uiu.el(score_span, 'span', {}, setup.umpire_name); - if (setup.service_judge_name) { - uiu.el(score_span, 'span', {}, ' \u200B+ '); - uiu.el(score_span, 'span', {}, setup.service_judge_name); - } - } else if (setup.tabletoperators && setup.tabletoperators.length > 0){ - uiu.el(score_span, 'div', 'tablet', ''); - uiu.el(score_span, 'span', 'match_no_umpire', setup.tabletoperators[0].name ); - if (setup.tabletoperators.length > 1) { - uiu.el(score_span, 'span', 'match_no_umpire', ' \u200B/ '); - uiu.el(score_span, 'span', 'match_no_umpire', setup.tabletoperators[1].name ); + resizable_elements.fixed_width_elements.push(operators_div); + resizable_elements.fixed_width.push(parrent_width - tablet_icon.offsetWidth - 20 - 20 + 'px'); } } } @@ -233,7 +439,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } } - if ((style === 'default' || style === 'plain') && match.setup.now_on_court != false){ + if ((style === 'default' || style === 'plain' || style === 'unasigned')){ const timer_td = uiu.el(tr, 'td', {'class': 'match_timer', 'data-match_id': match._id}); var timer_state = _extract_match_timer_state(match); @@ -249,14 +455,12 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } } - if ((style === 'default' || style === 'plain') && match.setup.now_on_court != false) { + if (style === 'default' || style === 'unasigned') { const call_td = uiu.el(tr, 'td', 'call_td'); - if (!court) { - if(completeMatch) { - create_match_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); - } - } else { + if (style === 'unasigned' && completeMatch) { + create_match_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); + } else if (style === 'default' && court) { create_match_button(call_td, 'vlink match_manual_call_button', 'match:manualcall', on_announce_match_manually_button_click, match._id); create_match_button(call_td, 'vlink match_begin_to_play_button', 'match:begintoplay', on_begin_to_play_button_click, match._id); } @@ -285,7 +489,18 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } }); } + + return resizable_elements; +} + +function short_name (first_names, last_name, name) { + if(first_names && last_name){ + const split_name = first_names.split(" "); + return split_name[0][0] + '. ' + last_name; + } + return name; } + function create_match_button(targetEl, cssClass, title, listener, matchId,) { const btn = uiu.el(targetEl, 'div', { 'class': cssClass, @@ -348,7 +563,7 @@ function update_match_score(m) { }); } -function render_players_el(parentNode, setup, team_id, match, show_player_status) { +function render_players_el(parentNode, setup, team_id, match, show_player_status, style) { const team = setup.teams[team_id]; const nat0 = team.players[0] && team.players[0].nationality; @@ -357,7 +572,7 @@ function render_players_el(parentNode, setup, team_id, match, show_player_status } if (team.players.length > 0) { - render_player_el(parentNode, team.players[0], match._id, setup.now_on_court, show_player_status); + render_player_el(parentNode, team.players[0], match._id, setup.now_on_court, show_player_status, style, team.players.length > 1 ? true : false); } else { let dependency = '???'; @@ -426,27 +641,35 @@ function render_players_el(parentNode, setup, team_id, match, show_player_status cflags.render_flag_el(p1_el, nat1); } - render_player_el(parentNode, team.players[1], match._id, setup.now_on_court, show_player_status); + render_player_el(parentNode, team.players[1], match._id, setup.now_on_court, show_player_status, style, true); } } -function render_player_el(parentNode, player, match_id, now_on_court, show_player_status) { +function render_player_el(parentNode, player, match_id, now_on_court, show_player_status, style, is_doubles) { let player_status = get_player_status(player, now_on_court, show_player_status); + const player_name = (style === 'public' || style === 'upcoming' && is_doubles) ? short_name(player.firstname, player.lastname) : player.name; let player_element = uiu.el(parentNode, 'span', { - 'class' : 'player ' + player_status, + 'class' : 'person player ' + player_status + (style === 'public' || style === 'upcoming' ? '_public' : ''), 'data-btp_id' : player.btp_id, 'data-match_id': match_id, - }, player.name.replace(' ', '\xa0')); + }, player_name.replace(' ', '\xa0')); + + if(player.check_in_per_match) { + if(player_status == "checked_in") { + player_element.classList.add("can_check_out"); + } else if (player_status == "not_checked_in") { + player_element.classList.add("can_check_in"); + } + } player_element.addEventListener("click", (ev) => { if(curt.btp_settings.check_in_per_match) { - send({ type: 'match_player_check_in', match_id: ev.target.getAttribute("data-match_id"), player_id: ev.target.getAttribute("data-btp_id"), - checked_in: (ev.target.getAttribute("class") == "player not_checked_in" ? true : false), + checked_in: (ev.target.classList.contains('not_checked_in') ? true : false), tournament_key: curt.key }, function (err) { if (err) { @@ -457,8 +680,7 @@ function render_player_el(parentNode, player, match_id, now_on_court, show_playe }, false); - - if (player.now_playing_on_court && player_status != "now_on_court") { + if ((player.now_playing_on_court && player_status != "now_on_court") && player_status != "no_status") { let parts = player.now_playing_on_court.split("_"); let court_number = parts[parts.length - 1]; uiu.el(player_element, 'div', 'court', court_number); @@ -478,7 +700,9 @@ function render_player_el(parentNode, player, match_id, now_on_court, show_playe function get_player_status(player, now_on_court, show_player_status) { let player_status = ""; - if (!show_player_status || now_on_court) { + if (!show_player_status) { + player_status = "no_status"; + } else if(now_on_court) { player_status = "now_on_court"; } else if (player.now_playing_on_court) { player_status = "now_playing"; @@ -488,6 +712,8 @@ function get_player_status(player, now_on_court, show_player_status) { player_status = "not_checked_in"; } + + return player_status; } @@ -508,15 +734,22 @@ function update_player(match_id, player, now_on_court, show_player_status) { uiu.qsEach('.player[data-match_id=' + JSON.stringify(match_id) + '][data-btp_id="' + JSON.stringify(player.btp_id) + '"]' , function(player_el) { let player_status = get_player_status(player, now_on_court, show_player_status); - player_el.classList.remove("now_on_court", "now_playing", "checked_in", "not_checked_in"); + player_el.classList.remove("now_on_court", "now_playing", "checked_in", "not_checked_in", "no_status", "can_check_out", "can_check_in"); player_el.classList.add(player_status); + if(player.check_in_per_match) { + if(player_status == "checked_in") { + player_el.classList.add("can_check_out"); + } else if (player_status == "not_checked_in") { + player_el.classList.add("can_check_in"); + } + } //The only Child should be the now_playing_on_court icon or the now_tablet_on_court icon while (player_el.firstElementChild) { player_el.removeChild(player_el.lastElementChild); } - if (player.now_playing_on_court && player_status != "now_on_court") { + if ((player.now_playing_on_court && player_status != "now_on_court") && player_status != "no_status") { let parts = player.now_playing_on_court.split("_"); let court_number = parts[parts.length - 1]; uiu.el(player_el, 'div', 'court', court_number); @@ -537,6 +770,7 @@ function update_player(match_id, player, now_on_court, show_player_status) { } function remove_match_from_gui(m, old_section) { + switch (old_section) { case 'finished': case 'unassigned': @@ -545,14 +779,26 @@ function update_player(match_id, player, now_on_court, show_player_status) { }); break; default: - uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { - const court_number = match_row_el.getElementsByClassName('court_number')[0].children[0].innerHTML; - const c = { _id:m.setup.court_id, - num: court_number}; - - match_row_el.innerHTML = ""; - render_droppable_row(match_row_el, c, 'plain', true); - }); + const main_container = document.getElementsByClassName('main_upcoming'); + if (main_container.length > 0){ + uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { + const court_number = match_row_el.getElementsByClassName('court_number')[0].children[0].innerHTML; + const c = { _id:m.setup.court_id, + num: court_number}; + + match_row_el.innerHTML = ""; + render_empty_court_row(match_row_el, c, 'public', false); + }); + } else { + uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { + const court_number = match_row_el.getElementsByClassName('court_number')[0].children[0].innerHTML; + const c = { _id:m.setup.court_id, + num: court_number}; + + match_row_el.innerHTML = ""; + render_empty_court_row(match_row_el, c, 'plain', true); + }); + } break; } } @@ -589,7 +835,7 @@ function update_match(m, old_section, new_section) { case 'unassigned': uiu.qsEach( '.unassigned_container > table > tbody > .match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { match_row_el.setAttribute('class', 'match highlight_' + (m.setup.highlight ? m.setup.highlight : 0)); - render_match_row(match_row_el, m, null, 'default', true, true); + render_match_row(match_row_el, m, null, 'unasigned', true, true); }); break; default: @@ -911,9 +1157,17 @@ function _update_setup(setup, d) { result.court_id = d.court_id; result.now_on_court = !! d.now_on_court; - result.scheduled_time_str = d.scheduled_time_str; - result.umpire_name = d.umpire_name; - result.service_judge_name = d.service_judge_name; + + for (const u of curt.umpires) { + if (u.name === d.umpire_name) { + result.umpire = u; + } + + if (u.name === d.service_judge_name) { + result.service_judge = u; + } + } + result.override_colors = override_colors; return result; @@ -969,7 +1223,8 @@ function _cancel_ui_edit() { } cbts_utils.esc_stack_pop(); uiu.remove(dlg); - ctournament.ui_show(); + + crouting.set('t/:key/', { key: curt.key }); } function _delete_match_btn_click(e) { @@ -1053,7 +1308,6 @@ function ui_edit(match_id) { }, ci18n('Change')); form_utils.onsubmit(form, function(d) { - const setup = _make_setup(d); match.setup = _update_setup(match.setup, d); btn.setAttribute('disabled', 'disabled'); send({ @@ -1176,11 +1430,15 @@ crouting.register(/t\/([a-z0-9]+)\/m\/([-a-zA-Z0-9_ ]+)\/scoresheet$/, function( ui_scoresheet(match_id); })); -function render_match_table(container, matches, show_player_status, show_add_tabletoperator) { +function render_match_table(container, matches, style, show_player_status, show_add_tabletoperator) { if(!show_player_status) { show_player_status = false; } + + if(! style) { + style = 'default'; + } const table = uiu.el(container, 'table', 'match_table'); render_match_table_header(table, true); @@ -1189,7 +1447,7 @@ function render_match_table(container, matches, show_player_status, show_add_tab for (const m of matches) { if(m.setup.is_match) { const tr = uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); - render_match_row(tr, m, null, 'default', show_player_status, show_add_tabletoperator); + render_match_row(tr, m, null, style, show_player_status, show_add_tabletoperator); } } @@ -1200,26 +1458,32 @@ function render_unassigned(container) { uiu.el(container, 'h3', 'section', ci18n('Unassigned Matches')); const unassigned_matches = curt.matches.filter(m => calc_section(m) === 'unassigned'); - render_match_table(container, unassigned_matches, true, true); + render_match_table(container, unassigned_matches, 'unasigned', true, true); } function render_upcoming_matches(container) { - const UPCOMING_MATCH_COUNT = 10; + const UPCOMING_MATCH_COUNT = 13; uiu.empty(container); - uiu.el(container, 'h3', { + uiu.el(container, 'h2', { style: 'text-align: center;', - }, ci18n('Next Matches')); + }, ci18n('Next Matches')); const upcoming_table = uiu.el(container, 'table', 'upcoming_table'); + const upcoming_tbody = uiu.el(upcoming_table, 'tbody', 'upcoming_tbody'); const unassigned_matches = curt.matches.filter(m => calc_section(m) === 'unassigned'); + + + var resizable_rows = []; for (const match of unassigned_matches.slice(0, UPCOMING_MATCH_COUNT)) { - const tr = uiu.el(upcoming_table, 'tr', { + const tr = uiu.el(upcoming_tbody, 'tr', { style: 'padding-top: 1em;', }); - render_match_row(tr, match, null, 'upcoming'); + resizable_rows.push(render_match_row(tr, match, null, 'upcoming')); } + resize_table(resizable_rows, 0.98); + const qr = uiu.el(container, 'img', { type: 'img', id: 'main_q_code_upcoming', @@ -1233,14 +1497,18 @@ function render_finished(container) { uiu.el(container, 'h3', 'section', ci18n('Finished Matches')); const matches = curt.matches.filter(m => calc_section(m) === 'finished').sort((a, b) => {return b.end_ts - a.end_ts}); - render_match_table(container, matches, false, true); + render_match_table(container, matches, 'default', false, true); } function render_courts(container, style) { style = style || 'plain'; uiu.empty(container); + if(style === 'public') { + uiu.el(container, 'h2', {}, 'Aktuelle Spiele'); + } const table = uiu.el(container, 'table', 'match_table'); const tbody = uiu.el(table, 'tbody'); + var resizable_rows = []; for (const c of curt.courts) { const expected_section = 'court_' + c._id; const court_matches = curt.matches.filter(m => calc_section(m) === expected_section); @@ -1258,31 +1526,46 @@ function render_courts(container, style) { if (court_matches.length === 0) { - render_droppable_row(tr, c, style, true); + render_empty_court_row(tr, c, style, true); } else { let i = 0; for (const cm of court_matches) { const my_tr = (i > 0) ? uiu.el(tbody, 'tr') : tr; - render_match_row(my_tr, cm, c, style); + resizable_rows.push(render_match_row(my_tr, cm, c, style)); i++; } } } + + if(style === 'public') { + resize_table(resizable_rows, 0.98); + } } -function render_droppable_row(tr, court, style, is_droppable) { - const lead_target_td = uiu.el(tr, 'td', {class: "droppable", colspan: 1, "data-court_id":court._id}, ''); +function render_empty_court_row(tr, court, style, is_droppable) { + if(style != 'public') { + const lead_target_td = uiu.el(tr, 'td', {class: "droppable actions", colspan: 1, "data-court_id":court._id}, ''); - const court_number_td = uiu.el(tr, "td", {'class':('court_number', 'droppable'), "data-court_id":court._id}); + lead_target_td.addEventListener("drop", drop); + lead_target_td.addEventListener("dragover", allowDrop); + } + + let court_number_class = ('court_number'); + let empty_row_class = ('empty_element'); + + + const court_number_td = uiu.el(tr, "td", {'class':'court_number', "data-court_id":court._id}); uiu.el(court_number_td, "div", 'court_num', court.num); - const target_td = uiu.el(tr, 'td', {class: "droppable", colspan: 11, "data-court_id":court._id}, ''); + const target_td = uiu.el(tr, 'td', {class: 'empty_element', colspan: 11, "data-court_id":court._id}, ''); - lead_target_td.addEventListener("drop", drop); - lead_target_td.addEventListener("dragover", allowDrop); + if( style != 'public') { + court_number_td.classList.add('droppable'); + target_td.classList.add('droppable'); - target_td.addEventListener("drop", drop); - target_td.addEventListener("dragover", allowDrop); + target_td.addEventListener("drop", drop); + target_td.addEventListener("dragover", allowDrop); + } } @@ -1310,14 +1593,14 @@ function drag(ev) { ev.dataTransfer.setData('text', ev.target.getAttribute("data-match_id")); for (const dropp_row of document.getElementsByClassName ("droppable")) { - dropp_row.setAttribute("class", "droppable droppable_active"); + dropp_row.classList.add("droppable_active"); } } } function dragend(ev) { for (const dropp_row of document.getElementsByClassName ("droppable")) { - dropp_row.setAttribute("class", "droppable"); + dropp_row.classList.remove("droppable_active"); } } @@ -1645,7 +1928,7 @@ function render_edit(form, match) { name: 'umpire_name', size: 1, }); - render_umpire_options(umpire_select, setup.umpire_name); + render_umpire_options(umpire_select, (setup.umpire && setup.umpire.name) ? setup.umpire.name : ''); // Service judge uiu.el(tos_container, 'span', { @@ -1656,7 +1939,7 @@ function render_edit(form, match) { name: 'service_judge_name', size: 1, }); - render_umpire_options(service_judge_select, setup.service_judge_name, true); + render_umpire_options(service_judge_select, (setup.service_judge && setup.service_judge.name) ? setup.service_judge.name : '' , true); render_override_colors(edit_match_container, setup); } diff --git a/static/js/ctabletoperator.js b/static/js/ctabletoperator.js index d2e7838..b44082f 100644 --- a/static/js/ctabletoperator.js +++ b/static/js/ctabletoperator.js @@ -37,15 +37,24 @@ function render_tabletoperator_table_header(table) { function render_tabletoperator_row(tr, tabletoperator, is_fist_entry, is_last_entry) { const to = tabletoperator.tabletoperator; const to_td = uiu.el(tr, 'td'); - uiu.el(to_td, 'div', 'tablet', ''); - uiu.el(to_td, 'span', 'match_no_umpire', to[0].name); + const tablet_div = uiu.el(to_td, 'div', 'tablet_operator', ''); + uiu.el(tablet_div, 'div', 'tablet', ''); + const operators_div = uiu.el(tablet_div, 'div', 'operators'); + const person_div = uiu.el(operators_div, 'div', 'person'); + uiu.el(person_div, 'span', 'match_no_umpire', to[0].name); if (to.length > 1) { - uiu.el(to_td, 'span', 'match_no_umpire', ' \u200B/ '); - uiu.el(to_td, 'span', 'match_no_umpire', to[1].name); + + uiu.el(person_div, 'span', 'match_no_umpire', ' \u200B/ '); + const person2_div = uiu.el(operators_div, 'div', 'person'); + uiu.el(person2_div, 'span', 'match_no_umpire', to[1].name ); + } const court = curt.courts_by_id[tabletoperator.played_on_court]; - uiu.el(tr, 'td', court ? 'court_history' : '', court ? court.num : ''); + const court_td = uiu.el(tr, 'td', 'court_played'); + + + uiu.el(court_td, 'div', court ? 'court_history' : '', court ? court.num : ''); if (tabletoperator.court == null) { const buttonbar = uiu.el(tr, 'td'); diff --git a/static/js/ctournament.js b/static/js/ctournament.js index cd602f2..449e7a6 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -159,7 +159,6 @@ var ctournament = (function() { } function update_match(c) { const cval = c.val; - const match_id = cval.match__id; // Find the match @@ -194,7 +193,9 @@ var ctournament = (function() { return; } const old_section = cmatch.calc_section(m); - m.network_score = cval.match.network_score; + if(cval.match.network_score) { + m.network_score = cval.match.network_score; + } m.presses = cval.match.presses; m.team1_won = cval.match.team1_won; m.shuttle_count = cval.match.shuttle_count; @@ -203,8 +204,6 @@ var ctournament = (function() { const new_section = cmatch.calc_section(m); cmatch.update_match(m, old_section, new_section); - console.log(new_section); - if (old_section != new_section || new_section == 'unassigned') { uiu.qsEach('.upcoming_container', (upcoming_container) => { cmatch.render_upcoming_matches(upcoming_container); @@ -274,6 +273,11 @@ var ctournament = (function() { _show_render_tabletoperators(); } + + function _update_all_ui_elements_edit() { + update_general_displaysettings(uiu.qs('.general_displaysettings')); + } + function _update_all_ui_elements_upcoming() { cmatch.render_courts(uiu.qs('.courts_container'), 'public'); cmatch.render_upcoming_matches(uiu.qs('.upcoming_container')); @@ -293,6 +297,7 @@ var ctournament = (function() { } + function ui_btp_fetch() { send({ type: 'btp_fetch', @@ -981,10 +986,15 @@ var ctournament = (function() { render_courts(main); + + const general_displaysettings_div = uiu.el(main, 'div', 'general_displaysettings'); + render_general_displaysettings(general_displaysettings_div); render_displaysettings(main); render_normalisation_values(uiu.el(main, 'div','normalizations_values_div')); } - _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit); + _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit, change.default_handler(_update_all_ui_elements_edit, { + update_general_displaysettings: update_general_displaysettings, + })); function render_normalisation_values(main) { uiu.el(main, 'h2', {}, ci18n('tournament:edit:normalizations')); @@ -1074,10 +1084,65 @@ var ctournament = (function() { } } - function render_displaysettings(main) { - uiu.el(main, 'h2', {}, ci18n('tournament:edit:displays')); + function render_general_displaysettings(main) { + uiu.el(main, 'h2', {}, ci18n('tournament:edit:general_displaysettings')); + const display_settings_table = uiu.el(main, 'table'); + const display_settings_tbody = uiu.el(display_settings_table, 'tbody'); + const tr = uiu.el(display_settings_tbody, 'tr'); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:setting')); + uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:description')); + + for (const s of curt.displaysettings) { + const tr = uiu.el(display_settings_tbody, 'tr'); + uiu.el(tr, 'th', {}, s.id); + const description_td = uiu.el(tr, 'td', {}, '40" Fernseher (Platzhalter)'); + const actions_td = uiu.el(tr, 'td', {}); + const edit_btn = uiu.el(actions_td, 'button', { + 'data-display-setting-id': s.id, + }, 'Edit'); - const display_table = uiu.el(main, 'table'); + edit_btn.addEventListener('click', (e) => { + const edt_btn = e.target; + const display_setting_id = edt_btn.getAttribute('data-display-setting-id'); + + }); + + + const delete_btn = uiu.el(actions_td, 'button', { + 'data-display-setting-id': s.id, + }, 'Delete'); + + + + delete_btn.addEventListener('click', (e) => { + const del_btn = e.target; + const setting_id = del_btn.getAttribute('data-display-setting-id'); + + send({ + type: 'delete_display_setting', + tournament_key: curt.key, + setting_id: setting_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + + } + } + + function update_general_displaysettings(c) + { + const general_displaysettings_div = uiu.qs('.general_displaysettings') + general_displaysettings_div.innerHTML = ''; + render_general_displaysettings(general_displaysettings_div); + } + + function render_displaysettings(general_displaysettings_div) { + uiu.el(general_displaysettings_div, 'h2', {}, ci18n('tournament:edit:displays')); + + const display_table = uiu.el(general_displaysettings_div, 'table'); const display_tbody = uiu.el(display_table, 'tbody'); const tr = uiu.el(display_tbody, 'tr'); uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:num')); @@ -1349,6 +1414,7 @@ var ctournament = (function() { score: update_score, court_current_match: update_upcoming_current_match, match_edit: update_upcoming_match, + update_player_status: update_player_status, })); @@ -1533,7 +1599,7 @@ var ctournament = (function() { bts_status_changed, remove_normalization, add_normalization, - + update_general_displaysettings, }; })(); diff --git a/static/js/cumpires.js b/static/js/cumpires.js index ddb573d..de6f5d6 100644 --- a/static/js/cumpires.js +++ b/static/js/cumpires.js @@ -16,7 +16,7 @@ var cumpires = (function() { uiu.el(tr, 'td', { class: 'umpires_firstname', title: ci18n('umpires:btp_id', { btp_id: u.btp_id }), - }, u.firstName); + }, u.firstname); uiu.el(tr, 'td', { class: 'umpires_name', title: ci18n('umpires:btp_id', {btp_id: u.btp_id}), From 6fc4839a27e4f46aece4ace3f0911bdfa75810f4 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 31 Oct 2024 00:44:53 +0100 Subject: [PATCH 150/224] fix: it was not possible to remoch an umpire or service judge via match edit dialog --- static/js/cmatch.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 687a12d..dfe01f2 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -1158,6 +1158,14 @@ function _update_setup(setup, d) { result.court_id = d.court_id; result.now_on_court = !! d.now_on_court; + if(!d.umpire_name) { + delete result.umpire; + } + + if(!d.service_judge_name) { + delete result.service_judge; + } + for (const u of curt.umpires) { if (u.name === d.umpire_name) { result.umpire = u; From 794a558e0b7510ec3c597bf8605c8efd49170b6b Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 2 Nov 2024 14:50:45 +0100 Subject: [PATCH 151/224] Improve Admin View and Upcomming Matches: Button Begin to play was not Visible --- static/js/cmatch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index dfe01f2..119b105 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -455,12 +455,12 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } } - if (style === 'default' || style === 'unasigned') { + if (style === 'default' || style === 'plain' || style === 'unasigned') { const call_td = uiu.el(tr, 'td', 'call_td'); if (style === 'unasigned' && completeMatch) { create_match_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); - } else if (style === 'default' && court) { + } else if ((style === 'default' || style === 'plain') && court) { create_match_button(call_td, 'vlink match_manual_call_button', 'match:manualcall', on_announce_match_manually_button_click, match._id); create_match_button(call_td, 'vlink match_begin_to_play_button', 'match:begintoplay', on_begin_to_play_button_click, match._id); } From 4e33fa5e0c26e9bbbe72f3ca4fb1fbad98b388a0 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 2 Nov 2024 15:26:19 +0100 Subject: [PATCH 152/224] #65: Server.Core: Automatic call of a match which is in preparation when a match is finished. Handle Racecondition if two matched finishing simuntaniously --- bts/bupws.js | 4 +-- bts/match_utils.js | 65 ++++++++++++++++++++++++++++++---------------- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index 351ec2d..6258172 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -242,9 +242,7 @@ async function handle_score_update(app, ws, msg) { return cb(new Error('Cannot find match ' + JSON.stringify(match))); } if (finish_confirmed && match.team1_won != undefined && match.team1_won != null) { - match_utils.call_preparation_match_on_court(app, tournament_key, match.setup.court_id, (err) => { - - }); + const next_match = match_utils.call_preparation_match_on_court(app, tournament_key, match.setup.court_id); } return cb(null, match, changed_court); }, diff --git a/bts/match_utils.js b/bts/match_utils.js index f26b733..66242d4 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -793,30 +793,49 @@ function update_umpire(app, tkey, umpire, status, last_time_on_court_ts, court_i }); } -async function call_preparation_match_on_court(app, tournament_key, court_id, callback) { - app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { - if (err) { - return callback("No tournament found for "); - } - if (tournament.call_preparation_matches_automatically_enabled) { - const match_querry = { 'tournament_key': tournament_key, 'setup.highlight': 6 }; - app.db.matches.find(match_querry).sort({ 'setup.preparation_call_timestamp': 1 }).exec((err, matches) => { - if (err) { - return callback(msg, err); - } - if (matches && matches.length > 0) { - const next_match = matches[0]; - next_match.setup.court_id = court_id; - next_match.setup.now_on_court = true; - call_match(app, tournament, next_match, callback); +function serialized_call_preparation_match_on_court(fn) { + let queue = Promise.resolve(); + return (...args) => { + const res = queue.then(() => fn(...args)); + queue = res.catch(() => { }); + return res; + } +} - } else { - return callback("No match found to call on court."); - } - }); - } else { - return callback(null); - } +const call_preparation_match_on_court = serialized_call_preparation_match_on_court(call_preparation_match_on_court_async); + +function call_preparation_match_on_court_async(app, tournament_key, court_id) { + return new Promise((resolve, reject) => { + app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { + if (err) { + return reject("No tournament found for "); + } + if (tournament.call_preparation_matches_automatically_enabled) { + const match_querry = { 'tournament_key': tournament_key, 'setup.highlight': 6 }; + app.db.matches.find(match_querry).sort({ 'setup.preparation_call_timestamp': 1 }).exec((err, matches) => { + if (err) { + return reject(msg, err); + } + if (matches && matches.length > 0) { + const next_match = matches[0]; + next_match.setup.court_id = court_id; + next_match.setup.now_on_court = true; + call_match(app, tournament, next_match, (err) => { + if (err) { + return reject(err); + } else { + return resolve(next_match); + } + }); + + } else { + return reject("No match found to call on court."); + } + }); + } else { + return reject(null); + } + }); }); } From ed33d97220b6e762cb4ffc8fdad49ea7a41ebb1c Mon Sep 17 00:00:00 2001 From: tengmel Date: Thu, 7 Nov 2024 21:34:32 +0100 Subject: [PATCH 153/224] #65: Server.Core: Automatic call of a match which is in preparation when a match is finished. Handle Racecondition if btp sync is active and games will finshed --- bts/admin.js | 29 +++++++++++++++++++++-------- bts/btp_conn.js | 9 +++------ bts/btp_sync.js | 42 +++++++++++++++++++++++++----------------- bts/bupws.js | 7 +++++-- bts/match_utils.js | 14 ++------------ bts/update_queue.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 45 deletions(-) create mode 100644 bts/update_queue.js diff --git a/bts/admin.js b/bts/admin.js index f7f5ac2..e66837e 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -7,6 +7,7 @@ const uuidv4 = require('uuid/v4'); const {promisify} = require('util'); const btp_manager = require('./btp_manager'); +const update_queue = require('./update_queue'); const serror = require('./serror'); const stournament = require('./stournament'); const ticker_manager = require('./ticker_manager'); @@ -487,7 +488,6 @@ function handle_tabletoperator_add(app, ws, msg) { } function handle_match_call_on_court(app, ws, msg) { - const match_utils = require('./match_utils'); if (!_require_msg(ws, msg, ['tournament_key', 'court_id', 'match_id'])) { return; } @@ -495,23 +495,36 @@ function handle_match_call_on_court(app, ws, msg) { if (err) { return ws.respond(msg, err); } - app.db.matches.findOne({tournament_key: msg.tournament_key, _id: msg.match_id}, async (err, match) => { + const result = await update_queue.instance().execute(process_match,app, msg, tournament); + ws.respond(msg, result); + }); + +} + + +function process_match(app, msg, tournament) { + return new Promise((resolve, reject) => { + const match_utils = require('./match_utils'); + app.db.matches.findOne({ tournament_key: msg.tournament_key, _id: msg.match_id }, async (err, match) => { if (err) { - return ws.respond(msg, err); + reject(err); + return; } if (match != null) { match.setup.court_id = msg.court_id; match.setup.now_on_court = true; match_utils.call_match(app, tournament, match, (err, updated_match) => { - ws.respond(msg, err); - return; + if (err) { + reject(err); + } else { + resolve(updated_match); + } }); - }else { - return ws.respond(msg, "Match cannot be fetched from DB 222 " + msg.match_id); + } else { + reject(new Error("Match cannot be fetched from DB 222 " + msg.match_id)); } }); }); - } function handle_match_edit(app, ws, msg) { diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 59ee5d2..64e021a 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -7,6 +7,7 @@ const async = require('async'); const btp_proto = require('./btp_proto'); const btp_sync = require('./btp_sync'); +const update_queue = require('./update_queue'); const serror = require('./serror'); const AUTOFETCH_TIMEOUT = 30000; @@ -15,6 +16,7 @@ const WAIT_TIMEOUT = 10000; const BTP_PORT = 9901; const BLP_PORT = 9911; + function send_raw_request(ip, port, raw_req, callback) { assert(callback); if (!ip) { @@ -117,12 +119,7 @@ class BTPConn { fetch() { const ir = btp_proto.get_info_request(this.password); this.send(ir, response => { - btp_sync.fetch(this.app, this.tkey, response, (err) => { - if (err) { - this.report_status('error','Synchronisations-Fehler: ' + err.stack); - console.error(err.stack); - } - }); + update_queue.instance().execute(btp_sync.sync_btp_data,this.app, this.tkey, response); }); } diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 2bb293c..b4f6bb2 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -998,30 +998,38 @@ async function integrate_now_on_court(app, tkey, callback) { } -function fetch(app, tkey, response, callback) { - let btp_state; - try { - btp_state = btp_parse.get_btp_state(response); - } catch (e) { - return callback(e); - } +async function sync_btp_data(app, tkey, response) { + return new Promise((resolve, reject) => { + let btp_state; + try { + btp_state = btp_parse.get_btp_state(response); + } catch (e) { + return reject(e); + } - async.waterfall([ - cb => integrate_btp_settings(app, tkey, btp_state, cb), - cb => integrate_player_state(app, tkey, btp_state, cb), - cb => integrate_umpires(app, tkey, btp_state, cb), - cb => integrate_courts(app, tkey, btp_state, cb), - (court_map, cb) => integrate_matches(app, tkey, btp_state, court_map, cb), - cb => integrate_now_on_court(app, tkey, cb), - cb => cleanup_entities(app, tkey, btp_state, cb), - ], callback); + async.waterfall([ + cb => integrate_btp_settings(app, tkey, btp_state, cb), + cb => integrate_player_state(app, tkey, btp_state, cb), + cb => integrate_umpires(app, tkey, btp_state, cb), + cb => integrate_courts(app, tkey, btp_state, cb), + (court_map, cb) => integrate_matches(app, tkey, btp_state, court_map, cb), + cb => integrate_now_on_court(app, tkey, cb), + cb => cleanup_entities(app, tkey, btp_state, cb), + ], (err) => { + if (err) { + return reject(err); + } else { + return resolve(true); + } + }); + }); } module.exports = { calculate_match_ids_on_court, craft_match, date_str, - fetch, + sync_btp_data, time_str, // test only _integrate_umpires: integrate_umpires, diff --git a/bts/bupws.js b/bts/bupws.js index 6258172..459e4c6 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -8,7 +8,9 @@ const cp = require('child_process'); const os = require('os'); const btp_manager = require('./btp_manager'); +const btp_conn = require('./btp_conn'); const ticker_manager = require('./ticker_manager'); +const update_queue = require('./update_queue'); const stournament = require('./stournament'); const all_panels = []; @@ -155,7 +157,8 @@ async function handle_score_update(app, ws, msg) { update.btp_winner = (update.team1_won === true) ? 1 : 2; update.btp_needsync = true; update["setup.now_on_court"] = false; - match_utils.reset_player_tabletoperator(app, tournament_key, match_id, update.end_ts); + + await update_queue.instance().execute(match_utils.reset_player_tabletoperator,app, tournament_key, match_id, update.end_ts); } if (score_data.shuttle_count) { @@ -242,7 +245,7 @@ async function handle_score_update(app, ws, msg) { return cb(new Error('Cannot find match ' + JSON.stringify(match))); } if (finish_confirmed && match.team1_won != undefined && match.team1_won != null) { - const next_match = match_utils.call_preparation_match_on_court(app, tournament_key, match.setup.court_id); + const next_match = update_queue.instance().execute(match_utils.call_preparation_match_on_court,app, tournament_key, match.setup.court_id); } return cb(null, match, changed_court); }, diff --git a/bts/match_utils.js b/bts/match_utils.js index 66242d4..ec9897c 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -793,18 +793,8 @@ function update_umpire(app, tkey, umpire, status, last_time_on_court_ts, court_i }); } -function serialized_call_preparation_match_on_court(fn) { - let queue = Promise.resolve(); - return (...args) => { - const res = queue.then(() => fn(...args)); - queue = res.catch(() => { }); - return res; - } -} - -const call_preparation_match_on_court = serialized_call_preparation_match_on_court(call_preparation_match_on_court_async); -function call_preparation_match_on_court_async(app, tournament_key, court_id) { +function call_preparation_match_on_court(app, tournament_key, court_id) { return new Promise((resolve, reject) => { app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { if (err) { @@ -833,7 +823,7 @@ function call_preparation_match_on_court_async(app, tournament_key, court_id) { } }); } else { - return reject(null); + return resolve("Function call_preparation_matches_automatically_enabled disabled"); } }); }); diff --git a/bts/update_queue.js b/bts/update_queue.js new file mode 100644 index 0000000..342a7a1 --- /dev/null +++ b/bts/update_queue.js @@ -0,0 +1,42 @@ +'use strict'; + +class update_queue { + constructor() { + this.queue = []; + this.active = false; + } + + async process() { + if (this.active) { + return; + } + this.active = true; + while (this.queue.length > 0) { + const { task, args, resolve, reject } = this.queue.shift(); + try { + //console.log(`Execution of function: ${task.name}`); + const res = await task(...args); + resolve(res); + } catch (err) { + reject(err); + } + } + this.active = false; + } + + async execute(task, ...args) { + return new Promise((resolve, reject) => { + this.queue.push({ task, args, resolve, reject }); + this.process(); + }); + } +} +const update_queue_inst = new update_queue(); + +function instance() { + return update_queue_inst; +} + +module.exports = { + instance +}; From 9edf90d8e7506cee5aab08110267c95368dc9960 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 9 Nov 2024 11:44:41 +0100 Subject: [PATCH 154/224] #65: Server.Core: Automatic call of a match which is in preparation when a match is finished. handling of adding tabletoperator of finished matches to list was leading to race conditions --- bts/bupws.js | 21 ++++++++++++++++-- bts/match_utils.js | 55 ++++++++++++++++++++++++++++------------------ 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index 459e4c6..5bbc8d5 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -157,8 +157,6 @@ async function handle_score_update(app, ws, msg) { update.btp_winner = (update.team1_won === true) ? 1 : 2; update.btp_needsync = true; update["setup.now_on_court"] = false; - - await update_queue.instance().execute(match_utils.reset_player_tabletoperator,app, tournament_key, match_id, update.end_ts); } if (score_data.shuttle_count) { @@ -189,6 +187,25 @@ async function handle_score_update(app, ws, msg) { } cb(null, match); }, + (match, cb) => { + if (match) { + if (finish_confirmed) { + update_queue.instance().execute(match_utils.reset_player_tabletoperator, app, tournament_key, match_id, update.end_ts) + .then(() => { + cb(null, match); + }) + .catch((err) => { + console.error("Error in reset_player_tabletoperator:", err); + cb(null, match); + }); + + } else { + cb(null, match); + } + } else { + cb(null, match); + } + }, (match, cb) => db.courts.findOne(court_q, (err, court) => cb(err, match, court)), (match, court, cb) => { if (!match) { diff --git a/bts/match_utils.js b/bts/match_utils.js index ec9897c..e5d4c6e 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -414,27 +414,28 @@ function add_player_to_tabletoperator_list(app, tournament_key, cur_match_id, en if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { if (err) { - return reject(err); + return callback(err); } - add_player_to_tabletoperator_list_by_match(app, tournament, tournament_key, cur_match, end_ts) + add_player_to_tabletoperator_list_by_match(app, tournament, tournament_key, cur_match, end_ts, callback) }); + } else { + return callback(null); } - return callback(null); }); } -function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_key, cur_match, end_ts) { +function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_key, cur_match, end_ts, callback) { if (cur_match.network_score) { // walkovers and retirements will not be recorgnized. app.db.tabletoperators.findOne({ 'tournament_key': tournament_key, 'match_id': cur_match._id }, (err, no_tabletoperator) => { if (err) { - return reject(err); + return callback(err); } if (no_tabletoperator == null) { const round = cur_match.setup.match_name; var team = null; - if (tournament.tabletoperator_winner_of_quaterfinals_enabled && (round == 'VF' || round == 'QF')) { + if (tournament.tabletoperator_winner_of_quaterfinals_enabled && (round == 'VF' || round == 'QF')) { team = cur_match.setup.teams[cur_match.btp_winner - 1]; } else { const index = cur_match.btp_winner % 2; @@ -450,11 +451,11 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ var toinsert = player if (tournament.tabletoperator_with_state_enabled && player.state) { toinsert = create_team_from_player_state(player); - } + } var newTeam = { players: [toinsert] }; - teams.push(newTeam); + teams.push(newTeam); } } else { var toinsert = team; @@ -463,9 +464,10 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ players: [create_team_from_player_state(team.players[0])] }; } - teams.push(toinsert); + teams.push(toinsert); } + var i = 0; for (const t of teams) { var tabletoperator = []; t.players.forEach((player) => { @@ -484,17 +486,25 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ app.db.tabletoperators.insert(new_tabletoperator, function (err, inserted_t) { if (err) { - ws.respond(msg, err); - return; + return callback(err); } const admin = require('./admin'); // avoid dependency cycle admin.notify_change(app, tournament_key, 'tabletoperator_add', { tabletoperator: inserted_t }); + if (i == teams.length - 1) { + callback(null); + } + i++; }); } - + } else { + return callback(null); } + } else { + return callback(null); } }); + } else { + return callback(null); } } function fetch_match(app, tournament_key, match_id) { @@ -963,15 +973,18 @@ function update_btp_courts(app, tournament_key, match, callback) { }); } function reset_player_tabletoperator(app, tournament_key, match_id, end_ts) { - async.waterfall([ - cb => remove_player_on_court(app, tournament_key, match_id, end_ts, cb), - cb => remove_tablet_on_court(app, tournament_key, match_id, end_ts, cb), - cb => remove_umpire_on_court(app, tournament_key, match_id, end_ts, cb), - cb => add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts, cb) - ], function (err) { - if (err) { - return; - } + return new Promise((resolve, reject) => { + async.waterfall([ + cb => remove_player_on_court(app, tournament_key, match_id, end_ts, cb), + cb => remove_tablet_on_court(app, tournament_key, match_id, end_ts, cb), + cb => remove_umpire_on_court(app, tournament_key, match_id, end_ts, cb), + cb => add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts, cb) + ], function (err) { + if (err) { + return reject(err); + } + return resolve(null); + }); }); } From fd43f849ce03d5fc6f4dde27f7dbddac5ba5e697 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 9 Nov 2024 12:17:39 +0100 Subject: [PATCH 155/224] Merge branch 'feat/automaticCall' of github.com:tlehr/bts into feat/automaticCall: Bugfix: H2 was displayed white on white --- static/css/admin.css | 11 ++++++++++- static/js/ctournament.js | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/static/css/admin.css b/static/css/admin.css index 6450d2c..189b900 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -239,4 +239,13 @@ h2 { font-size: 70px; color: #fff; text-align: center; -} \ No newline at end of file +} + + h2.edit { + margin: 0; + padding-top: 20px; + padding-bottom: 10px; + font-size: 25px; + color: black; + text-align: left; + } \ No newline at end of file diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 449e7a6..7d3f104 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -35,7 +35,7 @@ var ctournament = (function() { uiu.empty(main); const form = uiu.el(main, 'form'); - uiu.el(form, 'h2', {}, ci18n('Create tournament')); + uiu.el(form, 'h2', 'edit', ci18n('Create tournament')); const id_label = uiu.el(form, 'label', {}, ci18n('create:id:label')); const key_input = uiu.el(id_label, 'input', { type: 'text', @@ -733,13 +733,13 @@ var ctournament = (function() { }; const bts_fieldset = uiu.el(form, 'fieldset'); - uiu.el(bts_fieldset, 'h2', {}, ci18n('tournament:edit:bts')); + uiu.el(bts_fieldset, 'h2', 'edit', ci18n('tournament:edit:bts')); create_checkbox(curt, bts_fieldset, 'call_preparation_matches_automatically_enabled'); create_checkbox(curt, bts_fieldset, 'call_next_possible_scheduled_match_in_preparation'); // BTP const btp_fieldset = uiu.el(form, 'fieldset'); - uiu.el(btp_fieldset, 'h2', {}, ci18n('tournament:edit:btp')); + uiu.el(btp_fieldset, 'h2', 'edit', ci18n('tournament:edit:btp')); const btp_enabled_label = uiu.el(btp_fieldset, 'label'); const ba_attrs = { type: 'checkbox', @@ -814,7 +814,7 @@ var ctournament = (function() { // Ticker const ticker_fieldset = uiu.el(form, 'fieldset'); - uiu.el(ticker_fieldset, 'h2', {}, ci18n('tournament:edit:ticker')); + uiu.el(ticker_fieldset, 'h2', 'edit', ci18n('tournament:edit:ticker')); const ticker_enabled_label = uiu.el(ticker_fieldset, 'label'); const te_attrs = { type: 'checkbox', @@ -845,7 +845,7 @@ var ctournament = (function() { const tablet_fieldset = uiu.el(form, 'fieldset'); - uiu.el(tablet_fieldset, 'h2', {}, ci18n('tournament:edit:tablets')); + uiu.el(tablet_fieldset, 'h2', 'edit', ci18n('tournament:edit:tablets')); create_checkbox(curt, tablet_fieldset, 'tabletoperator_enabled'); create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_umpire_enabled'); create_checkbox(curt, tablet_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled'); @@ -937,7 +937,7 @@ var ctournament = (function() { uiu.el(logo_preview_container, 'div', {}, 'Court 42'); } - uiu.el(main, 'h2', {}, ci18n('tournament:edit:logo')); + uiu.el(main, 'h2', 'edit', ci18n('tournament:edit:logo')); const logo_form = uiu.el(main, 'form'); const logo_button = uiu.el(logo_form, 'input', { type: 'file', @@ -997,7 +997,7 @@ var ctournament = (function() { })); function render_normalisation_values(main) { - uiu.el(main, 'h2', {}, ci18n('tournament:edit:normalizations')); + uiu.el(main, 'h2','edit', ci18n('tournament:edit:normalizations')); const display_table = uiu.el(main, 'table'); const display_tbody = uiu.el(display_table, 'tbody'); @@ -1085,7 +1085,7 @@ var ctournament = (function() { } function render_general_displaysettings(main) { - uiu.el(main, 'h2', {}, ci18n('tournament:edit:general_displaysettings')); + uiu.el(main, 'h2', 'edit', ci18n('tournament:edit:general_displaysettings')); const display_settings_table = uiu.el(main, 'table'); const display_settings_tbody = uiu.el(display_settings_table, 'tbody'); const tr = uiu.el(display_settings_tbody, 'tr'); @@ -1140,7 +1140,7 @@ var ctournament = (function() { } function render_displaysettings(general_displaysettings_div) { - uiu.el(general_displaysettings_div, 'h2', {}, ci18n('tournament:edit:displays')); + uiu.el(general_displaysettings_div, 'h2', 'edit', ci18n('tournament:edit:displays')); const display_table = uiu.el(general_displaysettings_div, 'table'); const display_tbody = uiu.el(display_table, 'tbody'); @@ -1197,7 +1197,7 @@ var ctournament = (function() { }); } function render_courts(main) { - uiu.el(main, 'h2', {}, ci18n('tournament:edit:courts')); + uiu.el(main, 'h2', 'edit', ci18n('tournament:edit:courts')); const courts_table = uiu.el(main, 'table'); const courts_tbody = uiu.el(courts_table, 'tbody'); From b849eb1b451cb02a3b548e513909ea98d2823fc5 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 15 Nov 2024 21:09:13 +0100 Subject: [PATCH 156/224] bugfix: after starting a new tournament it was possible that curt.btp_settings is not set when needed --- static/js/cmatch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 119b105..178c6b7 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -901,7 +901,7 @@ function _extract_player_timer_state(player) { s.settings.negative_timers = false; s.lang = "de"; s.timer = {}; - s.timer.duration = curt.btp_settings.pause_duration_ms; + s.timer.duration = (curt && curt.btp_settings && curt.btp_settings.pause_duration_ms) ? curt.btp_settings.pause_duration_ms : 0; s.timer.start = (player.last_time_on_court_ts ? player.last_time_on_court_ts : false); s.timer.upwards = false; s.timer.exigent = false; From 9e43a980d4992b7bdc20411a8d8126827315d63e Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Mon, 18 Nov 2024 15:38:05 +0100 Subject: [PATCH 157/224] fix: in admin.js the webserver sesponds with the result of an await. this was interpreted as error if the await responds success. --- bts/admin.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index e66837e..961238d 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -495,8 +495,12 @@ function handle_match_call_on_court(app, ws, msg) { if (err) { return ws.respond(msg, err); } - const result = await update_queue.instance().execute(process_match,app, msg, tournament); - ws.respond(msg, result); + + update_queue.instance().execute(process_match,app, msg, tournament).then(res => { + ws.respond(msg); + }).catch(err => { + ws.respond(msg, err); + }); }); } From a4d830df37cad3c233e2c815b4769e56ab830bc7 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Mon, 18 Nov 2024 15:48:28 +0100 Subject: [PATCH 158/224] #70: Fix that changes at the btp_settings are saved before games are imported. --- bts/btp_sync.js | 3 ++- static/js/change.js | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index b4f6bb2..69257cb 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -702,6 +702,7 @@ function integrate_courts(app, tournament_key, btp_state, callback) { } function integrate_btp_settings(app, tkey, btp_state, callback) { + const admin = require('./admin'); // avoid dependency cycle app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { if (err) return callback(err); @@ -746,7 +747,7 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { if (err) { return callback(err); } - + admin.notify_change(app, tkey, 'update_btp_settings', {btp_settings: toChange.btp_settings}); return callback(null); }); } else { diff --git a/static/js/change.js b/static/js/change.js index 1ea2881..02a0c91 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -195,6 +195,15 @@ function default_handler(rerender, special_funcs) { change_score(c.val); // Most dialogs don't show any matches, so do not rerender break; + case 'update_btp_settings': + if(!curt.btp_settings) { + curt.btp_settings = {}; + } + const btp_settings = c.val.btp_settings; + for (const [key, value] of Object.entries(btp_settings)) { + curt.btp_settings[key] = value; + } + break; case 'update_display_setting': const updated_setting = c.val.setting; const s = utils.find(curt.displaysettings, m => m.id === updated_setting.id); From e09c75bb1a37d7d0fbd4b3dd6455ed6bec8e321a Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Mon, 18 Nov 2024 16:03:26 +0100 Subject: [PATCH 159/224] #71: Server.Core / AdminPannel: Add new games at the rigth row of the table (before it wa sthe last row). If games are already running and then disciplines are (re)drawn, players who are on the field or operating the tablet are displayed correctly. --- bts/btp_sync.js | 45 ++++++--- bts/match_utils.js | 208 +++++++++++++++++++++------------------ static/js/change.js | 11 ++- static/js/cmatch.js | 117 +++++++++++++++++----- static/js/ctournament.js | 9 ++ 5 files changed, 254 insertions(+), 136 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 69257cb..2fcd592 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -401,12 +401,16 @@ function get_umpire(app, tkey, umpires , btp_id) { async function integrate_matches(app, tkey, btp_state, court_map, callback) { const admin = require('./admin'); // avoid dependency cycle + const match_utils = require('./match_utils'); const { draws, events } = btp_state; const match_ids_on_court = calculate_match_ids_on_court(btp_state); const officials = await get_umpires(app, tkey); + const matches_to_add = []; + const matches_on_court = []; + async.each(btp_state.matches, function (bm, cb) { const draw = draws.get(bm.DrawID[0]); assert(draw); @@ -535,6 +539,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { if (match.setup.now_on_court === true) { match.setup.state = 'oncourt'; + matches_on_court.push(match); } for (let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { @@ -608,16 +613,9 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { return; } - app.db.matches.insert(match, function(err) { - if (err) { - cb(null); - return; - } - - admin.notify_change(app, tkey, 'match_add', { match }); - cb(null) - return; - }); + matches_to_add.push(match); + cb(null) + return; }, error => { cb(null); return; @@ -625,8 +623,31 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { }); }, (error) => { if (error) { - console.log(error); + console.error(error); } + + matches_to_add.forEach((match_to_add) => { + let match = match_to_add; + matches_on_court.forEach((match_on_court) => { + const changed_match_on_court = match_utils.calc_match_set_player_on_court(match, match_on_court.setup); + if(changed_match_on_court != null) { + match = changed_match_on_court; + } + const changed_match_tablet_operator = match_utils.calc_match_set_player_on_tablet(match, match_on_court.setup); + if(changed_match_tablet_operator != null) { + match = changed_match_tablet_operator; + } + }); + + app.db.matches.insert(match, function(err) { + if (err) { + console.error(err); + } + + admin.notify_change(app, tkey, 'match_add', { match }); + }); + }); + callback(null); }); } @@ -718,8 +739,6 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { const check_in_per_match = btp_state.btp_settings.get(1003).Value[0] ? false : true; const pause_duration_ms = btp_state.btp_settings.get(1303).Value[0] * 60 * 1000; - - if (tournament.btp_settings.tournament_name != tournament_name) { tournament.btp_settings.tournament_name = tournament_name; changed = true; diff --git a/bts/match_utils.js b/bts/match_utils.js index e5d4c6e..b976346 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -269,6 +269,54 @@ function get_last_looser_on_court(admin, app, tkey, court_id) { }); } +function calc_match_set_player_on_tablet(match, match_on_court_setup) { + if(match.setup.now_on_court == false) { + return null; + } + + if(!match_on_court_setup.tabletoperators || match_on_court_setup.tabletoperators.length <1) { + return null; + } + + let tablet_operatorns_btp_ids = [match_on_court_setup.tabletoperators[0].btp_id]; + + if(match_on_court_setup.tabletoperators.length > 1) { + tablet_operatorns_btp_ids.push(match_on_court_setup.tabletoperators[1].btp_id); + } + + let change = false; + + if (match.setup.teams[0].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + match.setup.teams[0].players[0].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[0].checked_in = false; + change = true; + } + + if (match.setup.teams[0].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + match.setup.teams[0].players[1].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[1].checked_in = false; + change = true; + } + + if (match.setup.teams[1].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + match.setup.teams[1].players[0].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[0].checked_in = false; + change = true; + } + + if (match.setup.teams[1].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + match.setup.teams[1].players[1].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[1].checked_in = false; + change = true; + } + + if (change) { + return match; + } + + return null; +} + function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { if(!match_on_court_setup.tabletoperators || match_on_court_setup.tabletoperators.length == 0) { @@ -282,51 +330,15 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { } async.each(matches, async (match, cb) => { - if(match.setup.now_on_court == false) { - return; - } - - const match_id = match._id; - let tablet_operatorns_btp_ids = [match_on_court_setup.tabletoperators[0].btp_id]; - - if(match_on_court_setup.tabletoperators.length > 1) { - tablet_operatorns_btp_ids.push(match_on_court_setup.tabletoperators[1].btp_id); - } - - let change = false; - - if (match.setup.teams[0].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { - match.setup.teams[0].players[0].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[0].checked_in = false; - change = true; - } - - if (match.setup.teams[0].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { - match.setup.teams[0].players[1].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[1].checked_in = false; - change = true; - } - - if (match.setup.teams[1].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { - match.setup.teams[1].players[0].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[0].checked_in = false; - change = true; - } - - if (match.setup.teams[1].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { - match.setup.teams[1].players[1].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[1].checked_in = false; - change = true; - } - - if (change) { - const setup = match.setup; - const match_q = {_id: match_id}; + const changed_match = calc_match_set_player_on_tablet(match, match_on_court_setup) + if (changed_match != null) { + const setup = changed_match.setup; + const match_q = {_id: changed_match._id}; app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { if (err) return callback(err); - admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); + admin.notify_change(app, changed_match.tournament_key, 'update_player_status', {match__id: changed_match._id, + btp_winner: changed_match.btp_winner, + setup: changed_match.setup}); }); } }); @@ -335,6 +347,56 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { }); } +function calc_match_set_player_on_court(match, match_on_court_setup) { + if(match.setup.now_on_court == false) { + return null; + } + + let on_court_btp_ids = [match_on_court_setup.teams[0].players[0].btp_id, + match_on_court_setup.teams[1].players[0].btp_id]; + + if(match_on_court_setup.teams[0].players.length > 1) { + on_court_btp_ids.push(match_on_court_setup.teams[0].players[1].btp_id); + } + + if(match_on_court_setup.teams[1].players.length > 1) { + on_court_btp_ids.push(match_on_court_setup.teams[1].players[1].btp_id); + } + + let change = false; + + if (match.setup.teams[0].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + match.setup.teams[0].players[0].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[0].tablet_break_active = false; + match.setup.state = 'blocked'; + change = true; + } + + if (match.setup.teams[0].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + match.setup.teams[0].players[1].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[1].tablet_break_active = false; + match.setup.state = 'blocked'; + change = true; + } + + if (match.setup.teams[1].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + match.setup.teams[1].players[0].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[0].tablet_break_active = false; + match.setup.state = 'blocked'; + change = true; + } + + if (match.setup.teams[1].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + match.setup.teams[1].players[1].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[1].tablet_break_active = false; + match.setup.state = 'blocked'; + change = true; + } + if (change) { + return match; + } + return null; +} function set_player_on_court (app, tkey, match_on_court_setup, callback) { const admin = require('./admin'); // avoid dependency cycle @@ -344,60 +406,16 @@ function set_player_on_court (app, tkey, match_on_court_setup, callback) { } async.each(matches, async (match) => { - if(match.setup.now_on_court == false) { - return; - } - - const match_id = match._id; - let on_court_btp_ids = [match_on_court_setup.teams[0].players[0].btp_id, - match_on_court_setup.teams[1].players[0].btp_id]; - - if(match_on_court_setup.teams[0].players.length > 1) { - on_court_btp_ids.push(match_on_court_setup.teams[0].players[1].btp_id); - } - - if(match_on_court_setup.teams[1].players.length > 1) { - on_court_btp_ids.push(match_on_court_setup.teams[1].players[1].btp_id); - } - - let change = false; - - if (match.setup.teams[0].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { - match.setup.teams[0].players[0].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[0].tablet_break_active = false; - match.setup.state = 'blocked'; - change = true; - } - - if (match.setup.teams[0].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { - match.setup.teams[0].players[1].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[1].tablet_break_active = false; - match.setup.state = 'blocked'; - change = true; - } - - if (match.setup.teams[1].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { - match.setup.teams[1].players[0].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[0].tablet_break_active = false; - match.setup.state = 'blocked'; - change = true; - } - - if (match.setup.teams[1].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { - match.setup.teams[1].players[1].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[1].tablet_break_active = false; - match.setup.state = 'blocked'; - change = true; - } - if (change) { - const setup = match.setup; - const match_q = {_id: match_id}; + const changed_match = calc_match_set_player_on_court(match, match_on_court_setup); + if (changed_match != null) { + const setup = changed_match.setup; + const match_q = {_id: changed_match._id}; app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { if (err) return callback(err); - admin.notify_change(app, match.tournament_key, 'update_player_status', {match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup}); + admin.notify_change(app, changed_match.tournament_key, 'update_player_status', {match__id: changed_match._id, + btp_winner: changed_match.btp_winner, + setup: changed_match.setup}); }); } }); @@ -991,6 +1009,8 @@ function reset_player_tabletoperator(app, tournament_key, match_id, end_ts) { module.exports ={ add_player_to_tabletoperator_list, call_match, + calc_match_set_player_on_court, + calc_match_set_player_on_tablet, match_update, uncall_match, fetch_match, diff --git a/static/js/change.js b/static/js/change.js index 02a0c91..9309b09 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -99,8 +99,15 @@ function default_handler(rerender, special_funcs) { ctournament.update_match(c); break; case 'match_add': - curt.matches.push(c.val.match); - rerender(); + const match_id = c.val.match__id; + // Find the match + const m = utils.find(curt.matches, m => m._id === match_id); + if (!m) { + ctournament.add_match(c); + curt.matches.push(c.val.match); + } else { + ctournament.add_match(c); + } break; case 'match_delete': { diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 178c6b7..904a253 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -240,6 +240,8 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ if (style === 'plain' || style === 'public') { const court_number_td = uiu.el(tr, "td", 'court_number'); + if(!court) + console.warn('no court'); uiu.el(court_number_td, "div", 'court_num', court.num); } @@ -802,6 +804,62 @@ function update_player(match_id, player, now_on_court, show_player_status) { break; } } + +function add_match(m, section) { + console.log('in cmatch'); + console.log(section); + + switch (section) { + case 'finished': + uiu.qsEach('.finished_container', (finished_container) => { + const tbody = finished_container.querySelector('.match_table > tbody'); + const match_row_el = uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); + render_match_row(match_row_el, m, null, 'default', false, true); + for (const child of tbody.children) { + const child_btp_id = child.dataset.match_id; + const child_match = utils.find(curt.matches, m => 'btp_'+m.btp_id === child_btp_id); + if(child_match) { + if(cmp_match_order(m, child_match) < 0) { + tbody.insertBefore(match_row_el, child); + break; + } + } + } + }); + break; + case 'unassigned': + uiu.qsEach('.unassigned_container', (unassigned_container) => { + const tbody = unassigned_container.querySelector('.match_table > tbody'); + const match_row_el = uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); + render_match_row(match_row_el, m, null, 'unasigned', true, true); + for (const child of tbody.children) { + const child_btp_id = child.dataset.match_id; + const child_match = utils.find(curt.matches, m => 'btp_'+m.btp_id === child_btp_id); + if(child_match) { + if(cmp_match_order(m, child_match) < 0) { + tbody.insertBefore(match_row_el, child); + break; + } + } + } + }); + break; + default: + const court = utils.find(curt.courts, c => c._id === m.setup.court_id); + console.log(court); + uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { + match_row_el.innerHTML = ""; + const closest = match_row_el.closest('.main_upcoming'); + if(Boolean(closest)) { + render_match_row(match_row_el, m, court, 'public'); + } else { + render_match_row(match_row_el, m, court, 'plain', false, false); + } + }); + break; + } +} + function update_match(m, old_section, new_section) { if(old_section != new_section) { remove_match_from_gui(m, old_section); @@ -839,13 +897,14 @@ function update_match(m, old_section, new_section) { }); break; default: + const court = utils.find(curt.courts, c => c._id === m.setup.court_id); uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { match_row_el.innerHTML = ""; const closest = match_row_el.closest('.main_upcoming'); if(Boolean(closest)) { - render_match_row(match_row_el, m, null, 'public'); + render_match_row(match_row_el, m, court, 'public'); } else { - render_match_row(match_row_el, m, null, 'plain', false, false); + render_match_row(match_row_el, m, court, 'plain', false, false); } }); break; @@ -941,36 +1000,38 @@ function _extract_match_timer_state(match) { return rs; } -function prepare_render(t) { - t.matches.sort(function(m1, m2) { - const time_str1 = m1.setup.scheduled_time_str; - const time_str2 = m2.setup.scheduled_time_str; - - if (time_str1 && !time_str2) { - return -1; - } else if (time_str2 && !time_str1) { - return 1; - } +function cmp_match_order(m1, m2) { + const time_str1 = m1.setup.scheduled_time_str; + const time_str2 = m2.setup.scheduled_time_str; - const cmp1 = cbts_utils.cmp(m1.setup.scheduled_date, m2.setup.scheduled_date); - if (cmp1 != 0) return cmp1; + if (time_str1 && !time_str2) { + return -1; + } else if (time_str2 && !time_str1) { + return 1; + } - if (time_str1 === '00:00' && time_str2 !== '00:00') { - return 1; - } else if (time_str2 === '00:00' && time_str1 !== '00:00') { - return -1; - } + const cmp1 = cbts_utils.cmp(m1.setup.scheduled_date, m2.setup.scheduled_date); + if (cmp1 != 0) return cmp1; - const cmp2 = cbts_utils.cmp(time_str1, time_str2); - if (cmp2 != 0) return cmp2; + if (time_str1 === '00:00' && time_str2 !== '00:00') { + return 1; + } else if (time_str2 === '00:00' && time_str1 !== '00:00') { + return -1; + } - if ((m1.match_order !== undefined) && (m2.match_order !== undefined)) { - const cmp_result = cbts_utils.cmp(m1.match_order, m2.match_order); - if (cmp_result != 0) return cmp_result; - } + const cmp2 = cbts_utils.cmp(time_str1, time_str2); + if (cmp2 != 0) return cmp2; - return cbts_utils.cmp(m1.setup.match_num, m2.setup.match_num); - }); + if ((m1.match_order !== undefined) && (m2.match_order !== undefined)) { + const cmp_result = cbts_utils.cmp(m1.match_order, m2.match_order); + if (cmp_result != 0) return cmp_result; + } + + return cbts_utils.cmp(m1.setup.match_num, m2.setup.match_num); +} + +function prepare_render(t) { + t.matches.sort((m1, m2) => {return cmp_match_order(m1, m2)}); t.courts_by_id = {}; for (const c of t.courts) { @@ -2062,7 +2123,9 @@ function render_create(container) { } return { + add_match, calc_section, + cmp_match_order, prepare_render, render_create, render_finished, diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 7d3f104..00ca622 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -157,6 +157,14 @@ var ctournament = (function() { cmatch.remove_match_from_gui(m, section); } + + function add_match(c){ + const cval = c.val; + const m = cval.match; + const new_section = cmatch.calc_section(m); + cmatch.add_match(m, new_section); + } + function update_match(c) { const cval = c.val; const match_id = cval.match__id; @@ -1591,6 +1599,7 @@ var ctournament = (function() { switch_tournament, ui_show, ui_list, + add_match, update_match, update_upcoming_match, update_display, From d468f3a72a50d1047d0d55be1834bbceebd06ea8 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Mon, 18 Nov 2024 16:26:52 +0100 Subject: [PATCH 160/224] #72: Server.Core / AdminPannel: Matches can be moved to a new field if necessary. --- bts/admin.js | 21 ++++++++++++----- bts/btp_sync.js | 2 +- bts/match_utils.js | 50 ++++++++++++++++++++++++++++++++-------- static/js/change.js | 1 + static/js/cmatch.js | 18 ++++++++++----- static/js/ctournament.js | 3 ++- 6 files changed, 72 insertions(+), 23 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 961238d..34191b7 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -517,7 +517,7 @@ function process_match(app, msg, tournament) { if (match != null) { match.setup.court_id = msg.court_id; match.setup.now_on_court = true; - match_utils.call_match(app, tournament, match, (err, updated_match) => { + match_utils.call_match(app, tournament, match, undefined, (err, updated_match) => { if (err) { reject(err); } else { @@ -534,7 +534,7 @@ function process_match(app, msg, tournament) { function handle_match_edit(app, ws, msg) { const match_utils = require('./match_utils'); - if (!_require_msg(ws, msg, ['tournament_key', 'id', 'match'])) { + if (!_require_msg(ws, msg, ['tournament_key', 'id', 'match', 'old_court'])) { return; } const tournament_key = msg.tournament_key; @@ -546,17 +546,26 @@ function handle_match_edit(app, ws, msg) { } if(setup.now_on_court && !setup.called_timestamp) { - match_utils.call_match(app, tournament, msg.match, (err, match) => { + match_utils.call_match(app, tournament, msg.match, msg.old_court, (err, match) => { ws.respond(msg, err); return; }); - } else if (!setup.now_on_court && setup.called_timestamp) { - match_utils.uncall_match(app, tournament, msg.match, (err) => { + } + else if(setup.now_on_court && setup.court_id) { + match_utils.switch_court(app, tournament, msg.match, msg.old_court, (err, match) => { + ws.respond(msg, err); + return; + }); + } + else if (!setup.now_on_court && setup.called_timestamp) { + match_utils.uncall_match(app, tournament, msg.match, msg.old_court, (err) => { ws.respond(msg, err); return; }); } else { + console.log("ELSE"); + // TODO get old setup, make sure no key has been removed app.db.matches.update({_id: msg.id, tournament_key}, {$set: {setup}}, {returnUpdatedDocs: true}, function(err, numAffected, changed_match) { if (err) { @@ -573,7 +582,7 @@ function handle_match_edit(app, ws, msg) { ws.respond(msg, new Error(errmsg)); return; } - + console.log(changed_match); notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, match: changed_match}); if (msg.btp_update) { btp_manager.update_score(app, changed_match); diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 2fcd592..8c2e561 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -1006,7 +1006,7 @@ async function integrate_now_on_court(app, tkey, callback) { const setup = match.setup; if(!setup.called_timestamp) { - match_utils.call_match(app, tournament, match, (err) => { + match_utils.call_match(app, tournament, match, undefined, (err) => { if (err) console.log(err); }); } diff --git a/bts/match_utils.js b/bts/match_utils.js index b976346..4eabf32 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -3,12 +3,12 @@ const assert = require('assert'); const async = require('async'); -async function match_update(app, match, callback) { +async function match_update(app, match, old_court, callback) { async.waterfall([ (wcb) => update_match_btp(app, match, wcb), (wcb) => update_match_db(app, match, wcb), (wcb) => notify_change_match_edit(app, match, wcb), - (wcb) => notify_bupws(app, match, wcb), + (wcb) => notify_bupws(app, match, old_court, wcb), ], (err) => { return callback(err); @@ -16,7 +16,7 @@ async function match_update(app, match, callback) { ); } -async function uncall_match(app, tournament, match, callback) { +async function uncall_match(app, tournament, match, old_court, callback) { // Imports // Requrements @@ -28,7 +28,7 @@ async function uncall_match(app, tournament, match, callback) { (wcb) => update_match_db(app, match, wcb), (wcb) => update_court_db(app, match, wcb), (wcb) => notify_change_match_edit(app, match, wcb), - (wcb) => notify_bupws(app, match, wcb), + (wcb) => notify_bupws(app, match, old_court, wcb), (wcb) => remove_player_on_court(app, tournament.key, match._id, null, wcb), (wcb) => update_btp_courts(app, tournament.key, match, wcb)], (err) => { @@ -38,7 +38,7 @@ async function uncall_match(app, tournament, match, callback) { } -async function call_match(app, tournament, match, callback) { +async function call_match(app, tournament, match, old_court, callback) { if (!match.setup.court_id || !match._id) { return; // TODO in async we would assert both to be true } @@ -54,7 +54,7 @@ async function call_match(app, tournament, match, callback) { (wcb) => update_court_db(app, match, wcb), (wcb) => notify_change_match_edit(app, match, wcb), (wcb) => notify_change_match_called_on_court(app, match, wcb), - (wcb) => notify_bupws(app, match, wcb), + (wcb) => notify_bupws(app, match, old_court, wcb), (wcb) => set_player_on_court(app, tournament.key, match.setup, wcb), (wcb) => set_player_on_tablet(app, tournament.key, match.setup, wcb), (wcb) => update_btp_courts(app, tournament.key, match, wcb), @@ -65,6 +65,33 @@ async function call_match(app, tournament, match, callback) { ); } +async function switch_court(app, tournament, match, old_court, callback) { + if (!match.setup.court_id || !match._id) { + return; // TODO in async we would assert both to be true + } + if (match_completly_initialized(match.setup) == false) { + return callback("Match cannot be called one or more Teams are not set."); + } + async.waterfall([ + (wcb) => add_tabletoperators(app, tournament, match, wcb), + (wcb) => set_umpires_on_court(app, tournament, match, wcb), + (wcb) => remove_highlight_preperation(match, wcb), + (wcb) => update_match_btp(app, match, wcb), + (wcb) => update_match_db(app, match, wcb), + (wcb) => update_court_db(app, match, wcb), + (wcb) => notify_change_match_edit(app, match, wcb), + (wcb) => notify_change_match_called_on_court(app, match, wcb), + (wcb) => notify_bupws(app, match, old_court, wcb), + (wcb) => set_player_on_court(app, tournament.key, match.setup, wcb), + (wcb) => set_player_on_tablet(app, tournament.key, match.setup, wcb), + (wcb) => update_btp_courts(app, tournament.key, match, wcb) + ], + (err) => { + return callback(err, match); + } + ); +} + function match_completly_initialized(setup) { if (setup.teams[0].players.length == 0 || setup.teams[1].players.length == 0) { return false; @@ -225,10 +252,14 @@ function notify_change_match_called_on_court (app, match, callback) { return callback(null); } -function notify_bupws(app, match, callback) { +function notify_bupws(app, match, old_court, callback) { const bupws = require('./bupws'); bupws.handle_score_change(app, match.tournament_key, match.setup.court_id); + + if(old_court) { + bupws.handle_score_change(app, match.tournament_key, old_court); + } return callback(null); } @@ -838,7 +869,7 @@ function call_preparation_match_on_court(app, tournament_key, court_id) { const next_match = matches[0]; next_match.setup.court_id = court_id; next_match.setup.now_on_court = true; - call_match(app, tournament, next_match, (err) => { + call_match(app, tournament, next_match, undefined, (err) => { if (err) { return reject(err); } else { @@ -866,7 +897,7 @@ async function call_next_possible_match_for_preparation(app, tournament_key, cal const match_querry = { 'tournament_key': tournament_key, 'setup.state': 'scheduled' }; app.db.matches.find(match_querry).sort({ 'setup.scheduled_date': 1, 'setup.scheduled_time_str': 1, 'match_order': 1 }).exec((err, matches) => { if (err) { - return callback(msg, err); + return callback(err); } if (matches && matches.length > 0) { const now = new Date(); @@ -1011,6 +1042,7 @@ module.exports ={ call_match, calc_match_set_player_on_court, calc_match_set_player_on_tablet, + switch_court, match_update, uncall_match, fetch_match, diff --git a/static/js/change.js b/static/js/change.js index 9309b09..9931526 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -96,6 +96,7 @@ function default_handler(rerender, special_funcs) { //nothing todo here break; case 'match_edit': + console.log("match_edit in change.js"); ctournament.update_match(c); break; case 'match_add': diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 904a253..331f1ea 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -772,7 +772,6 @@ function update_player(match_id, player, now_on_court, show_player_status) { } function remove_match_from_gui(m, old_section) { - switch (old_section) { case 'finished': case 'unassigned': @@ -792,7 +791,7 @@ function update_player(match_id, player, now_on_court, show_player_status) { render_empty_court_row(match_row_el, c, 'public', false); }); } else { - uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { + uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(old_section.slice(6, old_section.length)) + ']', (match_row_el) => { const court_number = match_row_el.getElementsByClassName('court_number')[0].children[0].innerHTML; const c = { _id:m.setup.court_id, num: court_number}; @@ -1330,11 +1329,17 @@ function _delete_match_btn_click(e) { } function ui_edit(match_id) { - const match = utils.find(curt.matches, m => m._id === match_id); + const match = structuredClone(utils.find(curt.matches, m => m._id === match_id)); + let old_court = structuredClone(match.setup.court_id); if (!match) { cerror.silent('Match ' + match_id + ' konnte nicht gefunden werden'); return; } + + if(!old_court) { + old_court = "not_on_court" + } + crouting.set('t/' + curt.key + '/m/' + match_id + '/edit', {}, _cancel_ui_edit); cbts_utils.esc_stack_push(_cancel_ui_edit); @@ -1383,6 +1388,7 @@ function ui_edit(match_id) { type: 'match_edit', id: d.match_id, match, + old_court, tournament_key: curt.key, btp_update: (curt.btp_enabled && !! d.btp_update), }, function match_edit_callback(err) { @@ -1688,9 +1694,9 @@ function drop(ev) { } }); - //for (const dropp_row of document.getElementsByClassName("droppable")) { - // dropp_row.setAttribute("class", "droppable"); - //} + for (const dropp_row of document.getElementsByClassName("droppable")) { + dropp_row.setAttribute("class", "droppable"); + } } } diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 00ca622..ddbbaa6 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -188,6 +188,8 @@ var ctournament = (function() { } const new_section = cmatch.calc_section(m); cmatch.update_match(m, old_section, new_section); + + return old_section; } function update_upcoming_match(c) { @@ -375,7 +377,6 @@ var ctournament = (function() { function change_announcements(e) { let enable_announcements = document.getElementById('enable_announcements'); - console.log(enable_announcements.checked); window.localStorage.setItem('enable_announcements', enable_announcements.checked); } From f4a6154eb63391d822636ab05af16bdab7131bda Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 23 Nov 2024 17:28:12 +0100 Subject: [PATCH 161/224] #44: Integrate Umpiremanagement in BTS: Bugfix: ServiceJudge was not announced --- static/js/announcements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/announcements.js b/static/js/announcements.js index 5d7a6d0..edfe539 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -118,7 +118,7 @@ function createUmpire(matchSetup) { } function createServiceJudge(matchSetup) { - if (matchSetup.serviceJudge && matchSetup.service_judge.name && matchSetup.service_judge.name != null) { + if (matchSetup.service_judge && matchSetup.service_judge.name && matchSetup.service_judge.name != null) { return ci18n('announcements:service_judge') + normalizeNames(matchSetup.service_judge.name); } return null; From 0860367be277a6f60867683222069e8ffe32993a Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Mon, 23 Dec 2024 00:10:41 +0100 Subject: [PATCH 162/224] Fix Bug #83: Make sure that there is a tournament object when the displaysettings_general is accessed. --- bts/bupws.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bts/bupws.js b/bts/bupws.js index 5bbc8d5..c2dc8f5 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -45,7 +45,7 @@ async function notify_admin_display_status_changed(app, ws, ws_online) { } function generate_default_displaysettings_id(tournament) { - return tournament.displaysettings_general ? tournament.displaysettings_general : default_displaysettings_key; + return (tournament && tournament.displaysettings_general) ? tournament.displaysettings_general : default_displaysettings_key; } function notify_change(tournament_key, court_id, ctype, val) { From 3243536b1cb660d4da7892dd0060b7d9f7752a81 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Mon, 23 Dec 2024 13:13:37 +0100 Subject: [PATCH 163/224] Fix Bug #84: Add missing callback calls in integrate_umpires function of bts/btp_sync.js. --- bts/btp_sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 8c2e561..0d41881 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -919,7 +919,7 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { const admin = require('./admin'); admin.notify_change(app, tournament_key, 'umpire_updated', changed_umpire); }); - return; + return cb(); } } @@ -939,6 +939,7 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { return cb(err); } admin.notify_change(app, tournament_key, 'umpire_add', { umpire: inserted_umpire }); + return cb(); }); }); }, err => { From e0015838fc587f04e88f5409891994b14fb08099 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 24 Dec 2024 12:24:44 +0100 Subject: [PATCH 164/224] #91: Improve the insertion to the match table after new Matches received on Sync --- bts/btp_sync.js | 56 +++++++++----- bts/match_utils.js | 174 ++++++++++++++++++++++---------------------- static/js/cerror.js | 2 +- static/js/cmatch.js | 62 ++++++---------- 4 files changed, 150 insertions(+), 144 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 0d41881..30ae8b3 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -410,6 +410,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { const matches_to_add = []; const matches_on_court = []; + const matches_incomplete = []; async.each(btp_state.matches, function (bm, cb) { const draw = draws.get(bm.DrawID[0]); @@ -442,16 +443,21 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { craft_match(app, tkey, btp_id, court_map, event, draw, btp_state.links, officials, bm, match_ids_on_court).then(match => { - if (cur_match) { - match.setup.state = 'unscheduled'; - if (match.setup.scheduled_date && match.setup.scheduled_time_str) { - match.setup.state = 'scheduled'; - } - - if (match.setup.incomplete == true) { - match.setup.state = 'incomplete'; - } + + match.setup.state = 'unscheduled'; + if (match.setup.now_on_court === true) { + match.setup.state = 'oncourt'; + matches_on_court.push(match); + } + else if (match.setup.incomplete == true) { + match.setup.state = 'incomplete'; + matches_incomplete.push(match); + } + else if (match.setup.scheduled_date && match.setup.scheduled_time_str) { + match.setup.state = 'scheduled'; + } + if (cur_match) { if (cur_match.team1_won === null) { cur_match.team1_won = undefined; } @@ -537,11 +543,6 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { } } - if (match.setup.now_on_court === true) { - match.setup.state = 'oncourt'; - matches_on_court.push(match); - } - for (let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { if ('tablet_break_active' in cur_match.setup.teams[team_index].players[player_index]) { @@ -626,19 +627,24 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { console.error(error); } - matches_to_add.forEach((match_to_add) => { + + matches_to_add.forEach(async (match_to_add) => { let match = match_to_add; - matches_on_court.forEach((match_on_court) => { - const changed_match_on_court = match_utils.calc_match_set_player_on_court(match, match_on_court.setup); + matches_on_court.forEach(async (match_on_court) => { + const changed_match_on_court = await match_utils.calc_match_set_player_on_court(match, match_on_court.setup); if(changed_match_on_court != null) { match = changed_match_on_court; } - const changed_match_tablet_operator = match_utils.calc_match_set_player_on_tablet(match, match_on_court.setup); + const changed_match_tablet_operator = await match_utils.calc_match_set_player_on_tablet(match, match_on_court.setup); if(changed_match_tablet_operator != null) { match = changed_match_tablet_operator; } }); + if(match.setup.now_on_court && !match.setup.called_timestamp) { + match.setup.called_timestamp = Date.now(); + } + app.db.matches.insert(match, function(err) { if (err) { console.error(err); @@ -647,6 +653,16 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { admin.notify_change(app, tkey, 'match_add', { match }); }); }); + + setTimeout(function(){ + matches_incomplete.forEach(match => { + admin.notify_change(app, match.tournament_key, 'match_edit', { + match__id: match._id, + match: match + }); + }); + }, 500); + callback(null); }); @@ -709,7 +725,9 @@ function integrate_courts(app, tournament_key, btp_state, callback) { }); }); }, (err) => { - if (err) return callback(err); + if (err) { + return callback(err); + } if (changed) { stournament.get_courts(app.db, tournament_key, function (err, all_courts) { diff --git a/bts/match_utils.js b/bts/match_utils.js index 4eabf32..f3de411 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -301,54 +301,56 @@ function get_last_looser_on_court(admin, app, tkey, court_id) { } function calc_match_set_player_on_tablet(match, match_on_court_setup) { - if(match.setup.now_on_court == false) { - return null; - } - - if(!match_on_court_setup.tabletoperators || match_on_court_setup.tabletoperators.length <1) { - return null; - } - - let tablet_operatorns_btp_ids = [match_on_court_setup.tabletoperators[0].btp_id]; + return new Promise((resolve) => { + if(match.setup.now_on_court == false) { + resolve(null); + } + + if(!match_on_court_setup.tabletoperators || match_on_court_setup.tabletoperators.length <1) { + resolve(null); + } + + let tablet_operatorns_btp_ids = [match_on_court_setup.tabletoperators[0].btp_id]; - if(match_on_court_setup.tabletoperators.length > 1) { - tablet_operatorns_btp_ids.push(match_on_court_setup.tabletoperators[1].btp_id); - } + if(match_on_court_setup.tabletoperators.length > 1) { + tablet_operatorns_btp_ids.push(match_on_court_setup.tabletoperators[1].btp_id); + } - let change = false; - - if (match.setup.teams[0].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { - match.setup.teams[0].players[0].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[0].checked_in = false; - change = true; - } + let change = false; + + if (match.setup.teams[0].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + match.setup.teams[0].players[0].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[0].checked_in = false; + change = true; + } - if (match.setup.teams[0].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { - match.setup.teams[0].players[1].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[1].checked_in = false; - change = true; - } + if (match.setup.teams[0].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + match.setup.teams[0].players[1].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[1].checked_in = false; + change = true; + } - if (match.setup.teams[1].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { - match.setup.teams[1].players[0].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[0].checked_in = false; - change = true; - } + if (match.setup.teams[1].players.length > 0 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + match.setup.teams[1].players[0].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[0].checked_in = false; + change = true; + } - if (match.setup.teams[1].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { - match.setup.teams[1].players[1].now_tablet_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[1].checked_in = false; - change = true; - } + if (match.setup.teams[1].players.length > 1 && tablet_operatorns_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + match.setup.teams[1].players[1].now_tablet_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[1].checked_in = false; + change = true; + } - if (change) { - return match; - } + if (change) { + resolve(match); + } - return null; + resolve(null); + }); } -function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { +async function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { if(!match_on_court_setup.tabletoperators || match_on_court_setup.tabletoperators.length == 0) { return callback(null); @@ -361,7 +363,7 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { } async.each(matches, async (match, cb) => { - const changed_match = calc_match_set_player_on_tablet(match, match_on_court_setup) + const changed_match = await calc_match_set_player_on_tablet(match, match_on_court_setup) if (changed_match != null) { const setup = changed_match.setup; const match_q = {_id: changed_match._id}; @@ -379,57 +381,59 @@ function set_player_on_tablet (app, tkey, match_on_court_setup, callback) { } function calc_match_set_player_on_court(match, match_on_court_setup) { - if(match.setup.now_on_court == false) { - return null; - } - - let on_court_btp_ids = [match_on_court_setup.teams[0].players[0].btp_id, - match_on_court_setup.teams[1].players[0].btp_id]; + return new Promise((resolve) => { + if(match.setup.now_on_court == false) { + resolve(null); + } + + let on_court_btp_ids = [match_on_court_setup.teams[0].players[0].btp_id, + match_on_court_setup.teams[1].players[0].btp_id]; - if(match_on_court_setup.teams[0].players.length > 1) { - on_court_btp_ids.push(match_on_court_setup.teams[0].players[1].btp_id); - } - - if(match_on_court_setup.teams[1].players.length > 1) { - on_court_btp_ids.push(match_on_court_setup.teams[1].players[1].btp_id); - } + if(match_on_court_setup.teams[0].players.length > 1) { + on_court_btp_ids.push(match_on_court_setup.teams[0].players[1].btp_id); + } + + if(match_on_court_setup.teams[1].players.length > 1) { + on_court_btp_ids.push(match_on_court_setup.teams[1].players[1].btp_id); + } - let change = false; - - if (match.setup.teams[0].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { - match.setup.teams[0].players[0].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[0].tablet_break_active = false; - match.setup.state = 'blocked'; - change = true; - } + let change = false; + + if (match.setup.teams[0].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[0].players[0].btp_id)) { + match.setup.teams[0].players[0].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[0].tablet_break_active = false; + match.setup.state = 'blocked'; + change = true; + } - if (match.setup.teams[0].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { - match.setup.teams[0].players[1].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[0].players[1].tablet_break_active = false; - match.setup.state = 'blocked'; - change = true; - } + if (match.setup.teams[0].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[0].players[1].btp_id)) { + match.setup.teams[0].players[1].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[0].players[1].tablet_break_active = false; + match.setup.state = 'blocked'; + change = true; + } - if (match.setup.teams[1].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { - match.setup.teams[1].players[0].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[0].tablet_break_active = false; - match.setup.state = 'blocked'; - change = true; - } + if (match.setup.teams[1].players.length > 0 && on_court_btp_ids.includes(match.setup.teams[1].players[0].btp_id)) { + match.setup.teams[1].players[0].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[0].tablet_break_active = false; + match.setup.state = 'blocked'; + change = true; + } - if (match.setup.teams[1].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { - match.setup.teams[1].players[1].now_playing_on_court = match_on_court_setup.court_id; - match.setup.teams[1].players[1].tablet_break_active = false; - match.setup.state = 'blocked'; - change = true; - } - if (change) { - return match; - } - return null; + if (match.setup.teams[1].players.length > 1 && on_court_btp_ids.includes(match.setup.teams[1].players[1].btp_id)) { + match.setup.teams[1].players[1].now_playing_on_court = match_on_court_setup.court_id; + match.setup.teams[1].players[1].tablet_break_active = false; + match.setup.state = 'blocked'; + change = true; + } + if (change) { + resolve(match); + } + resolve(null); + }); } -function set_player_on_court (app, tkey, match_on_court_setup, callback) { +async function set_player_on_court (app, tkey, match_on_court_setup, callback) { const admin = require('./admin'); // avoid dependency cycle app.db.matches.find({'tournament_key': tkey}, async (err, matches) => { if (err) { @@ -437,7 +441,7 @@ function set_player_on_court (app, tkey, match_on_court_setup, callback) { } async.each(matches, async (match) => { - const changed_match = calc_match_set_player_on_court(match, match_on_court_setup); + const changed_match = await calc_match_set_player_on_court(match, match_on_court_setup); if (changed_match != null) { const setup = changed_match.setup; const match_q = {_id: changed_match._id}; diff --git a/static/js/cerror.js b/static/js/cerror.js index b0ef6f6..f117cd8 100644 --- a/static/js/cerror.js +++ b/static/js/cerror.js @@ -23,7 +23,7 @@ var cerror = (function() { } function silent(msg) { - console.error(msg); // eslint-disable-line no-console + console.error(msg); on_error(msg, undefined, undefined, undefined, new Error()); } diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 331f1ea..47154a9 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -805,9 +805,10 @@ function update_player(match_id, player, now_on_court, show_player_status) { } function add_match(m, section) { - console.log('in cmatch'); - console.log(section); + insert_new_match_row(m, section); +} +function insert_new_match_row(m, section) { switch (section) { case 'finished': uiu.qsEach('.finished_container', (finished_container) => { @@ -859,54 +860,37 @@ function add_match(m, section) { } } -function update_match(m, old_section, new_section) { - if(old_section != new_section) { - remove_match_from_gui(m, old_section); +function update_match_row(m, new_section) { + uiu.qsEach('.match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { + match_row_el.innerHTML = ''; + switch (new_section) { case 'finished': - uiu.qsEach('.finished_container', (finished_container) => { - let tbody = finished_container.querySelector('.match_table > tbody'); - uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); - }); - case 'unassigned': - uiu.qsEach('.unassigned_container', (unassigned_container) => { - let tbody = unassigned_container.querySelector('.match_table > tbody'); - uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); - }); - break; - default: - break; - } - } else { - uiu.qsEach('.match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { - match_row_el.innerHTML = ''; - }); - } - - switch (new_section) { - case 'finished': - uiu.qsEach('.finished_container > table > tbody > .match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { render_match_row(match_row_el, m, null, 'default', false, true); - }); - break; - case 'unassigned': - uiu.qsEach( '.unassigned_container > table > tbody > .match[data-match_id=' + JSON.stringify(m._id) + ']', (match_row_el) => { + break; + case 'unassigned': match_row_el.setAttribute('class', 'match highlight_' + (m.setup.highlight ? m.setup.highlight : 0)); render_match_row(match_row_el, m, null, 'unasigned', true, true); - }); - break; - default: - const court = utils.find(curt.courts, c => c._id === m.setup.court_id); - uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { - match_row_el.innerHTML = ""; + break; + default: + const court = utils.find(curt.courts, c => c._id === m.setup.court_id); const closest = match_row_el.closest('.main_upcoming'); if(Boolean(closest)) { render_match_row(match_row_el, m, court, 'public'); } else { render_match_row(match_row_el, m, court, 'plain', false, false); } - }); - break; + break; + } + }); +} + +function update_match(m, old_section, new_section) { + if(old_section != new_section) { + remove_match_from_gui(m, old_section); + insert_new_match_row(m, new_section); + } else { + update_match_row(m, new_section); } } From 458d270d56fa9d4f9285db7f7c850a18c476cf0f Mon Sep 17 00:00:00 2001 From: tengmel Date: Tue, 24 Dec 2024 13:15:27 +0100 Subject: [PATCH 165/224] #75: The Upcoming Matches display rotates too quickly --- bts/admin.js | 1 + static/js/change.js | 3 +- static/js/ci18n_de.js | 9 ++++- static/js/ci18n_en.js | 8 +++++ static/js/cmatch.js | 37 +++++++++++++------ static/js/ctournament.js | 77 +++++++++++++++++++++++++++++++++++----- 6 files changed, 114 insertions(+), 21 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 34191b7..ab80bf9 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -68,6 +68,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'btp_ip', 'btp_password', 'is_team', 'is_nation_competition', 'warmup', 'warmup_ready', 'warmup_start', + 'upcoming_matches_animation_speed', 'upcoming_matches_max_count','upcoming_matches_animation_pause', 'ticker_enabled', 'ticker_url', 'ticker_password', 'language', 'dm_style', 'displaysettings_general', 'tabletoperator_enabled', 'tabletoperator_break_seconds', diff --git a/static/js/change.js b/static/js/change.js index 9931526..90af7c2 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -62,7 +62,8 @@ function default_handler(rerender, special_funcs) { 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_with_state_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds', - 'announcement_speed','announcement_pause_time_ms', + 'announcement_speed', 'announcement_pause_time_ms', + 'upcoming_matches_animation_speed', 'upcoming_matches_max_count', 'upcoming_matches_animation_pause', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', 'annoncement_include_event', 'annoncement_include_round', 'annoncement_include_matchnumber', 'call_preparation_matches_automatically_enabled','call_next_possible_scheduled_match_in_preparation', diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 1fba2f3..5467c72 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -5,6 +5,10 @@ var ci18n_de = { 'Unassigned Matches': 'Nicht zugewiesene Spiele', 'Next Matches': 'Nächste Spiele', + 'Current Matches': 'Laufende Spiele', + 'Matchoverview': 'Spielübersicht', + 'Scoreboard': 'Anzeigetafel', + 'Umpire Panel': 'Schiedsrichter-Panel', 'edit tournament': 'bearbeiten', 'Court': 'Court', 'Match': 'Spiel', @@ -138,8 +142,11 @@ var ci18n_de = { 'tournament:edit:tablets': 'Tablets Einstellungen:', 'tournament:edit:ticker': 'Ticker Einstellungen:', 'tournament:edit:btp': 'Badminton Turnier Planer Einstellungen:', - 'tournament:edit:bts': 'Badminton Turnier Server Einstellungen:', + 'tournament:edit:upcoming_matches_settings': 'Spielübersichts Einstellungen', + 'tournament:edit:upcoming_matches_animation_speed': 'Animationsgeschwindigkeit beim Scrollen der Spielübersichten', + 'tournament:edit:upcoming_matches_animation_pause': 'Animationsunterbrechung am Anfang und Ende der Seite (sec)', + 'tournament:edit:upcoming_matches_max_count': 'Maximale Anzahl von Spielen in der Spielübersicht', 'tournament:edit:call_preparation_matches_automatically_enabled': 'Spiele in Vorbereitung automatisch auf freien Felden aufrufen', 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Nächst mögliches Spiel automatisch in Vorbereitung aufrufen', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 55afe71..07c136c 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -5,6 +5,10 @@ var ci18n_en = { 'Unassigned Matches': 'Unassigned Matches', 'Next Matches': 'Next Matches', + 'Current Matches': 'Current Matches', + 'Matchoverview': 'Matchoverview', + 'Scoreboard': 'Scoreboard', + 'Umpire Panel': 'Umpire Panel', 'edit tournament': 'edit', 'Court': 'Court', 'Match': 'Match', @@ -140,6 +144,10 @@ var ci18n_en = { 'tournament:edit:ticker': 'Ticker Settings:', 'tournament:edit:btp': 'Badminton Tournament Planer Settings:', 'tournament:edit:bts': 'Badminton Tournament Server Settings:', + 'tournament:edit:upcoming_matches_settings': 'Game Overview Settings', + 'tournament:edit:upcoming_matches_animation_speed': 'Animationspeed for scroll on game overviews', + 'tournament:edit:upcoming_matches_animation_pause': 'Animation interruption at the beginning and end of the page (sec)', + 'tournament:edit:upcoming_matches_max_count': 'Maximum number of games in the game overview', 'tournament:edit:call_preparation_matches_automatically_enabled': 'Call games in preparation on free fields automatically', 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Automatically call up next possible game in preparation', 'to_stats:header': 'Technical Officials Statistics', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 47154a9..095c49f 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -3,10 +3,9 @@ var cmatch = (function() { var has_resize_event = false; -var scroll_timer = setTimeout(auto_scroll, 10000); -var resize_timer = false; - +var scroll_timer = setTimeout(auto_scroll, 4000); var scroll_down = true; +let is_paused = false; const OVERRIDE_COLORS_KEYS = ['', 'bg']; @@ -28,11 +27,21 @@ function calc_section(m) { return 'unassigned'; } -function auto_scroll(){ +function auto_scroll() { + if (is_paused) { + return; + } + + const scroll_speed = parseInt(curt.upcoming_matches_animation_speed ? curt.upcoming_matches_animation_speed : 2); + if (scroll_speed == 0) { + return; + } + const scroll_object = document.querySelectorAll('.main_upcoming'); let new_top = 0; let height = 0; let child_higth = 0; + scroll_object.forEach((item) =>{ let old_top = 0; @@ -41,9 +50,9 @@ function auto_scroll(){ } if(scroll_down) { - item.style.top = (old_top - 2)+'px'; + item.style.top = (old_top - scroll_speed)+'px'; } else { - item.style.top = (old_top + 2)+'px'; + item.style.top = (old_top + scroll_speed)+'px'; } new_top = parseInt(item.style.top); @@ -55,16 +64,22 @@ function auto_scroll(){ height = item.offsetHeight; }); - let scroll_interval = 1; if(new_top >= 0) { - scroll_interval = 15000; scroll_down = true; + pause_scroll(); } else if (height >= child_higth) { - scroll_interval = 15000; scroll_down = false; + pause_scroll(); } - scroll_timer = setTimeout(auto_scroll, scroll_interval); + requestAnimationFrame(auto_scroll); // Verwendet eine gleichmäßige Animation +} +function pause_scroll() { + is_paused = true; + setTimeout(() => { + is_paused = false; + auto_scroll(); + }, parseInt(curt.upcoming_matches_animation_pause ? curt.upcoming_matches_animation_pause : 4) * 1000); } function resize_table(resizable_rows, table_width_factor) { @@ -1521,7 +1536,7 @@ function render_unassigned(container) { } function render_upcoming_matches(container) { - const UPCOMING_MATCH_COUNT = 13; + const UPCOMING_MATCH_COUNT = parseInt(curt.upcoming_matches_max_count ? curt.upcoming_matches_max_count : 15); uiu.empty(container); uiu.el(container, 'h2', { diff --git a/static/js/ctournament.js b/static/js/ctournament.js index ddbbaa6..d4e3d06 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -293,6 +293,14 @@ var ctournament = (function() { cmatch.render_upcoming_matches(uiu.qs('.upcoming_container')); } + function _update_all_ui_elements_current_matches() { + cmatch.render_courts(uiu.qs('.courts_container'), 'public'); + } + + function _update_all_ui_elements_next_matches() { + cmatch.render_upcoming_matches(uiu.qs('.upcoming_container')); + } + function _show_render_matches() { cmatch.render_courts(uiu.qs('.courts_container')); cmatch.render_unassigned(uiu.qs('.unassigned_container')); @@ -392,15 +400,21 @@ var ctournament = (function() { func: ui_show, 'class': 'ct_name', }], [{ - label: 'Scoreboard', + label: ci18n('Scoreboard'), href: '/bup/#btsh_e=' + encodeURIComponent(curt.key) + '&display' + bup_dm_style + bup_lang, }, { - label: 'Umpire Panel', + label: ci18n('Umpire Panel'), href: '/bup/#btsh_e=' + encodeURIComponent(curt.key) + bup_lang, }, { - label: ci18n('Next Matches'), + label: ci18n('Matchoverview'), href: '/admin/t/' + encodeURIComponent(curt.key) + '/upcoming', - },]); + }, { + label: ci18n('Current Matches'), + href: '/admin/t/' + encodeURIComponent(curt.key) + '/current_matches', + }, { + label: ci18n('Next Matches'), + href: '/admin/t/' + encodeURIComponent(curt.key) + '/next_matches', + }]); const main = uiu.qs('.main'); uiu.empty(main); @@ -741,6 +755,12 @@ var ctournament = (function() { } }; + const upcoming_fieldset = uiu.el(form, 'fieldset'); + uiu.el(upcoming_fieldset, 'h2', 'edit', ci18n('tournament:edit:upcoming_matches_settings')); + create_numeric_input(curt, form, 'upcoming_matches_animation_speed', 0, 10, 2, 1); + create_numeric_input(curt, form, 'upcoming_matches_animation_pause', 1, 20, 4, 1); + create_numeric_input(curt, form, 'upcoming_matches_max_count', 10, 50, 15, 1); + const bts_fieldset = uiu.el(form, 'fieldset'); uiu.el(bts_fieldset, 'h2', 'edit', ci18n('tournament:edit:bts')); create_checkbox(curt, bts_fieldset, 'call_preparation_matches_automatically_enabled'); @@ -900,6 +920,9 @@ var ctournament = (function() { ticker_enabled: (!!data.ticker_enabled), ticker_url: data.ticker_url, ticker_password: data.ticker_password, + upcoming_matches_animation_speed: data.upcoming_matches_animation_speed, + upcoming_matches_max_count: data.upcoming_matches_max_count, + upcoming_matches_animation_pause: data.upcoming_matches_animation_pause, tabletoperator_with_umpire_enabled: (!!data.tabletoperator_with_umpire_enabled), tabletoperator_winner_of_quaterfinals_enabled: (!!data.tabletoperator_winner_of_quaterfinals_enabled), tabletoperator_split_doubles: (!!data.tabletoperator_split_doubles), @@ -1401,24 +1424,49 @@ var ctournament = (function() { cmatch.prepare_render(curt); const courts_container = uiu.el(container, 'div', 'courts_container'); cmatch.render_courts(courts_container, 'public'); + const upcoming_container = uiu.el(container, 'div', 'upcoming_container'); + cmatch.render_upcoming_matches(upcoming_container); + } + function render_current_matches(container) { + cmatch.prepare_render(curt); + const courts_container = uiu.el(container, 'div', 'courts_container'); + cmatch.render_courts(courts_container, 'public'); + } + + function render_next_matches(container) { + cmatch.prepare_render(curt); const upcoming_container = uiu.el(container, 'div', 'upcoming_container'); cmatch.render_upcoming_matches(upcoming_container); } function ui_upcoming() { - crouting.set('t/:key/upcoming', { key: curt.key }); - toprow.hide(); + const main = ui_match_screens('t/:key/upcoming'); + render_upcoming(main); + } + + function ui_current_matches() { + const main = ui_match_screens('t/:key/current_matches'); + render_current_matches(main); + } + + function ui_next_matches() { + const main = ui_match_screens('t/:key/next_matches'); + render_next_matches(main); + } + function ui_match_screens(route) { + crouting.set(route, { key: curt.key }); + toprow.hide(); const main = uiu.qs('.main'); uiu.empty(main); main.classList.add('main_upcoming'); - - render_upcoming(main); main.addEventListener('click', () => { fullscreen.toggle(); }); + return main; } + _route_single(/t\/([a-z0-9]+)\/upcoming/, ui_upcoming, change.default_handler(_update_all_ui_elements_upcoming, { score: update_score, court_current_match: update_upcoming_current_match, @@ -1426,6 +1474,19 @@ var ctournament = (function() { update_player_status: update_player_status, })); + _route_single(/t\/([a-z0-9]+)\/current_matches/, ui_current_matches, change.default_handler(_update_all_ui_elements_upcoming, { + score: update_score, + court_current_match: update_upcoming_current_match, + match_edit: update_upcoming_match, + update_player_status: update_player_status, + })); + _route_single(/t\/([a-z0-9]+)\/next_matches/, ui_next_matches, change.default_handler(_update_all_ui_elements_upcoming, { + score: update_score, + court_current_match: update_upcoming_current_match, + match_edit: update_upcoming_match, + update_player_status: update_player_status, + })); + function init() { send({ From cf9a39522401372a69c3d81bd07e1971f8a40e36 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 24 Dec 2024 14:45:42 +0100 Subject: [PATCH 166/224] remove prints to the console --- bts/admin.js | 3 --- bts/btp_sync.js | 1 - 2 files changed, 4 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index ab80bf9..9f1ecdc 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -565,8 +565,6 @@ function handle_match_edit(app, ws, msg) { }); } else { - console.log("ELSE"); - // TODO get old setup, make sure no key has been removed app.db.matches.update({_id: msg.id, tournament_key}, {$set: {setup}}, {returnUpdatedDocs: true}, function(err, numAffected, changed_match) { if (err) { @@ -583,7 +581,6 @@ function handle_match_edit(app, ws, msg) { ws.respond(msg, new Error(errmsg)); return; } - console.log(changed_match); notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, match: changed_match}); if (msg.btp_update) { btp_manager.update_score(app, changed_match); diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 30ae8b3..4a69395 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -98,7 +98,6 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { if (err) { - console.log("reject"); reject(err); } From 41f1a85d5e974daf730dff90a0d0bfebfdc6787b Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 24 Dec 2024 14:46:21 +0100 Subject: [PATCH 167/224] fix autosize the umipires in Upcoming and current Matches --- static/js/cmatch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 095c49f..ba64a4f 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -411,7 +411,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ //auto_size(umpire_name_div, parrent_width - umpire_icon.offsetWidth - 20); resizable_elements.fixed_width_elements.push(umpire_name_div); - resizable_elements.fixed_width.push(parrent_width - umpire_icon.offsetWidth - 20 + 'px'); + resizable_elements.fixed_width.push(parrent_width - umpire_icon.offsetWidth - 20); } else if (setup.tabletoperators && setup.tabletoperators.length > 0){ const tablet_icon = uiu.el(score_span, 'div', 'tablet', ''); @@ -429,7 +429,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ //auto_size(operators_div, parrent_width - tablet_icon.offsetWidth - 20); resizable_elements.fixed_width_elements.push(operators_div); - resizable_elements.fixed_width.push(parrent_width - tablet_icon.offsetWidth - 20 - 20 + 'px'); + resizable_elements.fixed_width.push(parrent_width - tablet_icon.offsetWidth - 20 - 20); } } } From 1080cd3c43479b2780da04e367e10f243517b1c1 Mon Sep 17 00:00:00 2001 From: tengmel Date: Fri, 27 Dec 2024 17:19:17 +0100 Subject: [PATCH 168/224] #92: BUP/BTS: Advertisement: Make it possible to confgure the advertisements which should be displayed in BTS --- bts/bupws.js | 18 ++++++++++++++++-- bts/database.js | 3 ++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index c2dc8f5..e08af2d 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -511,7 +511,14 @@ function get_display_setting(app, tkey, client_id, court_id, displaysetting) { returnvalue.court_id = display_court_displaysetting[0].court_id; returnvalue.displaymode_court_id = display_court_displaysetting[0].court_id; } - resolve(returnvalue); + app.db.advertisements.find({}, function (err, advertisements) { + if (err) { + return resolve(returnvalue); + } + returnvalue.advertisements = advertisements; + resolve(returnvalue); + + }); }); } else { app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { @@ -532,7 +539,14 @@ function get_display_setting(app, tkey, client_id, court_id, displaysetting) { returnvalue.court_id = court_id; returnvalue.displaymode_court_id = court_id; } - resolve(returnvalue); + app.db.advertisements.find({}, function (err, advertisements) { + if (err) { + return resolve(returnvalue); + } + returnvalue.advertisements = advertisements; + resolve(returnvalue); + + }); }); }); } diff --git a/bts/database.js b/bts/database.js index 57fedbe..cb805a9 100644 --- a/bts/database.js +++ b/bts/database.js @@ -19,7 +19,8 @@ const TABLES = [ 'tabletoperators', 'displaysettings', 'display_court_displaysettings', - 'normalizations' + 'normalizations', + 'advertisements' ]; From 0d0c8c225b56ee7718f6c5a5c0d1609bdbe4b405 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 28 Dec 2024 11:34:47 +0100 Subject: [PATCH 169/224] #92: BUP/BTS: Advertisement: Make it possible to confgure the advertisements which should be displayed in BTS --- bts/admin.js | 58 ++++++++++++++++++---- bts/stournament.js | 8 ++- static/js/change.js | 9 ++-- static/js/ci18n_de.js | 9 +++- static/js/ci18n_en.js | 9 +++- static/js/ctournament.js | 103 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 174 insertions(+), 22 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 9f1ecdc..ef1c889 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -199,20 +199,25 @@ function handle_tournament_get(app, ws, msg) { cb(err); }); }, function (cb) { - stournament.get_displays(app, tournament, function (err, displays) { - tournament.displays = displays; - cb(err); - }); + stournament.get_displays(app, tournament, function (err, displays) { + tournament.displays = displays; + cb(err); + }); }, function (cb) { stournament.get_normalizations(app.db, tournament.key, function (err, normalizations) { tournament.normalizations = normalizations; cb(err); }); }, function (cb) { - stournament.get_displaysettings(app.db, tournament.key, function (err, displaysettings) { - tournament.displaysettings = displaysettings; - cb(err); - }); + stournament.get_advertisements(app.db, tournament.key, function (err, advertisements) { + tournament.advertisements = advertisements; + cb(err); + }); + }, function (cb) { + stournament.get_displaysettings(app.db, tournament.key, function (err, displaysettings) { + tournament.displaysettings = displaysettings; + cb(err); + }); }], function(err) { tournament.btp_status = btp_manager.get_status(tournament.key); tournament.ticker_status = ticker_manager.get_status(tournament.key); @@ -312,7 +317,6 @@ function handle_normalization_add(app, ws, msg) { notify_change(app, msg.tournament_key, 'normalization_add', { normalization: inserted_normalization }); }); } - function handle_normalization_remove(app, ws, msg) { if (!msg.tournament_key) { return ws.respond(msg, { message: 'Missing tournament_key' }); @@ -328,6 +332,40 @@ function handle_normalization_remove(app, ws, msg) { return; }); } +function handle_advertisement_add(app, ws, msg) { + if (!msg.tournament_key) { + return ws.respond(msg, { message: 'Missing tournament_key' }); + } + + if (!msg.advertisement) { + return ws.respond(msg, { message: 'Missing required advertisement' }); + } + + app.db.advertisements.insert(msg.advertisement, function (err, inserted_advertisement) { + if (err) { + ws.respond(msg, err); + return; + } + notify_change(app, msg.tournament_key, 'advertisement_add', { advertisement: inserted_advertisement }); + }); +} + +function handle_advertisement_remove(app, ws, msg) { + if (!msg.tournament_key) { + return ws.respond(msg, { message: 'Missing tournament_key' }); + } + + if (!msg.advertisement_id) { + return ws.respond(msg, { message: 'Missing required advertisement' }); + } + + const query = { _id: msg.advertisement_id }; + app.db.advertisements.remove(query, {}, (err) => { + notify_change(app, msg.tournament_key, 'advertisement_removed', { advertisement_id: msg.advertisement_id }); + return; + }); +} + function handle_tabletoperator_move_up(app, ws, msg) { if (!msg.tournament_key) { return ws.respond(msg, { message: 'Missing tournament_key' }); @@ -957,6 +995,8 @@ module.exports = { handle_confirm_match_finished, handle_normalization_add, handle_normalization_remove, + handle_advertisement_add, + handle_advertisement_remove, handle_tabletoperator_add, handle_tabletoperator_move_up, handle_tabletoperator_move_down, diff --git a/bts/stournament.js b/bts/stournament.js index 0fa4869..eb5c7da 100644 --- a/bts/stournament.js +++ b/bts/stournament.js @@ -61,6 +61,12 @@ function get_normalizations(db, tournament_key, callback) { return callback(err, normalizations); }); } +function get_advertisements(db, tournament_key, callback) { + db.advertisements.find({}, function (err, advertisements) { + if (err) return callback(err); + return callback(err, advertisements); + }); +} function get_displaysettings(db, tournament_key, callback) { db.displaysettings.find({}, function (err, displaysettings) { @@ -69,7 +75,6 @@ function get_displaysettings(db, tournament_key, callback) { }); } - module.exports = { get_courts, get_matches, @@ -78,4 +83,5 @@ module.exports = { get_displays, get_normalizations, get_displaysettings, + get_advertisements, }; diff --git a/static/js/change.js b/static/js/change.js index 90af7c2..d523608 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -163,12 +163,15 @@ function default_handler(rerender, special_funcs) { case 'normalization_add': ctournament.add_normalization(c); break; - case 'normalization_add': - ctournament.add_normalization(c); - break; case 'normalization_removed': ctournament.remove_normalization(c); break; + case 'advertisement_add': + ctournament.add_advertisement(c); + break; + case 'advertisement_removed': + ctournament.remove_advertisement(c); + break; case 'umpires_changed': curt.umpires = c.val.all_umpires; uiu.qsEach('select[name="umpire_name"]', function(select) { diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 5467c72..ac583b2 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -80,8 +80,8 @@ var ci18n_de = { 'announcements:game_for_place': 'Spiel um Platz', 'announcements:voice': 'Google Deutsch', 'announcements:lang': 'de-DE', - - + 'tournament:edit:add': 'Hinzufügen', + 'tournament:edit:delete':'Löschen', 'tournament:edit:id': 'Turnier-ID:', 'tournament:edit:language': 'Sprache:', 'tournament:edit:language:auto': 'Nicht gesetzt (Browser-Einstellung)', @@ -121,6 +121,11 @@ var ci18n_de = { 'tournament:edit:normalizations:origin': 'Original', 'tournament:edit:normalizations:replace': 'Ersetzung', 'tournament:edit:normalizations:language': 'Sprache', + 'tournament:edit:advertisements': 'Werbung', + 'tournament:edit:advertisements:id': 'Id', + 'tournament:edit:advertisements:url': 'URL', + 'tournament:edit:advertisements:type': 'Typ', + 'tournament:edit:advertisements:disabled': 'Deaktiviert', 'tournament:edit:announcement_speed': 'Ansagegeschwindigkeit (0.8-1.3): ', 'tournament:edit:announcement_pause_time_ms': 'Pause zwischen Ansagen (sek): ', 'tournament:edit:preparation_meetingpoint_enabled': 'Meetingpoint für Vorbereitung nutzen', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 07c136c..01aacbe 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -80,8 +80,8 @@ var ci18n_en = { 'announcements:game_for_place': 'Game for place ', 'announcements:voice': 'Google UK English Male', 'announcements:lang': 'en-EN', - - + 'tournament:edit:add': 'Add', + 'tournament:edit:delete': 'Delete', 'tournament:edit:id': 'Tournament id:', 'tournament:edit:language': 'Language:', 'tournament:edit:language:auto': 'Not set (browser default)', @@ -129,6 +129,11 @@ var ci18n_en = { 'tournament:edit:normalizations:origin': 'Origin', 'tournament:edit:normalizations:replace': 'Replacement', 'tournament:edit:normalizations:language': 'Language', + 'tournament:edit:advertisements': 'Advertisements', + 'tournament:edit:advertisements:id': 'Id', + 'tournament:edit:advertisements:url': 'URL', + 'tournament:edit:advertisements:type': 'Type', + 'tournament:edit:advertisements:disabled': 'Disabled', 'tournament:edit:general_displaysettings': 'Manaage Display Settings:', 'tournament:edit:displays': 'Manage Displays:', 'tournament:edit:displays:hostname': 'Hostname', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index d4e3d06..f3de41c 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -262,14 +262,33 @@ var ctournament = (function() { } update_normalization_values(c) } - function update_normalization_values(c) { - uiu.qsEach('.normalizations_values_div',(div_el) => { + uiu.qsEach('.normalizations_values_div', (div_el) => { div_el.innerHTML = ""; render_normalisation_values(div_el); }); } + function add_advertisement(c) { + curt.advertisements.push(c.val.advertisement); + update_advertisements(c) + } + + function remove_advertisement(c) { + const changed_t = utils.find(curt.advertisements, m => m._id === c.val.advertisement_id); + if (changed_t) { + curt.advertisements.splice(curt.advertisements.indexOf(changed_t), 1); + } + update_advertisements(c) + } + + function update_advertisements(c) { + uiu.qsEach('.advertisements_div', (div_el) => { + div_el.innerHTML = ""; + render_advertisements(div_el); + }); + } + function update_current_match(c) { update_match(c); } @@ -453,6 +472,8 @@ var ctournament = (function() { match_remove: remove_match, normalization_removed: remove_normalization, normalization_add: add_normalization, + advertisement_removed: remove_advertisement, + advertisement_add: add_advertisement, tabletoperator_add: tabletoperator_add, tabletoperator_moved_up: tabletoperator_moved_up, tabletoperator_moved_down: tabletoperator_moved_down, @@ -1022,7 +1043,8 @@ var ctournament = (function() { const general_displaysettings_div = uiu.el(main, 'div', 'general_displaysettings'); render_general_displaysettings(general_displaysettings_div); render_displaysettings(main); - render_normalisation_values(uiu.el(main, 'div','normalizations_values_div')); + render_normalisation_values(uiu.el(main, 'div', 'normalizations_values_div')); + render_advertisements(uiu.el(main, 'div', 'advertisements_div')); } _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit, change.default_handler(_update_all_ui_elements_edit, { update_general_displaysettings: update_general_displaysettings, @@ -1043,7 +1065,7 @@ var ctournament = (function() { create_undecorated_input("text", uiu.el(tr_input, 'td', {}), 'normalizations_replace'); create_undecorated_input("text", uiu.el(tr_input, 'td', {}), 'normalizations_language'); const actions_td = uiu.el(tr_input, 'td', {}); - const add_btn = uiu.el(actions_td, 'button', {}, 'Add'); + const add_btn = uiu.el(actions_td, 'button', {}, ci18n('tournament:edit:add')); add_btn.addEventListener('click', function (e) { var new_normalization = {} @@ -1069,7 +1091,7 @@ var ctournament = (function() { const actions_td = uiu.el(tr, 'td', {}); const delete_btn = uiu.el(actions_td, 'button', { 'data-normalization-id': nv._id, - }, 'Delete'); + }, ci18n('tournament:edit:delete')); delete_btn.addEventListener('click', function (e) { const del_btn = e.target; @@ -1087,6 +1109,75 @@ var ctournament = (function() { } } + function render_advertisements(main) { + uiu.el(main, 'h2', 'edit', ci18n('tournament:edit:advertisements')); + + const display_table = uiu.el(main, 'table'); + const display_tbody = uiu.el(display_table, 'tbody'); + const tr = uiu.el(display_tbody, 'tr'); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:advertisements:id')); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:advertisements:url')); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:advertisements:type')); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:advertisements:disabled')); + uiu.el(tr, 'th', {}, ''); + const tr_input = uiu.el(display_tbody, 'tr'); + uiu.el(tr_input, 'td', {}, ''); + create_undecorated_input("text", uiu.el(tr_input, 'td', {}), 'advertisement_url'); + create_undecorated_input("text", uiu.el(tr_input, 'td', {}), 'advertisement_type'); + uiu.el(tr_input, 'td', {}, ''); + const actions_td = uiu.el(tr_input, 'td', {}); + const add_btn = uiu.el(actions_td, 'button', {}, ci18n('tournament:edit:add')); + add_btn.addEventListener('click', function (e) { + + var new_advertisement = {} + new_advertisement.id = generateGUID(); + new_advertisement.url = document.getElementById('advertisement_url').value; + new_advertisement.type = document.getElementById('advertisement_type').value; + new_advertisement.disabled = false; + send({ + type: 'advertisement_add', + tournament_key: curt.key, + advertisement: new_advertisement, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + for (const nv of curt.advertisements) { + const tr = uiu.el(display_tbody, 'tr'); + uiu.el(tr, 'td', {}, nv.id); + uiu.el(tr, 'td', {}, nv.url); + uiu.el(tr, 'td', {}, nv.type); + uiu.el(tr, 'td', {}, nv.disabled); + const actions_td = uiu.el(tr, 'td', {}); + const delete_btn = uiu.el(actions_td, 'button', { + 'data-advertisement-id': nv._id, + }, ci18n('tournament:edit:delete')); + + delete_btn.addEventListener('click', function (e) { + const del_btn = e.target; + const advertisement_id = del_btn.getAttribute('data-advertisement-id'); + send({ + type: 'advertisement_remove', + tournament_key: curt.key, + advertisement_id: advertisement_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + } + } + function generateGUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) { + const random = Math.random() * 16 | 0; + const value = char === 'x' ? random : (random & 0x3 | 0x8); + return value.toString(16); + }); + } + function set_battery_state(battery, node) { if (battery && battery != null) { node.removeAttribute("class"); @@ -1670,6 +1761,8 @@ var ctournament = (function() { bts_status_changed, remove_normalization, add_normalization, + remove_advertisement, + add_advertisement, update_general_displaysettings, }; From 75dbe2a4cc8d5780c5411dd86c87aa069ff1ae09 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 29 Dec 2024 11:32:02 +0100 Subject: [PATCH 170/224] #92: BUP/BTS: Advertisement: Make it possible to confgure the advertisements which should be displayed in BTS --- bts/admin.js | 5 +++++ bts/bupws.js | 33 +++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index ef1c889..a2a2477 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -347,6 +347,9 @@ function handle_advertisement_add(app, ws, msg) { return; } notify_change(app, msg.tournament_key, 'advertisement_add', { advertisement: inserted_advertisement }); + const bupws = require('./bupws'); + bupws.send_advertisement_add(msg.tournament_key,inserted_advertisement); + return; }); } @@ -362,6 +365,8 @@ function handle_advertisement_remove(app, ws, msg) { const query = { _id: msg.advertisement_id }; app.db.advertisements.remove(query, {}, (err) => { notify_change(app, msg.tournament_key, 'advertisement_removed', { advertisement_id: msg.advertisement_id }); + const bupws = require('./bupws'); + bupws.send_advertisement_remove(msg.tournament_key,msg.advertisement_id); return; }); } diff --git a/bts/bupws.js b/bts/bupws.js index e08af2d..f7014ff 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -54,20 +54,31 @@ function notify_change(tournament_key, court_id, ctype, val) { } } +function notify_change_broadcast(tournament_key, ctype, val) { + for (const panel_ws of all_panels) { + notify_change_send(panel_ws, tournament_key, ctype, val); + } +} + function notify_change_ws(ws, tournament_key, court_id, ctype, val) { if (ws == null) { notify_change(tournament_key, court_id, ctype, val); } else { if (ws.court_id === court_id) { - ws.sendmsg({ - type: 'change', - tournament_key, - ctype, - val, - }); + notify_change_send(ws,tournament_key, ctype, val); } } } + +function notify_change_send(ws,tournament_key, ctype, val) { + ws.sendmsg({ + type: 'change', + tournament_key, + ctype, + val, + }); +} + function send_courts(app, ws, tournament_key) { stournament.get_courts(app.db, tournament_key, function (err, courts) { notify_change_ws(ws,tournament_key, ws.court_id, "courts-update", courts); @@ -351,6 +362,14 @@ async function send_finshed_confirmed(app, tournament_key, court_id) { notify_change(tournament_key, court_id, 'confirm-match-finished', {}); } +async function send_advertisement_add(tournament_key, advertisement) { + notify_change_broadcast(tournament_key, 'advertisement_add', advertisement); +} + +async function send_advertisement_remove(tournament_key, advertisement_id) { + notify_change_broadcast(tournament_key, 'advertisement_remove', { advertisement_id: advertisement_id }); +} + async function initialize_client(ws, app, tournament_key, court_id, displaysetting) { const client_id = determine_client_id(ws); const hostname = await determine_client_hostname(ws); @@ -843,6 +862,8 @@ module.exports = { update_device_info, restart_panel, send_finshed_confirmed, + send_advertisement_add, + send_advertisement_remove, change_display_mode, change_default_display_mode, add_display_status, From ae6b4a88e03a50d4d419e3b618577a5ec1261088 Mon Sep 17 00:00:00 2001 From: tengmel Date: Tue, 31 Dec 2024 10:57:38 +0100 Subject: [PATCH 171/224] # 49: Ticker: Revision of the display of all matches --- static/ticker/root.html | 9 +-------- static/ticker/ticker.css | 4 +--- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/static/ticker/root.html b/static/ticker/root.html index 6794513..1da2223 100644 --- a/static/ticker/root.html +++ b/static/ticker/root.html @@ -7,25 +7,18 @@ {{tournament_name_html}} -
    -

    {{tournament_name_html}}

    -
    Stand: {{last_update_str}}
    -
    {{prefix_html}}
    -
    {{courts_html}}
    -
    {{note_html}} +
    Stand: {{last_update_str}}
    - - diff --git a/static/ticker/ticker.css b/static/ticker/ticker.css index 0bda4c8..3bfc308 100644 --- a/static/ticker/ticker.css +++ b/static/ticker/ticker.css @@ -21,9 +21,7 @@ h1 { font-size: 120%; } .last_update { - position: absolute; - right: 10px; - top: 8px; + float: right; font-size: 10px; font-weight: bold; } From 70b51850d049edd780977bf24e8b5793b23f8e62 Mon Sep 17 00:00:00 2001 From: tengmel Date: Tue, 31 Dec 2024 14:47:08 +0100 Subject: [PATCH 172/224] # 49: Ticker: Revision of the display of all matches: Tournamentlogo will be send to Ticker and displayed in the toprow --- bts/ticker_conn.js | 57 +++++++++++++++++++++++++++++++--------- static/ticker/root.html | 1 + static/ticker/ticker.css | 9 +++++-- ticker/tupdate.js | 6 +++++ ticker/tweb.js | 3 +++ 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/bts/ticker_conn.js b/bts/ticker_conn.js index 4bce812..713bf0b 100644 --- a/bts/ticker_conn.js +++ b/bts/ticker_conn.js @@ -6,7 +6,8 @@ const ws_module = require('ws'); const utils = require('./utils'); const serror = require('./serror'); - +const fs = require('fs').promises; +const path = require('path'); const RECONNECT_TIMEOUT = 1000; @@ -190,21 +191,51 @@ class TickerConn { } } - - var tname = ""; - var turl = ""; if (db_tournaments && db_tournaments.length == 1) { const tournament = db_tournaments[0]; - tname = tournament.name; - turl = "https://" + ((tournament.btp_settings && tournament.btp_settings.tournament_urn) ? tournament.btp_settings.tournament_urn : "www.turnier.de") + "/tournament" + (tournament.tguid ? "/" + tournament.tguid+"/matches" : "s/"); + const tname = tournament.name; + const turl = "https://" + ((tournament.btp_settings && tournament.btp_settings.tournament_urn) ? tournament.btp_settings.tournament_urn : "www.turnier.de") + "/tournament" + (tournament.tguid ? "/" + tournament.tguid + "/matches" : "s/"); + + const file_path = path.join(utils.root_dir(), 'data', 'logos', tournament.logo_id); + fs.readFile(file_path) + .then((file_buffer) => { + const base64_image = file_buffer.toString('base64'); + const filetype = tournament.logo_id.split(".")[1]; + const mime = { + gif: 'image/gif', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + svg: 'image/svg+xml', + webp: 'image/webp', + }[filetype]; + + return cb(null, { + courts: db_courts.map(craft_court), + matches: interesting_matches.map(craft_match), + tournament_name: tname, + tournament_url: turl, + tournament_logo: base64_image, + tournament_logo_mime: mime, + tournament_logo_background_color: tournament.logo_background_color + }); + }) + .catch((error) => { + return cb(null, { + courts: db_courts.map(craft_court), + matches: interesting_matches.map(craft_match), + tournament_name: tname, + tournament_url: turl + }); + }); + } else { + return cb(null, { + courts: db_courts.map(craft_court), + matches: interesting_matches.map(craft_match), + tournament_name: "", + tournament_url: "" + }); } - - return cb(null, { - courts: db_courts.map(craft_court), - matches: interesting_matches.map(craft_match), - tournament_name: tname, - tournament_url: turl - }); }); } diff --git a/static/ticker/root.html b/static/ticker/root.html index 1da2223..499ccbc 100644 --- a/static/ticker/root.html +++ b/static/ticker/root.html @@ -9,6 +9,7 @@
    +

    {{tournament_name_html}}

    {{prefix_html}}
    diff --git a/static/ticker/ticker.css b/static/ticker/ticker.css index 3bfc308..ea5dc19 100644 --- a/static/ticker/ticker.css +++ b/static/ticker/ticker.css @@ -9,12 +9,17 @@ html, body { .topline { position: relative; - margin: 0.3em 0 0.5em 0; + align-items: center; + display: flex; + margin: 0.3em 0 0 0; } +.tournament_logo { + height: 32px; +} h1 { - margin: 0; + margin: 0 0 0 10px; padding: 0; font-size: inherit; font-weight: bold; diff --git a/ticker/tupdate.js b/ticker/tupdate.js index 3a71cc3..b627e75 100644 --- a/ticker/tupdate.js +++ b/ticker/tupdate.js @@ -25,6 +25,12 @@ function handle_tset(app, ws, msg) { if (msg.event.tournament_name) { app.config.tournament_name = msg.event.tournament_name; } + + if (msg.event.tournament_logo) { + app.config.tournament_logo = msg.event.tournament_logo; + app.config.tournament_logo_background_color = msg.event.tournament_logo_background_color; + app.config.tournament_logo_mime = msg.event.tournament_logo_mime; + } if (msg.event.tournament_url) { app.config.note_html = "Alle Spiele auf Turnier.de"; } diff --git a/ticker/tweb.js b/ticker/tweb.js index 55aafae..0bf6b5c 100644 --- a/ticker/tweb.js +++ b/ticker/tweb.js @@ -33,6 +33,9 @@ function main_handler(req, res, next) { const app = req.app; html = html.replace(/{{error_reporting}}/g, JSON.stringify(serror.active(app.config))); html = html.replace(/{{tournament_name_html}}/g, utils.encode_html(app.config.tournament_name)); + html = html.replace(/{{tournament_logo_mime_html}}/g, app.config.tournament_logo_mime || 'image/png'); + html = html.replace(/{{tournament_logo_background_color_html}}/g, app.config.tournament_logo_background_color || '#FFFFFF'); + html = html.replace(/{{tournament_logo_html}}/g, app.config.tournament_logo || 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wIAAgkBA8gL3QoAAAAASUVORK5CYII'); html = html.replace(/{{last_update_str}}/g, utils.encode_html(app.ticker_data ? (app.ticker_data.last_update_str || '') : '')); html = html.replace(/{{note_html}}/g, app.config.note_html); html = html.replace(/{{prefix_html}}/g, app.config.prefix_html || ''); From 390a376e2624ecdc95091ca85e04bb32a3b30bcc Mon Sep 17 00:00:00 2001 From: tengmel Date: Wed, 1 Jan 2025 11:02:55 +0100 Subject: [PATCH 173/224] # 49: Ticker: Revision of the display of all matches: Tournamentlogo will be send to Ticker and displayed in the toprow: BTS crashes on starup if logo_id missing --- bts/ticker_conn.js | 70 ++++++++++++++++++++++++++-------------------- ticker/tupdate.js | 4 +++ 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/bts/ticker_conn.js b/bts/ticker_conn.js index 713bf0b..3bfb274 100644 --- a/bts/ticker_conn.js +++ b/bts/ticker_conn.js @@ -195,39 +195,47 @@ class TickerConn { const tournament = db_tournaments[0]; const tname = tournament.name; const turl = "https://" + ((tournament.btp_settings && tournament.btp_settings.tournament_urn) ? tournament.btp_settings.tournament_urn : "www.turnier.de") + "/tournament" + (tournament.tguid ? "/" + tournament.tguid + "/matches" : "s/"); - - const file_path = path.join(utils.root_dir(), 'data', 'logos', tournament.logo_id); - fs.readFile(file_path) - .then((file_buffer) => { - const base64_image = file_buffer.toString('base64'); - const filetype = tournament.logo_id.split(".")[1]; - const mime = { - gif: 'image/gif', - png: 'image/png', - jpg: 'image/jpeg', - jpeg: 'image/jpeg', - svg: 'image/svg+xml', - webp: 'image/webp', - }[filetype]; - - return cb(null, { - courts: db_courts.map(craft_court), - matches: interesting_matches.map(craft_match), - tournament_name: tname, - tournament_url: turl, - tournament_logo: base64_image, - tournament_logo_mime: mime, - tournament_logo_background_color: tournament.logo_background_color - }); - }) - .catch((error) => { - return cb(null, { - courts: db_courts.map(craft_court), - matches: interesting_matches.map(craft_match), - tournament_name: tname, - tournament_url: turl + if (tournament.logo_id && tournament.logo_id != null) { + const file_path = path.join(utils.root_dir(), 'data', 'logos', tournament.logo_id); + fs.readFile(file_path) + .then((file_buffer) => { + const base64_image = file_buffer.toString('base64'); + const filetype = tournament.logo_id.split(".")[1]; + const mime = { + gif: 'image/gif', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + svg: 'image/svg+xml', + webp: 'image/webp', + }[filetype]; + + return cb(null, { + courts: db_courts.map(craft_court), + matches: interesting_matches.map(craft_match), + tournament_name: tname, + tournament_url: turl, + tournament_logo: base64_image, + tournament_logo_mime: mime, + tournament_logo_background_color: tournament.logo_background_color + }); + }) + .catch((error) => { + return cb(null, { + courts: db_courts.map(craft_court), + matches: interesting_matches.map(craft_match), + tournament_name: tname, + tournament_url: turl + }); }); + } else { + return cb(null, { + courts: db_courts.map(craft_court), + matches: interesting_matches.map(craft_match), + tournament_name: tname, + tournament_url: turl }); + } } else { return cb(null, { courts: db_courts.map(craft_court), diff --git a/ticker/tupdate.js b/ticker/tupdate.js index b627e75..2e7a21a 100644 --- a/ticker/tupdate.js +++ b/ticker/tupdate.js @@ -30,6 +30,10 @@ function handle_tset(app, ws, msg) { app.config.tournament_logo = msg.event.tournament_logo; app.config.tournament_logo_background_color = msg.event.tournament_logo_background_color; app.config.tournament_logo_mime = msg.event.tournament_logo_mime; + } else { + app.config.tournament_logo = undefined; + app.config.tournament_logo_background_color = undefined; + app.config.tournament_logo_mime = undefined; } if (msg.event.tournament_url) { app.config.note_html = "Alle Spiele auf Turnier.de"; From b427c57f8b43a26d1d3077c3d72d00a58cd5063c Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sun, 12 Jan 2025 03:26:37 +0100 Subject: [PATCH 174/224] #93: Show only called and finished (in the last Minute) matches on the tablets. --- bts/bupws.js | 1 + 1 file changed, 1 insertion(+) diff --git a/bts/bupws.js b/bts/bupws.js index f7014ff..9d49727 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -637,6 +637,7 @@ function matches_handler(app, ws, tournament_key, court_id) { if (!court_id) { matches = matches.filter(m => m.setup.now_on_court); } + matches = matches.filter(m => m.setup.state == 'oncourt' || m.setup.state == 'finished'); db_courts.sort(utils.cmp_key('num')); const courts = db_courts.map(function (dc) { From 853e6be181ed2b5df6aa3147a7aa65f1adfab2a2 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 12 Jan 2025 18:08:53 +0100 Subject: [PATCH 175/224] #65: Server.Core: Automatic call of a match which is in preparation when a match is finished. Handle Racecondition if btp sync is active and games will finshed more strict --- bts/btp_conn.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 64e021a..b18da5c 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -110,16 +110,16 @@ class BTPConn { this.pushall(); if (this.enabled_autofetch) { - this.fetch(); + update_queue.instance().execute(this.fetch, this); } - this.schedule_fetch(); }); } - fetch() { - const ir = btp_proto.get_info_request(this.password); - this.send(ir, response => { - update_queue.instance().execute(btp_sync.sync_btp_data,this.app, this.tkey, response); + fetch(connection) { + const ir = btp_proto.get_info_request(connection.password); + connection.send(ir, response => { + btp_sync.sync_btp_data(connection.app, connection.tkey, response); + connection.schedule_fetch(); }); } @@ -132,8 +132,7 @@ class BTPConn { } this.autofetch_timeout = setTimeout(() => { - this.fetch(); - this.schedule_fetch(); + update_queue.instance().execute(this.fetch, this); }, AUTOFETCH_TIMEOUT); } From 6f04ac37f1e3a06692e8ef013a5d6b2c22e4d6fb Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 15 Nov 2024 20:13:15 +0100 Subject: [PATCH 176/224] add better edit tournament site --- static/css/admin.css | 68 ++- static/js/change.js | 2 + static/js/ci18n_de.js | 15 + static/js/ctournament.js | 941 ++++++++++++++++++++++----------------- 4 files changed, 613 insertions(+), 413 deletions(-) diff --git a/static/css/admin.css b/static/css/admin.css index 189b900..2d4b42b 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -43,7 +43,7 @@ h1 { margin: 0; } h3 { - margin: 0; + margin: 20px 0 10px 0; } .connecting { @@ -105,14 +105,56 @@ a:hover { padding: 0; border: none; } + .tournament_settings > label, -.tournament_settings fieldset > label { - display: block; +.tournament_settings fieldset > label , +.tournament_settings > .settings > label, +.tournament_settings fieldset > .settings > label { + display: flex; + align-items: center; } + .tournament_settings > label > span, -.tournament_settings fieldset > label > span { +.tournament_settings fieldset > label > span, +.tournament_settings > .settings > label > span, +.tournament_settings fieldset > .settings > label > span { + display: inline-block; + width: 400px; + min-width: fit-content; +} + +.tournament_settings > label > input[type=text], +.tournament_settings fieldset > label > input[type=text], +.tournament_settings > .settings > label > input[type=text], +.tournament_settings fieldset > .settings > label > input[type=text], +.tournament_settings > label > input[type=number], +.tournament_settings fieldset > label > input[type=number], +.tournament_settings > .settings > label > input[type=number], +.tournament_settings fieldset > .settings > label > input[type=number] { + flex-grow: 1; +} + +.tournament_settings > label > select, +.tournament_settings fieldset > label > select, +.tournament_settings > .settings > label > select, +.tournament_settings fieldset > .settings > label > select { + flex-grow: 1; +} + + + +.tournament_settings { + display: block; + columns: 920px 6; +} + +.settings { display: inline-block; - min-width: 5em; + background-color: #ddd; + width: 900px; + padding: 10px; + border-radius: 10px; + margin: 10px; } .dialog_bg { @@ -241,11 +283,11 @@ h2 { text-align: center; } - h2.edit { - margin: 0; - padding-top: 20px; - padding-bottom: 10px; - font-size: 25px; - color: black; - text-align: left; - } \ No newline at end of file +h2.edit { + margin: 10px 0 20px 0; + padding-top: 0px; + padding-bottom: 0px; + font-size: 25px; + color: black; + text-align: left; +} diff --git a/static/js/change.js b/static/js/change.js index d523608..4f4c58f 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -32,6 +32,8 @@ function default_handler(rerender, special_funcs) { announce([c.val.text]); break; case 'props': { + console.log(c.val); + curt.name = c.val.name; curt.is_team = c.val.is_team; curt.tguid = c.val.tguid; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index ac583b2..25ff7df 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -82,6 +82,21 @@ var ci18n_de = { 'announcements:lang': 'de-DE', 'tournament:edit:add': 'Hinzufügen', 'tournament:edit:delete':'Löschen', + + + 'tournament:edit:tournament': 'Turnier:', + 'tournament:edit:tournament_flow': 'Turnier-Ablauf:', + 'tournament:edit:ticker_connection': 'Ticker-Verbindung:', + 'tournament:edit:btp_connection': 'BTP-Verbindung:', + 'tournament:edit:devices': 'Verbundene Geräte:', + 'tournament:edit:calls': 'Aufrufe:', + 'tournament:edit:location': 'Spielort:', + 'tournament:edit': 'Einstellungen Verwalten:', + 'tournament:edit:save': 'Speichern', + 'tournament:edit:save_and_back': 'Speichern und zur Turnierübersicht', + + + 'tournament:edit:tournament:type': 'Turnier-Typ:', 'tournament:edit:id': 'Turnier-ID:', 'tournament:edit:language': 'Sprache:', 'tournament:edit:language:auto': 'Nicht gesetzt (Browser-Einstellung)', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index f3de41c..d6e842e 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -585,471 +585,610 @@ var ctournament = (function() { const main = uiu.qs('.main'); uiu.empty(main); - const form = uiu.el(main, 'form', 'tournament_settings'); - const key_label = uiu.el(form, 'label'); - uiu.el(key_label, 'span', {}, ci18n('tournament:edit:id')); - uiu.el(key_label, 'input', { - type: 'text', - name: 'key', - readonly: 'readonly', - disabled: 'disabled', - title: 'Can not be changed', - 'class': 'uneditable', - value: curt.key, - }); + const form = uiu.el(main, 'div', 'tournament_settings'); + let input = {}; + + // tournament-div################################################################################## + { + const tournament_div = uiu.el(form, 'div', 'settings'); + uiu.el(tournament_div, 'h2', 'edit', ci18n('tournament:edit:tournament')); + + const key_label = uiu.el(tournament_div, 'label'); + uiu.el(key_label, 'span', {}, ci18n('tournament:edit:id')); + uiu.el(key_label, 'input', { + type: 'text', + name: 'key', + readonly: 'readonly', + disabled: 'disabled', + title: 'Can not be changed', + 'class': 'uneditable', + value: curt.key, + }); - const name_label = uiu.el(form, 'label'); - uiu.el(name_label, 'span', {}, ci18n('tournament:edit:name')); - uiu.el(name_label, 'input', { - type: 'text', - name: 'name', - required: 'required', - value: curt.name || curt.key, - 'class': 'ct_name', - }); + const name_label = uiu.el(tournament_div, 'label'); + uiu.el(name_label, 'span', {}, ci18n('tournament:edit:name')); + input.name = uiu.el(name_label, 'input', { + type: 'text', + name: 'name', + required: 'required', + value: curt.name || curt.key, + 'class': 'ct_name', + }); - const name_tguid = uiu.el(form, 'label'); - uiu.el(name_tguid, 'span', {}, ci18n('tournament:edit:tguid')); - uiu.el(name_tguid, 'input', { - type: 'text', - name: 'tguid', - value: curt.tguid ? curt.tguid : "", - 'class': 'ct_tguid', - }); + const name_tguid = uiu.el(tournament_div, 'label'); + uiu.el(name_tguid, 'span', {}, ci18n('tournament:edit:tguid')); + input.tguid = uiu.el(name_tguid, 'input', { + type: 'text', + name: 'tguid', + value: curt.tguid ? curt.tguid : "", + 'class': 'ct_tguid', + }); - // Tournament language selection - const language_label = uiu.el(form, 'label'); - uiu.el(language_label, 'span', {}, ci18n('tournament:edit:language')); - const language_select = uiu.el(language_label, 'select', { - name: 'language', - required: 'required', - }); - const all_langs = ci18n.get_all_languages(); - uiu.el(language_select, 'option', { value: 'auto' }, ci18n('tournament:edit:language:auto')); - for (const l of all_langs) { - const l_attrs = { - value: l._code, + // Tournament language selection + const language_label = uiu.el(tournament_div, 'label'); + uiu.el(language_label, 'span', {}, ci18n('tournament:edit:language')); + const language_select = uiu.el(language_label, 'select', { + name: 'language', + required: 'required', + }); + const all_langs = ci18n.get_all_languages(); + uiu.el(language_select, 'option', { value: 'auto' }, ci18n('tournament:edit:language:auto')); + for (const l of all_langs) { + const l_attrs = { + value: l._code, + }; + if (l._code === curt.language) { + l_attrs.selected = 'selected'; + } + uiu.el(language_select, 'option', l_attrs, l._name); + } + input.language = language_select; + + // Team competition? + const is_team_label = uiu.el(tournament_div, 'label'); + uiu.el(is_team_label, 'span', {}, ci18n('tournament:edit:tournament:type')); + const is_team_attrs = { + type: 'checkbox', + name: 'is_team', }; - if (l._code === curt.language) { - l_attrs.selected = 'selected'; + if (curt.is_team) { + is_team_attrs.checked = 'checked'; } - uiu.el(language_select, 'option', l_attrs, l._name); - } - // Team competition? - const is_team_label = uiu.el(form, 'label'); - const is_team_attrs = { - type: 'checkbox', - name: 'is_team', - }; - if (curt.is_team) { - is_team_attrs.checked = 'checked'; - } - uiu.el(is_team_label, 'input', is_team_attrs); - uiu.el(is_team_label, 'span', {}, ci18n('team competition')); + input.is_team = uiu.el(is_team_label, 'input', is_team_attrs); + uiu.el(is_team_label, 'span', {}, ci18n('team competition')); - // Nation competition? - const is_nation_competition_label = uiu.el(form, 'label'); - const is_nation_competition_attrs = { - type: 'checkbox', - name: 'is_nation_competition', - }; - if (curt.is_nation_competition) { - is_nation_competition_attrs.checked = 'checked'; - } - uiu.el(is_nation_competition_label, 'input', is_nation_competition_attrs); - uiu.el(is_nation_competition_label, 'span', {}, ci18n('nation competition')); - - // Default display - const cur_dm_style = curt.dm_style || 'international'; - const dm_style_label = uiu.el(form, 'label'); - uiu.el(dm_style_label, 'span', {}, ci18n('tournament:edit:dm_style')); - const dm_style_select = uiu.el(dm_style_label, 'select', { - name: 'dm_style', - required: 'required', - }); - const all_dm_styles = displaymode.ALL_STYLES; - for (const s of all_dm_styles) { - const s_attrs = { - value: s, + // Nation competition? + const is_nation_competition_label = uiu.el(tournament_div, 'label'); + const is_nation_competition_attrs = { + type: 'checkbox', + name: 'is_nation_competition', }; - if (s === cur_dm_style) { - s_attrs.selected = 'selected'; + if (curt.is_nation_competition) { + is_nation_competition_attrs.checked = 'checked'; } - uiu.el(dm_style_select, 'option', s_attrs, s); - } - - const displaysettings_style_label = uiu.el(form, 'label'); - uiu.el(displaysettings_style_label, 'span', {}, ci18n('tournament:edit:displaysettings_general')); - createGeneralDisplaySettingsSelectBox(displaysettings_style_label, curt.displaysettings_general ? curt.displaysettings_general : "default"); - - - // Warmup Timer - if (!curt.warmup_ready) { - curt.warmup_ready = 150; + uiu.el(is_nation_competition_label, 'span', {}, ''); + input.is_nation_competition = uiu.el(is_nation_competition_label, 'input', is_nation_competition_attrs); + uiu.el(is_nation_competition_label, 'span', {}, ci18n('nation competition')); } - if (!curt.warmup_start) { - curt.warmup_start = 180; - } + // btp-connection-div################################################################################## + { + const btp_connection_div = uiu.el(form, 'div', 'settings'); + uiu.el(btp_connection_div, 'h2', 'edit', ci18n('tournament:edit:btp_connection')); + + // BTP + const btp_fieldset = uiu.el(btp_connection_div, 'fieldset'); + const btp_enabled_label = uiu.el(btp_fieldset, 'label'); + const ba_attrs = { + type: 'checkbox', + name: 'btp_enabled', + }; + if (curt.btp_enabled) { + ba_attrs.checked = 'checked'; + } + input.btp_enabled = uiu.el(btp_enabled_label, 'input', ba_attrs); + uiu.el(btp_enabled_label, 'span', {}, ci18n('tournament:edit:btp:enabled')); - var warmup_options = [['bwf-2016', 90, 120, true], - ['legacy', 120, 120, true], - ['choise', curt.warmup_ready, curt.warmup_start, false], - ['call-down', curt.warmup_ready, curt.warmup_start, false], - ['call-up', 0, 0, true], - ['none', 0, 0, true]]; + const btp_autofetch_enabled_label = uiu.el(btp_fieldset, 'label'); + const bae_attrs = { + type: 'checkbox', + name: 'btp_autofetch_enabled', + }; + if (curt.btp_autofetch_enabled) { + bae_attrs.checked = 'checked'; + } + input.btp_autofetch_enabled = uiu.el(btp_autofetch_enabled_label, 'input', bae_attrs); + uiu.el(btp_autofetch_enabled_label, 'span', {}, ci18n('tournament:edit:btp:autofetch_enabled')); - var last_selected_warmup = warmup_options[0]; + const btp_readonly_label = uiu.el(btp_fieldset, 'label'); + const bro_attrs = { + type: 'checkbox', + name: 'btp_readonly', + }; + if (curt.btp_readonly) { + bro_attrs.checked = 'checked'; + } + input.btp_readonly = uiu.el(btp_readonly_label, 'input', bro_attrs); + uiu.el(btp_readonly_label, 'span', {}, ci18n('tournament:edit:btp:readonly')); + + const btp_ip_label = uiu.el(btp_fieldset, 'label'); + uiu.el(btp_ip_label, 'span', {}, ci18n('tournament:edit:btp:ip')); + input.btp_ip = uiu.el(btp_ip_label, 'input', { + type: 'text', + name: 'btp_ip', + value: (curt.btp_ip || ''), + }); - const warmup_timer_label = uiu.el(form, 'label'); - uiu.el(warmup_timer_label, 'span', {}, ci18n('tournament:edit:warmup_timer_behavior')); - const warmup_timer_select = uiu.el(warmup_timer_label, 'select', { - name: 'warmup', - }); - uiu.el(warmup_timer_select, 'option', { value: warmup_options[0][0] }, ci18n('tournament:edit:warmup_timer_behavior:' + warmup_options[0][0]), { wo: warmup_options[0][0] }); - let warmup_marked = false; + const btp_password_label = uiu.el(btp_fieldset, 'label'); + uiu.el(btp_password_label, 'span', {}, ci18n('tournament:edit:btp:password')); + input.btp_password = uiu.el(btp_password_label, 'input', { + type: 'text', + name: 'btp_password', + value: (curt.btp_password || ''), + }); - const warmup_ready = uiu.el(form, 'label'); - uiu.el(warmup_ready, 'span', {}, ci18n('tournament:edit:warmup_ready')); - var warmup_ready_input = uiu.el(warmup_ready, 'input', { - type: 'number', - name: 'warmup_ready', - required: 'required', - disabled: warmup_options[0][3], - value: warmup_options[0][1], - 'class': 'ct_name', - }); + // BTP timezone + const btp_timezone_label = uiu.el(btp_fieldset, 'label'); + uiu.el(btp_timezone_label, 'span', {}, ci18n('tournament:edit:btp:timezone')); + const btp_timezone_select = uiu.el(btp_timezone_label, 'select', { + name: 'btp_timezone', + }); + uiu.el( + btp_timezone_select, 'option', { value: 'system' }, + ci18n('tournament:edit:btp:system timezone', { tz: curt.system_timezone })); + let marked = false; + for (const tz of timezones.ALL_TIMEZONES) { + const attrs = { + value: tz, + } - const warmup_start = uiu.el(form, 'label'); - uiu.el(warmup_start, 'span', {}, ci18n('tournament:edit:warmup_start')); - var warmup_start_input = uiu.el(warmup_start, 'input', { - type: 'number', - name: 'warmup_start', - required: 'required', - disabled: warmup_options[0][3], - value: warmup_options[0][2], - 'class': 'ct_name', - }); + if ((tz === curt.btp_timezone) && !marked) { + marked = true; + attrs.selected = 'selected'; + } - for (const wo of warmup_options.slice(1)) { - const attrs = { - value: wo[0], + uiu.el(btp_timezone_select, 'option', attrs, tz); + } + input.btp_timezone = btp_timezone_select; + } + + // tournament-flow-div################################################################################## + { + const tournament_flow_div = uiu.el(form, 'div', 'settings'); + uiu.el(tournament_flow_div, 'h2', 'edit', ci18n('tournament:edit:tournament_flow')); + // Warmup Timer + if (!curt.warmup_ready) { + curt.warmup_ready = 150; } - if ((wo[0] === curt.warmup) && !warmup_marked) { - warmup_marked = true; - attrs.selected = 'selected'; + if (!curt.warmup_start) { + curt.warmup_start = 180; + } - warmup_ready_input.value = wo[1]; - warmup_ready_input.disabled = wo[3]; - warmup_start_input.value = wo[2]; - warmup_start_input.disabled = wo[3]; + var warmup_options = [['bwf-2016', 90, 120, true], + ['legacy', 120, 120, true], + ['choise', curt.warmup_ready, curt.warmup_start, false], + ['call-down', curt.warmup_ready, curt.warmup_start, false], + ['call-up', 0, 0, true], + ['none', 0, 0, true]]; - last_selected_warmup = wo; - } + var last_selected_warmup = warmup_options[0]; - uiu.el(warmup_timer_select, 'option', attrs, ci18n('tournament:edit:warmup_timer_behavior:' + wo[0])); - } + const warmup_timer_label = uiu.el(tournament_flow_div, 'label'); + uiu.el(warmup_timer_label, 'span', {}, ci18n('tournament:edit:warmup_timer_behavior')); + const warmup_timer_select = uiu.el(warmup_timer_label, 'select', { + name: 'warmup', + }); + uiu.el(warmup_timer_select, 'option', { value: warmup_options[0][0] }, ci18n('tournament:edit:warmup_timer_behavior:' + warmup_options[0][0]), { wo: warmup_options[0][0] }); + let warmup_marked = false; + input.warmup = warmup_timer_select; + + const warmup_ready = uiu.el(tournament_flow_div, 'label'); + uiu.el(warmup_ready, 'span', {}, ci18n('tournament:edit:warmup_ready')); + var warmup_ready_input = uiu.el(warmup_ready, 'input', { + type: 'number', + name: 'warmup_ready', + required: 'required', + disabled: warmup_options[0][3], + value: warmup_options[0][1], + }); + input.warmup_ready = warmup_ready_input; + + const warmup_start = uiu.el(tournament_flow_div, 'label'); + uiu.el(warmup_start, 'span', {}, ci18n('tournament:edit:warmup_start')); + var warmup_start_input = uiu.el(warmup_start, 'input', { + type: 'number', + name: 'warmup_start', + required: 'required', + disabled: warmup_options[0][3], + value: warmup_options[0][2], + }); + input.warmup_start = warmup_start_input; - warmup_timer_select.onchange = function () { - if (!last_selected_warmup[3]) { - for (const wo of warmup_options) { - if (!wo[3]) { - wo[1] = warmup_ready_input.value; - wo[2] = warmup_start_input.value; - } + for (const wo of warmup_options.slice(1)) { + const attrs = { + value: wo[0], } - } - - for (const wo of warmup_options) { - if (warmup_timer_select.value == wo[0]) { + + if ((wo[0] === curt.warmup) && !warmup_marked) { + warmup_marked = true; + attrs.selected = 'selected'; + warmup_ready_input.value = wo[1]; warmup_ready_input.disabled = wo[3]; warmup_start_input.value = wo[2]; warmup_start_input.disabled = wo[3]; - + last_selected_warmup = wo; } + + uiu.el(warmup_timer_select, 'option', attrs, ci18n('tournament:edit:warmup_timer_behavior:' + wo[0])); } - }; - - const upcoming_fieldset = uiu.el(form, 'fieldset'); - uiu.el(upcoming_fieldset, 'h2', 'edit', ci18n('tournament:edit:upcoming_matches_settings')); - create_numeric_input(curt, form, 'upcoming_matches_animation_speed', 0, 10, 2, 1); - create_numeric_input(curt, form, 'upcoming_matches_animation_pause', 1, 20, 4, 1); - create_numeric_input(curt, form, 'upcoming_matches_max_count', 10, 50, 15, 1); + + warmup_timer_select.onchange = function () { + if (!last_selected_warmup[3]) { + for (const wo of warmup_options) { + if (!wo[3]) { + wo[1] = warmup_ready_input.value; + wo[2] = warmup_start_input.value; + } + } + } + + for (const wo of warmup_options) { + if (warmup_timer_select.value == wo[0]) { + warmup_ready_input.value = wo[1]; + warmup_ready_input.disabled = wo[3]; + warmup_start_input.value = wo[2]; + warmup_start_input.disabled = wo[3]; + + last_selected_warmup = wo; + } + } + }; - const bts_fieldset = uiu.el(form, 'fieldset'); - uiu.el(bts_fieldset, 'h2', 'edit', ci18n('tournament:edit:bts')); - create_checkbox(curt, bts_fieldset, 'call_preparation_matches_automatically_enabled'); - create_checkbox(curt, bts_fieldset, 'call_next_possible_scheduled_match_in_preparation'); + const bts_fieldset = uiu.el(tournament_flow_div, 'fieldset'); + input.call_preparation_matches_automatically_enabled = create_checkbox(curt, bts_fieldset, 'call_preparation_matches_automatically_enabled'); + input.call_next_possible_scheduled_match_in_preparation = create_checkbox(curt, bts_fieldset, 'call_next_possible_scheduled_match_in_preparation'); + + const tablet_fieldset = uiu.el(tournament_flow_div, 'fieldset'); + input.tabletoperator_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_enabled'); + input.tabletoperator_with_umpire_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_umpire_enabled'); + input.tabletoperator_winner_of_quaterfinals_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled'); + input.tabletoperator_use_manual_counting_boards_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_use_manual_counting_boards_enabled'); + input.tabletoperator_split_doubles = create_checkbox(curt, tablet_fieldset, 'tabletoperator_split_doubles'); + input.tabletoperator_with_state_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_state_enabled'); + input.tabletoperator_set_break_after_tabletservice = create_checkbox(curt, tablet_fieldset, 'tabletoperator_set_break_after_tabletservice'); + + if (!curt.tabletoperator_break_seconds) { + curt.tabletoperator_break_seconds = 300; + } + input.tabletoperator_break_seconds = create_input(curt, "number", tablet_fieldset, 'tabletoperator_break_seconds') - // BTP - const btp_fieldset = uiu.el(form, 'fieldset'); - uiu.el(btp_fieldset, 'h2', 'edit', ci18n('tournament:edit:btp')); - const btp_enabled_label = uiu.el(btp_fieldset, 'label'); - const ba_attrs = { - type: 'checkbox', - name: 'btp_enabled', - }; - if (curt.btp_enabled) { - ba_attrs.checked = 'checked'; } - uiu.el(btp_enabled_label, 'input', ba_attrs); - uiu.el(btp_enabled_label, 'span', {}, ci18n('tournament:edit:btp:enabled')); - const btp_autofetch_enabled_label = uiu.el(btp_fieldset, 'label'); - const bae_attrs = { - type: 'checkbox', - name: 'btp_autofetch_enabled', - }; - if (curt.btp_autofetch_enabled) { - bae_attrs.checked = 'checked'; - } - uiu.el(btp_autofetch_enabled_label, 'input', bae_attrs); - uiu.el(btp_autofetch_enabled_label, 'span', {}, ci18n('tournament:edit:btp:autofetch_enabled')); + // call-div################################################################################## + { + const call_div = uiu.el(form, 'div', 'settings'); + uiu.el(call_div, 'h2', 'edit', ci18n('tournament:edit:calls')); + + const announcements_fieldset = uiu.el(call_div, 'fieldset'); + input.annoncement_include_event = create_checkbox(curt, announcements_fieldset, 'annoncement_include_event'); + input.annoncement_include_round = create_checkbox(curt, announcements_fieldset, 'annoncement_include_round'); + input.annoncement_include_matchnumber = create_checkbox(curt, announcements_fieldset, 'annoncement_include_matchnumber'); + input.preparation_meetingpoint_enabled = create_checkbox(curt, announcements_fieldset, 'preparation_meetingpoint_enabled'); + input.preparation_tabletoperator_setup_enabled = create_checkbox(curt, announcements_fieldset, 'preparation_tabletoperator_setup_enabled'); - const btp_readonly_label = uiu.el(btp_fieldset, 'label'); - const bro_attrs = { - type: 'checkbox', - name: 'btp_readonly', - }; - if (curt.btp_readonly) { - bro_attrs.checked = 'checked'; - } - uiu.el(btp_readonly_label, 'input', bro_attrs); - uiu.el(btp_readonly_label, 'span', {}, ci18n('tournament:edit:btp:readonly')); + input.announcement_speed = create_numeric_input(curt, call_div, 'announcement_speed', 0.8, 1.3, 1.05, 0.01); + input.announcement_pause_time_ms = create_numeric_input(curt, call_div, 'announcement_pause_time_ms', 0.0, 5.0, 2.0, 0.1); - const btp_ip_label = uiu.el(btp_fieldset, 'label'); - uiu.el(btp_ip_label, 'span', {}, ci18n('tournament:edit:btp:ip')); - uiu.el(btp_ip_label, 'input', { - type: 'text', - name: 'btp_ip', - value: (curt.btp_ip || ''), - }); + render_normalisation_values(uiu.el(call_div, 'div','normalizations_values_div')); - const btp_password_label = uiu.el(btp_fieldset, 'label'); - uiu.el(btp_password_label, 'span', {}, ci18n('tournament:edit:btp:password')); - uiu.el(btp_password_label, 'input', { - type: 'text', - name: 'btp_password', - value: (curt.btp_password || ''), - }); + + } - // BTP timezone - const btp_timezone_label = uiu.el(btp_fieldset, 'label'); - uiu.el(btp_timezone_label, 'span', {}, ci18n('tournament:edit:btp:timezone')); - const btp_timezone_select = uiu.el(btp_timezone_label, 'select', { - name: 'btp_timezone', - }); - uiu.el( - btp_timezone_select, 'option', { value: 'system' }, - ci18n('tournament:edit:btp:system timezone', { tz: curt.system_timezone })); - let marked = false; - for (const tz of timezones.ALL_TIMEZONES) { - const attrs = { - value: tz, - } + // upcoming-div ################################################################################################### + { + const upcoming_div = uiu.el(form, 'div', 'settings'); + uiu.el(upcoming_div, 'h2', 'edit', ci18n('tournament:edit:upcoming_matches_settings')); - if ((tz === curt.btp_timezone) && !marked) { - marked = true; - attrs.selected = 'selected'; + const upcoming_fieldset = uiu.el(upcoming_div, 'fieldset'); + create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_animation_speed', 0, 10, 2, 1); + create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_animation_pause', 1, 20, 4, 1); + create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_max_count', 10, 50, 15, 1); + } + + + // devices-div################################################################################## + { + const devices_div = uiu.el(form, 'div', 'settings'); + uiu.el(devices_div, 'h2', 'edit', ci18n('tournament:edit:devices')); + + uiu.el(devices_div, 'h3', 'edit', ci18n('tournament:edit:logo')); + const logo_preview_container = uiu.el(devices_div, 'div', { + style: ( + 'position:relative;text-align:center;' + + 'height: 432px; width: 768px; font-size: 70px;' + + 'background:' + (curt.logo_background_color || '#000000') + ';' + + 'color:' + (curt.logo_foreground_color || '#aaaaaa') + ';' + ), + }); + if (curt.logo_id) { + uiu.el(logo_preview_container, 'img', { + style: 'height: 320px;', + src: '/h/' + encodeURIComponent(curt.key) + '/logo/' + curt.logo_id, + }); + uiu.el(logo_preview_container, 'div', {}, 'Court 42'); } - uiu.el(btp_timezone_select, 'option', attrs, tz); - } + const logo_form = uiu.el(devices_div, 'form'); + const logo_button = uiu.el(logo_form, 'input', { + type: 'file', + accept: 'image/*', + }); + logo_button.addEventListener('change', _upload_logo); + const logo_colors_container = uiu.el(logo_form, 'div', { style: 'display: block' }); + const bg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:background')); + const logo_background_color_input = uiu.el(bg_col_label, 'input', { + type: 'color', + name: 'logo_background_color', + value: curt.logo_background_color || '#000000', + }); + logo_background_color_input.addEventListener('change', (e) => { + send({ + type: 'tournament_edit_props', + key: curt.key, + props: { + logo_background_color: e.target.value, + }, + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + }); + const fg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:foreground')); + const fg_col_input = uiu.el(fg_col_label, 'input', { + type: 'color', + name: 'logo_foreground_color', + value: curt.logo_foreground_color || '#aaaaaa', + }); + fg_col_input.addEventListener('change', (e) => { + send({ + type: 'tournament_edit_props', + key: curt.key, + props: { + logo_foreground_color: e.target.value, + }, + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + }); - // Ticker - const ticker_fieldset = uiu.el(form, 'fieldset'); - uiu.el(ticker_fieldset, 'h2', 'edit', ci18n('tournament:edit:ticker')); - const ticker_enabled_label = uiu.el(ticker_fieldset, 'label'); - const te_attrs = { - type: 'checkbox', - name: 'ticker_enabled', - }; - if (curt.ticker_enabled) { - te_attrs.checked = 'checked'; - } - uiu.el(ticker_enabled_label, 'input', te_attrs); - uiu.el(ticker_enabled_label, 'span', {}, ci18n('tournament:edit:ticker_enabled')); + const default_display_fieldset = uiu.el(devices_div, 'fieldset'); + // Default display + const cur_dm_style = curt.dm_style || 'international'; + const dm_style_label = uiu.el(default_display_fieldset, 'label'); + uiu.el(dm_style_label, 'span', {}, ci18n('tournament:edit:dm_style')); + const dm_style_select = uiu.el(dm_style_label, 'select', { + name: 'dm_style', + required: 'required', + }); + const all_dm_styles = displaymode.ALL_STYLES; + for (const s of all_dm_styles) { + const s_attrs = { + value: s, + }; + if (s === cur_dm_style) { + s_attrs.selected = 'selected'; + } + uiu.el(dm_style_select, 'option', s_attrs, s); + } + input.dm_style = dm_style_select; - const ticker_url_label = uiu.el(ticker_fieldset, 'label'); - uiu.el(ticker_url_label, 'span', {}, ci18n('tournament:edit:ticker_url')); - uiu.el(ticker_url_label, 'input', { - type: 'text', - name: 'ticker_url', - value: (curt.ticker_url || ''), - }); + const displaysettings_style_label = uiu.el(default_display_fieldset, 'label'); + uiu.el(displaysettings_style_label, 'span', {}, ci18n('tournament:edit:displaysettings_general')); - const ticker_password_label = uiu.el(ticker_fieldset, 'label'); - uiu.el(ticker_password_label, 'span', {}, ci18n('tournament:edit:ticker_password')); - uiu.el(ticker_password_label, 'input', { - type: 'text', - name: 'ticker_password', - value: (curt.ticker_password || ''), - }); + input.displaysettings_general = createGeneralDisplaySettingsSelectBox(displaysettings_style_label, curt.displaysettings_general ? curt.displaysettings_general : "default"); + const general_displaysettings_div = uiu.el(devices_div, 'div', 'general_displaysettings'); + render_general_displaysettings(general_displaysettings_div); + render_displaysettings(devices_div); + } - const tablet_fieldset = uiu.el(form, 'fieldset'); - uiu.el(tablet_fieldset, 'h2', 'edit', ci18n('tournament:edit:tablets')); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_enabled'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_umpire_enabled'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_use_manual_counting_boards_enabled'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_split_doubles'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_state_enabled'); - create_checkbox(curt, tablet_fieldset, 'tabletoperator_set_break_after_tabletservice'); - create_checkbox(curt, tablet_fieldset, 'annoncement_include_event'); - create_checkbox(curt, tablet_fieldset, 'annoncement_include_round'); - create_checkbox(curt, tablet_fieldset, 'annoncement_include_matchnumber'); - create_checkbox(curt, tablet_fieldset, 'preparation_meetingpoint_enabled'); - create_checkbox(curt, tablet_fieldset, 'preparation_tabletoperator_setup_enabled'); - if (!curt.tabletoperator_break_seconds) { - curt.tabletoperator_break_seconds = 300; + // advertisement-div################################################################################## + { + const advertisement_div = uiu.el(form, 'div', 'settings'); + render_advertisements(advertisement_div); } - create_input(curt, "number", tablet_fieldset, 'tabletoperator_break_seconds') - create_numeric_input(curt, tablet_fieldset, 'announcement_speed', 0.8, 1.3, 1.05, 0.01); - create_numeric_input(curt, tablet_fieldset, 'announcement_pause_time_ms', 0.0, 5.0, 2.0, 0.1); - - uiu.el(form, 'button', { - role: 'submit', - }, ci18n('Change')); - form_utils.onsubmit(form, function (data) { - const props = { - name: data.name, - tguid: data.tguid, - language: data.language, - is_team: (!!data.is_team), - is_nation_competition: (!!data.is_nation_competition), - btp_enabled: (!!data.btp_enabled), - btp_autofetch_enabled: (!!data.btp_autofetch_enabled), - btp_readonly: (!!data.btp_readonly), - btp_ip: data.btp_ip, - btp_password: data.btp_password, - btp_timezone: data.btp_timezone, - dm_style: data.dm_style, - displaysettings_general: data.displaysettings_general, - warmup: data.warmup, - warmup_ready: data.warmup_ready, - warmup_start: data.warmup_start, - ticker_enabled: (!!data.ticker_enabled), - ticker_url: data.ticker_url, - ticker_password: data.ticker_password, - upcoming_matches_animation_speed: data.upcoming_matches_animation_speed, - upcoming_matches_max_count: data.upcoming_matches_max_count, - upcoming_matches_animation_pause: data.upcoming_matches_animation_pause, - tabletoperator_with_umpire_enabled: (!!data.tabletoperator_with_umpire_enabled), - tabletoperator_winner_of_quaterfinals_enabled: (!!data.tabletoperator_winner_of_quaterfinals_enabled), - tabletoperator_split_doubles: (!!data.tabletoperator_split_doubles), - tabletoperator_with_state_enabled: (!!data.tabletoperator_with_state_enabled), - tabletoperator_set_break_after_tabletservice: (!!data.tabletoperator_set_break_after_tabletservice), - tabletoperator_use_manual_counting_boards_enabled: (!!data.tabletoperator_use_manual_counting_boards_enabled), - annoncement_include_event: (!!data.annoncement_include_event), - annoncement_include_round: (!!data.annoncement_include_round), - annoncement_include_matchnumber: (!!data.annoncement_include_matchnumber), - tabletoperator_enabled: (!!data.tabletoperator_enabled), - tabletoperator_break_seconds: data.tabletoperator_break_seconds, - announcement_speed: data.announcement_speed, - announcement_pause_time_ms: data.announcement_pause_time_ms, - preparation_meetingpoint_enabled: (!!data.preparation_meetingpoint_enabled), - preparation_tabletoperator_setup_enabled: (!!data.preparation_tabletoperator_setup_enabled), - call_preparation_matches_automatically_enabled: (!!data.call_preparation_matches_automatically_enabled), - call_next_possible_scheduled_match_in_preparation: (!!data.call_next_possible_scheduled_match_in_preparation) + + // location-div################################################################################## + { + const location_div = uiu.el(form, 'div', 'settings'); + uiu.el(location_div, 'h2', 'edit', ci18n('tournament:edit:location')); + render_courts(location_div); + } + + // ticker-connection-div################################################################################## + { + const ticker_div = uiu.el(form, 'div', 'settings'); + uiu.el(ticker_div, 'h2', 'edit', ci18n('tournament:edit:ticker_connection')); + + const ticker_fieldset = uiu.el(ticker_div, 'fieldset'); + const ticker_enabled_label = uiu.el(ticker_fieldset, 'label'); + const te_attrs = { + type: 'checkbox', + name: 'ticker_enabled', }; - send({ - type: 'tournament_edit_props', - key: curt.key, - props: props, - }, function (err) { - if (err) { - return cerror.net(err); - } - ui_show(); + if (curt.ticker_enabled) { + te_attrs.checked = 'checked'; + } + input.ticker_enabled = uiu.el(ticker_enabled_label, 'input', te_attrs); + uiu.el(ticker_enabled_label, 'span', {}, ci18n('tournament:edit:ticker_enabled')); + + const ticker_url_label = uiu.el(ticker_fieldset, 'label'); + uiu.el(ticker_url_label, 'span', {}, ci18n('tournament:edit:ticker_url')); + input.ticker_url = uiu.el(ticker_url_label, 'input', { + type: 'text', + name: 'ticker_url', + value: (curt.ticker_url || ''), }); - }); - - const logo_preview_container = uiu.el(main, 'div', { - style: ( - 'float:right;position:relative;text-align:center;' + - 'height: 216px; width: 384px; font-size: 35px;' + - 'background:' + (curt.logo_background_color || '#000000') + ';' + - 'color:' + (curt.logo_foreground_color || '#aaaaaa') + ';' - ), - }); - if (curt.logo_id) { - uiu.el(logo_preview_container, 'img', { - style: 'height: 151px;', - src: '/h/' + encodeURIComponent(curt.key) + '/logo/' + curt.logo_id, + + const ticker_password_label = uiu.el(ticker_fieldset, 'label'); + uiu.el(ticker_password_label, 'span', {}, ci18n('tournament:edit:ticker_password')); + input.ticker_password = uiu.el(ticker_password_label, 'input', { + type: 'text', + name: 'ticker_password', + value: (curt.ticker_password || ''), }); - uiu.el(logo_preview_container, 'div', {}, 'Court 42'); } - uiu.el(main, 'h2', 'edit', ci18n('tournament:edit:logo')); - const logo_form = uiu.el(main, 'form'); - const logo_button = uiu.el(logo_form, 'input', { - type: 'file', - accept: 'image/*', - }); - logo_button.addEventListener('change', _upload_logo); - const logo_colors_container = uiu.el(logo_form, 'div', { style: 'display: block' }); - const bg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:background')); - const logo_background_color_input = uiu.el(bg_col_label, 'input', { - type: 'color', - name: 'logo_background_color', - value: curt.logo_background_color || '#000000', - }); - logo_background_color_input.addEventListener('change', (e) => { - send({ - type: 'tournament_edit_props', - key: curt.key, - props: { - logo_background_color: e.target.value, - }, - }, function (err) { + // save-div################################################################################## + { + const save_div = uiu.el(form, 'div', 'settings'); + uiu.el(save_div, 'h2', 'edit', ci18n('tournament:edit')); + + const save_btn = uiu.el(save_div, 'button', { + role: 'button', + }, ci18n('tournament:edit:save')); + save_btn.addEventListener('click', send_props(input, (err) => { if (err) { return cerror.net(err); } - }); - }); - const fg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:foreground')); - const fg_col_input = uiu.el(fg_col_label, 'input', { - type: 'color', - name: 'logo_foreground_color', - value: curt.logo_foreground_color || '#aaaaaa', - }); - fg_col_input.addEventListener('change', (e) => { - send({ - type: 'tournament_edit_props', - key: curt.key, - props: { - logo_foreground_color: e.target.value, - }, - }, function (err) { + })); + + const save_and_back_btn = uiu.el(save_div, 'button', { + role: 'button', + }, ci18n('tournament:edit:save_and_back')); + save_and_back_btn.addEventListener('click', send_props(input, (err) => { if (err) { return cerror.net(err); } - }); - }); - - render_courts(main); + ui_show(); + })); + + + /* + () => { + const props = { + name : input.name.value, + tguid: input.tguid.value, + language: input.language.value, + is_team: input.is_team.checked, + is_nation_competition: input.is_nation_competition.checked, + btp_enabled: input.btp_enabled.checked, + btp_autofetch_enabled: input.btp_autofetch_enabled.checked, + btp_readonly: input.btp_readonly.checked, + btp_ip: input.btp_ip.value, + btp_password: input.btp_password.value, + btp_timezone: input.btp_timezone.value, + dm_style: input.dm_style.value, + displaysettings_general: input.displaysettings_general.value, + warmup: input.warmup.value, + warmup_ready: input.warmup_ready.value, + warmup_start: input.warmup_start.value, + ticker_enabled: input.ticker_enabled.checked, + ticker_url: input.ticker_url.value, + ticker_password: input.ticker_password.value, + tabletoperator_enabled: input.tabletoperator_enabled.checked, + tabletoperator_with_umpire_enabled: input.tabletoperator_with_umpire_enabled.checked, + tabletoperator_winner_of_quaterfinals_enabled: input.tabletoperator_winner_of_quaterfinals_enabled.checked, + tabletoperator_split_doubles: input.tabletoperator_split_doubles.checked, + tabletoperator_with_state_enabled: input.tabletoperator_with_state_enabled.checked, + tabletoperator_set_break_after_tabletservice: input.tabletoperator_set_break_after_tabletservice.checked, + tabletoperator_use_manual_counting_boards_enabled: input.tabletoperator_use_manual_counting_boards_enabled.checked, + tabletoperator_break_seconds: input.tabletoperator_break_seconds.value, + annoncement_include_event: input.annoncement_include_event.checked, + annoncement_include_round: input.annoncement_include_round.checked, + annoncement_include_matchnumber: input.annoncement_include_matchnumber.checkt, + announcement_speed: input.announcement_speed.value, + announcement_pause_time_ms: input.announcement_pause_time_ms.value, + preparation_meetingpoint_enabled: input.preparation_meetingpoint_enabled.checked, + preparation_tabletoperator_setup_enabled: input.preparation_tabletoperator_setup_enabled.checked, + call_preparation_matches_automatically_enabled: input.call_preparation_matches_automatically_enabled.checked, + call_next_possible_scheduled_match_in_preparation: input.call_next_possible_scheduled_match_in_preparation.checked + } - const general_displaysettings_div = uiu.el(main, 'div', 'general_displaysettings'); - render_general_displaysettings(general_displaysettings_div); - render_displaysettings(main); - render_normalisation_values(uiu.el(main, 'div', 'normalizations_values_div')); - render_advertisements(uiu.el(main, 'div', 'advertisements_div')); + send({ + type: 'tournament_edit_props', + key: curt.key, + props: props, + }, function (err) { + if (err) { + return cerror.net(err); + } + ui_show(); + }); + + }); + */ + } } _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit, change.default_handler(_update_all_ui_elements_edit, { update_general_displaysettings: update_general_displaysettings, })); + function send_props(input, callback) { + const props = { + name : input.name.value, + tguid: input.tguid.value, + language: input.language.value, + is_team: input.is_team.checked, + is_nation_competition: input.is_nation_competition.checked, + btp_enabled: input.btp_enabled.checked, + btp_autofetch_enabled: input.btp_autofetch_enabled.checked, + btp_readonly: input.btp_readonly.checked, + btp_ip: input.btp_ip.value, + btp_password: input.btp_password.value, + btp_timezone: input.btp_timezone.value, + dm_style: input.dm_style.value, + displaysettings_general: input.displaysettings_general.value, + warmup: input.warmup.value, + warmup_ready: input.warmup_ready.value, + warmup_start: input.warmup_start.value, + ticker_enabled: input.ticker_enabled.checked, + ticker_url: input.ticker_url.value, + ticker_password: input.ticker_password.value, + upcoming_matches_animation_speed: data.upcoming_matches_animation_speed, + upcoming_matches_max_count: data.upcoming_matches_max_count, + upcoming_matches_animation_pause: data.upcoming_matches_animation_pause, + tabletoperator_enabled: input.tabletoperator_enabled.checked, + tabletoperator_with_umpire_enabled: input.tabletoperator_with_umpire_enabled.checked, + tabletoperator_winner_of_quaterfinals_enabled: input.tabletoperator_winner_of_quaterfinals_enabled.checked, + tabletoperator_split_doubles: input.tabletoperator_split_doubles.checked, + tabletoperator_with_state_enabled: input.tabletoperator_with_state_enabled.checked, + tabletoperator_set_break_after_tabletservice: input.tabletoperator_set_break_after_tabletservice.checked, + tabletoperator_use_manual_counting_boards_enabled: input.tabletoperator_use_manual_counting_boards_enabled.checked, + tabletoperator_break_seconds: input.tabletoperator_break_seconds.value, + annoncement_include_event: input.annoncement_include_event.checked, + annoncement_include_round: input.annoncement_include_round.checked, + annoncement_include_matchnumber: input.annoncement_include_matchnumber.checkt, + announcement_speed: input.announcement_speed.value, + announcement_pause_time_ms: input.announcement_pause_time_ms.value, + preparation_meetingpoint_enabled: input.preparation_meetingpoint_enabled.checked, + preparation_tabletoperator_setup_enabled: input.preparation_tabletoperator_setup_enabled.checked, + call_preparation_matches_automatically_enabled: input.call_preparation_matches_automatically_enabled.checked, + call_next_possible_scheduled_match_in_preparation: input.call_next_possible_scheduled_match_in_preparation.checked, + } + + send({ + type: 'tournament_edit_props', + key: curt.key, + props: props, + }, (err) => { + return callback(err); + }); + } + function render_normalisation_values(main) { uiu.el(main, 'h2','edit', ci18n('tournament:edit:normalizations')); @@ -1208,7 +1347,7 @@ var ctournament = (function() { } function render_general_displaysettings(main) { - uiu.el(main, 'h2', 'edit', ci18n('tournament:edit:general_displaysettings')); + uiu.el(main, 'h3', 'edit', ci18n('tournament:edit:general_displaysettings')); const display_settings_table = uiu.el(main, 'table'); const display_settings_tbody = uiu.el(display_settings_table, 'tbody'); const tr = uiu.el(display_settings_tbody, 'tr'); @@ -1263,7 +1402,7 @@ var ctournament = (function() { } function render_displaysettings(general_displaysettings_div) { - uiu.el(general_displaysettings_div, 'h2', 'edit', ci18n('tournament:edit:displays')); + uiu.el(general_displaysettings_div, 'h3', 'edit', ci18n('tournament:edit:displays')); const display_table = uiu.el(general_displaysettings_div, 'table'); const display_tbody = uiu.el(display_table, 'tbody'); @@ -1387,18 +1526,20 @@ var ctournament = (function() { if (curt[filed_id]) { attrs.checked = 'checked'; } - uiu.el(label, 'input', attrs); + const result = uiu.el(label, 'input', attrs); uiu.el(label, 'span', {}, ci18n('tournament:edit:' + filed_id)); + return result; } function create_input(curt, type, parent_el, filed_id) { const text_input = uiu.el(parent_el, 'label'); uiu.el(text_input, 'span', {}, ci18n('tournament:edit:' + filed_id)); - uiu.el(text_input, 'input', { + const result = uiu.el(text_input, 'input', { type: type, name: filed_id, value: curt[filed_id] || '', }); + return result; } function create_undecorated_input(type, parent_el, filed_id) { @@ -1413,7 +1554,7 @@ var ctournament = (function() { function create_numeric_input(curt, parent_el, filed_id, min_value, max_value, default_value, step_value) { const text_input = uiu.el(parent_el, 'label'); uiu.el(text_input, 'span', {}, ci18n('tournament:edit:' + filed_id)); - uiu.el(text_input, 'input', { + return uiu.el(text_input, 'input', { type: "number", name: filed_id, value: curt[filed_id] || default_value, @@ -1496,7 +1637,7 @@ var ctournament = (function() { name: 'displaysettings_general' }); createSelectBoxContent(displaysettings_select_box, curt.displaysettings, displaysettings_id); - + return displaysettings_select_box; } function createSelectBoxContent(select_box, content, selected_id) { for (const item of content) { From 03f896e2ae424b560601133941d7ce9bf86d623a Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sun, 12 Jan 2025 20:13:32 +0100 Subject: [PATCH 177/224] improve the settings view --- bts/admin.js | 19 ++ bts/bupws.js | 1 + static/css/admin.css | 7 + static/js/ci18n_de.js | 35 ++++ static/js/ci18n_en.js | 37 ++++ static/js/cmatch.js | 4 +- static/js/ctournament.js | 402 ++++++++++++++++++++++++++++++++++++--- 7 files changed, 476 insertions(+), 29 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index a2a2477..e4c022c 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -794,6 +794,23 @@ function handle_reset_display(app, ws, msg) { ws.respond("Angekommen: " + client_id); } +function handle_edit_display_setting(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'displaysetting'])) { + return; + } + const querry = {id : msg.displaysetting.id}; + const displaysetting = msg.displaysetting; + + //{_id: msg.id, tournament_key}, {$set: {setup}}, {returnUpdatedDocs: true}, function(err, numAffected, changed_match) + app.db.displaysettings.update(querry, {$set: displaysetting}, {returnUpdatedDocs: true}, (err, numAffected, changed_setting) => { + console.log(err); + console.log(numAffected); + console.log(changed_setting); + }); + + ws.respond(msg); +} + async function async_handle_delete_display_setting(app, ws, msg) { const tournament_key = msg.tournament_key; const setting_id = msg.setting_id; @@ -820,6 +837,7 @@ function handle_relocate_display(app, ws, msg) { bupws.restart_panel(app, tournament_key, client_id, new_court_id); ws.respond("Angekommen: " + client_id); } + function handle_change_display_mode(app, ws, msg) { const tournament_key = msg.tournament_key; const client_id = msg.display_setting_id; @@ -991,6 +1009,7 @@ async function async_handle_tournament_upload_logo(app, ws, msg) { } module.exports = { + handle_edit_display_setting, async_handle_delete_display_setting, async_handle_match_delete, async_handle_tournament_upload_logo, diff --git a/bts/bupws.js b/bts/bupws.js index 9d49727..57922bb 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -38,6 +38,7 @@ async function notify_admin_display_status_changed(app, ws, ws_online) { var display_court_displaysetting = await get_display_court_displaysettings(app, client_id); if (display_court_displaysetting == null) { display_court_displaysetting = create_display_court_displaysettings(client_id, hostname, null, generate_default_displaysettings_id(tournament)); + display_court_displaysetting = await persist_client_court_displaysetting(app, display_court_displaysetting); } display_court_displaysetting.online = ws_online; admin.notify_change(app, default_tournament_key, 'display_status_changed', {'display_court_displaysetting': display_court_displaysetting }); diff --git a/static/css/admin.css b/static/css/admin.css index 2d4b42b..1c408af 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -46,6 +46,13 @@ h3 { margin: 20px 0 10px 0; } +button { + margin-right: 5px; + margin-left: 5px; + margin-top: 2px; + margin-bottom: 2px; +} + .connecting { position: fixed; top: 45%; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 25ff7df..e52418b 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -29,6 +29,7 @@ var ci18n_de = { 'No umpire': 'Kein Schiedsrichter', 'No service judge': 'Kein Aufschlagrichter', 'Edit match': 'Match bearbeiten', + 'Edit display setting': 'Anzeigeeinstellung bearbeiten', 'Back': 'Zurück', 'PDF': 'PDF', 'Print': 'Drucken', @@ -82,6 +83,40 @@ var ci18n_de = { 'announcements:lang': 'de-DE', 'tournament:edit:add': 'Hinzufügen', 'tournament:edit:delete':'Löschen', + + 'display_setting:id': 'ID: ', + 'display_setting:devicemode': 'Betriebsmodus: ', + 'display_setting:style': 'Erscheinungsbild: ', + 'display_setting:show_pause': 'Zeige verbleibende Pausenzeit', + 'display_setting:show_court_number': 'Zeige Spielfeldnummer', + 'display_setting:show_competition': 'Zeige die Konkurenz', + 'display_setting:show_round': 'Zeige die Runde', + 'display_setting:show_middle_name': 'Zeige den zweiten Vornamen der Spieler', + 'display_setting:show_doubles_receiving': 'Unterstreiche den annehmenden Spieler im Doppel', + 'display_setting:colors': 'Farben: ', + 'display_setting:use_team_colors': 'Verwende Team Farben', + 'display_setting:scale': 'Skalierung: ', + 'display_setting:language': 'Sprache: ', + 'display_setting:language_automatic': 'Automatic', + 'display_setting:language_en' : 'English', + 'display_setting:language_de' : 'Deutsch (Deutschland)', + 'display_setting:language_de-AT' : 'Deutsch (Österreich)', + 'display_setting:language_de-CH' : 'Deutsch (Schweiz)', + 'display_setting:language_fr-CH' : 'Français (Suisse)', + 'display_setting:language_nl-BE' : 'Vlaams', + 'display_setting:fullscreen_ask' : 'Vollbildmodus anfragen: ', + 'display_setting:show_announcements': 'Ansagen zeigen: ', + 'display_setting:autohide': 'Verberge Einstellungen nach (ms): ', + 'display_setting:click_mode': 'Touch-Erkennung: ', + 'display_setting:double_click_timeout': 'Doppel-Touch-Sperre (ms): ', + 'display_setting:button_block_timeout': 'Doppel-Touch-Sperre (ms): ', + 'display_setting:negative_timers': 'Stoppuhr läuft bis zum nächsten Punkt (auch negative Werte anzeigen)', + 'display_setting:shuttle_counter': 'Federballzähler anzeigen', + 'display_setting:editmode_doubleclick': 'Manuelles bearbeiten mit Doppelklick auf Feld', + 'display_setting:settings_style': 'Oberfläche: ', + 'display_setting:network_timeout': 'Netzwerk-Timeout (ms): ', + 'display_setting:network_update_interval': 'Netzwerk-Wiederholungszeit (ms): ', + 'display_setting:displaymode_update_interval': 'Displaymode Update Intervall (ms): ', 'tournament:edit:tournament': 'Turnier:', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 01aacbe..478ec74 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -29,6 +29,7 @@ var ci18n_en = { 'No umpire': 'No umpire', 'No service judge': 'No service judge', 'Edit match': 'Edit match', + 'Edit display setting': 'Edit display setting', 'Back': 'Back', 'PDF': 'PDF', 'Print': 'Print', @@ -82,6 +83,42 @@ var ci18n_en = { 'announcements:lang': 'en-EN', 'tournament:edit:add': 'Add', 'tournament:edit:delete': 'Delete', + + + 'display_setting:id': 'ID:', + 'display_setting:wakelock': 'Operating mode: ', + 'display_setting:style': 'Style: ', + 'display_setting:show_pause': 'Show interval timer', + 'display_setting:show_court_number': 'Show court number', + 'display_setting:show_competition': 'Show the competition', + 'display_setting:show_round': 'Show the round', + 'display_setting:show_middle_name': 'Show middle names of players', + 'display_setting:show_doubles_receiving': 'Underline the receiving player in doubles', + 'display_setting:colors': 'Colors: : ', + 'display_setting:use_team_colors': 'Use team colors', + 'display_setting:scale': 'Scale: : ', + 'display_setting:language': 'Language: ', + 'display_setting:language_automatic': 'Automatic', + 'display_setting:language_en': 'English', + 'display_setting:language_de-DE': 'Deutsch (Deutschland)', + 'display_setting:language_de-AT': 'Deutsch (Österreich)', + 'display_setting:language_de-CH': 'Deutsch (Schweiz)', + 'display_setting:language_fr-CH': 'Français (Suisse)', + 'display_setting:language_nl-BE': 'Vlaams', + 'display_setting:fullscreen_ask': 'Request fullscrean on startup: ', + 'display_setting:show_announcements': 'Show announcements: ', + 'display_setting:autohide': 'Verberge Einstellungen nach (ms): ', + 'display_setting:double_click_timeout': 'Doppel-Touch-Sperre (ms): ', + 'display_setting:button_block_timeout': 'Double press protection(ms): ', + 'display_setting:negative_timers': 'Timers go into negative: ', + 'display_setting:shuttle_counter': 'Show shuttle counter: ', + 'display_setting:editmode_doubleclick': 'Touch-Erkennung: ', + 'display_setting:settings_style': 'User Interface: ', + 'display_setting:network_timeout': 'Network timeout (sm): ', + 'display_setting:network_update_interval': 'Network repeat interval (ms): ', + 'display_setting:displaymode_update_interval': 'Displaymode Update Intervall (ms): ', + + 'tournament:edit:id': 'Tournament id:', 'tournament:edit:language': 'Language:', 'tournament:edit:language:auto': 'Not set (browser default)', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index ba64a4f..9052c5c 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -32,7 +32,7 @@ function auto_scroll() { return; } - const scroll_speed = parseInt(curt.upcoming_matches_animation_speed ? curt.upcoming_matches_animation_speed : 2); + const scroll_speed = parseInt((curt && curt.upcoming_matches_animation_speed) ? curt.upcoming_matches_animation_speed : 2); if (scroll_speed == 0) { return; } @@ -79,7 +79,7 @@ function pause_scroll() { setTimeout(() => { is_paused = false; auto_scroll(); - }, parseInt(curt.upcoming_matches_animation_pause ? curt.upcoming_matches_animation_pause : 4) * 1000); + }, parseInt((curt && curt.upcoming_matches_animation_pause) ? curt.upcoming_matches_animation_pause : 4) * 1000); } function resize_table(resizable_rows, table_width_factor) { diff --git a/static/js/ctournament.js b/static/js/ctournament.js index d6e842e..0c22fd6 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -896,9 +896,9 @@ var ctournament = (function() { uiu.el(upcoming_div, 'h2', 'edit', ci18n('tournament:edit:upcoming_matches_settings')); const upcoming_fieldset = uiu.el(upcoming_div, 'fieldset'); - create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_animation_speed', 0, 10, 2, 1); - create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_animation_pause', 1, 20, 4, 1); - create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_max_count', 10, 50, 15, 1); + input.upcoming_animation_speed = create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_animation_speed', 0, 10, 2, 1); + input.upcoming_animation_pause = create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_animation_pause', 1, 20, 4, 1); + input.upcoming_matches_max_count = create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_max_count', 10, 50, 15, 1); } @@ -1059,22 +1059,27 @@ var ctournament = (function() { const save_btn = uiu.el(save_div, 'button', { role: 'button', }, ci18n('tournament:edit:save')); - save_btn.addEventListener('click', send_props(input, (err) => { - if (err) { - return cerror.net(err); - } - })); + save_btn.addEventListener('click', () => { + send_props(input, (err) => { + if (err) { + cerror.net(err); // Fehlerbehandlung + } + }); + }); const save_and_back_btn = uiu.el(save_div, 'button', { role: 'button', }, ci18n('tournament:edit:save_and_back')); - save_and_back_btn.addEventListener('click', send_props(input, (err) => { - if (err) { - return cerror.net(err); - } + save_and_back_btn.addEventListener('click', () => { + send_props(input, (err) => { + if (err) { + cerror.net(err); // Fehlerbehandlung + } - ui_show(); - })); + ui_show(); + }); + }); + /* @@ -1138,6 +1143,8 @@ var ctournament = (function() { })); function send_props(input, callback) { + console.log("send_props()"); + const props = { name : input.name.value, tguid: input.tguid.value, @@ -1158,9 +1165,9 @@ var ctournament = (function() { ticker_enabled: input.ticker_enabled.checked, ticker_url: input.ticker_url.value, ticker_password: input.ticker_password.value, - upcoming_matches_animation_speed: data.upcoming_matches_animation_speed, - upcoming_matches_max_count: data.upcoming_matches_max_count, - upcoming_matches_animation_pause: data.upcoming_matches_animation_pause, + upcoming_matches_animation_speed: input.upcoming_animation_speed.value, + upcoming_matches_max_count: input.upcoming_matches_max_count.value, + upcoming_matches_animation_pause: input.upcoming_animation_pause.value, tabletoperator_enabled: input.tabletoperator_enabled.checked, tabletoperator_with_umpire_enabled: input.tabletoperator_with_umpire_enabled.checked, tabletoperator_winner_of_quaterfinals_enabled: input.tabletoperator_winner_of_quaterfinals_enabled.checked, @@ -1356,17 +1363,15 @@ var ctournament = (function() { for (const s of curt.displaysettings) { const tr = uiu.el(display_settings_tbody, 'tr'); - uiu.el(tr, 'th', {}, s.id); - const description_td = uiu.el(tr, 'td', {}, '40" Fernseher (Platzhalter)'); + uiu.el(tr, 'th', {}, s.description ||s.id); + const description_td = uiu.el(tr, 'td', {}, s.devicemode + (s.devicemode == 'display' ? ' (' + s.displaymode_style + ')' : '')); const actions_td = uiu.el(tr, 'td', {}); const edit_btn = uiu.el(actions_td, 'button', { - 'data-display-setting-id': s.id, + 'data-display_setting_id': s.id, }, 'Edit'); - edit_btn.addEventListener('click', (e) => { - const edt_btn = e.target; - const display_setting_id = edt_btn.getAttribute('data-display-setting-id'); - + edit_btn.addEventListener('click', (e) => { + on_edit_display_setting_button_click(e); }); @@ -1394,11 +1399,352 @@ var ctournament = (function() { } } + function _cancel_ui_edit_display_setting() { + const dlg = document.querySelector('.display_setting_edit_dialog'); + if (!dlg) { + return; // Already cancelled + } + cbts_utils.esc_stack_pop(); + uiu.remove(dlg); + + crouting.set('t/:key/edit/', { key: curt.key }); + } + + function on_edit_display_setting_button_click(e) { + const btn = e.target; + const display_setting_id = btn.getAttribute('data-display_setting_id'); + console.log(display_setting_id); + ui_edit_display_setting(display_setting_id); + } + + function ui_edit_display_setting(display_setting_id) { + console.log(display_setting_id); + console.log(curt); + const display_setting = structuredClone(utils.find(curt.displaysettings, d => d.id === display_setting_id)); + console.log(display_setting); + + crouting.set('t/' + curt.key + '/edit/s/' + display_setting_id, {}, _cancel_ui_edit_display_setting); + + cbts_utils.esc_stack_push(_cancel_ui_edit_display_setting); + + const body = uiu.qs('body'); + const dialog_bg = uiu.el(body, 'div', 'dialog_bg display_setting_edit_dialog', { + 'data-display_setting_id': display_setting_id, + }); + const dialog = uiu.el(dialog_bg, 'div', 'dialog'); + + uiu.el(dialog, 'h3', {}, ci18n('Edit display setting')); + + const form = uiu.el(dialog, 'form'); + uiu.el(form, 'input', { + type: 'hidden', + name: 'display_setting_id', + value: display_setting_id, + }); + render_edit_display_setting(form, display_setting); + + const buttons = uiu.el(form, 'div', { + style: 'margin-top: 2em;', + }); + + const btn = uiu.el(buttons, 'button', { + 'class': 'match_save_button', + role: 'submit', + }, ci18n('Change')); + + form_utils.onsubmit(form, function(d) { + console.log(d); + const displaysetting = create_displaysettings_object(d); + console.log(displaysetting); + + send({ + type: 'edit_display_setting', + tournament_key: curt.key, + displaysetting: displaysetting, + }, err => { + if (err) { + return cerror.net(err); + } + console.log("call _cancel_ui_edit_display_setting()"); + _cancel_ui_edit_display_setting(); + }); + }); + + const cancel_btn = uiu.el(buttons, 'span', 'match_cancel_link vlink', ci18n('Cancel')); + cancel_btn.addEventListener('click', _cancel_ui_edit_display_setting); + } + crouting.register(/t\/([a-z0-9]+)\/edit\/s\/([-a-zA-Z0-9_ ]+)$/, function(m) { + ctournament.switch_tournament(m[1], function() { + ui_edit_display_setting(m[2]); + }); + }, change.default_handler(() => { + const dlg = uiu.qs('.display_setting_edit_dialog'); + const display_setting_id = dlg.getAttribute('data-display_setting_id'); + ui_edit_display_setting(display_setting_id); + })); + + function render_edit_display_setting(form, display_setting) { + + const edit_display_setting_container = uiu.el(form, 'div', 'edit_display_setting_container'); + const id_div = uiu.el(edit_display_setting_container, 'div'); + uiu.el(id_div, 'span', 'display_setting_id', ci18n('display_setting:id')); + uiu.el(id_div, 'input', { + type: 'text', + name: 'display_setting_id', + size: 24, + required: 'required', + value: display_setting.id || '', + tabindex: 1, + disabled: 'disabled', + }); + + + const description_div = uiu.el(edit_display_setting_container, 'div'); + uiu.el(description_div, 'span', 'display_setting_description', 'Description:'); + uiu.el(description_div, 'input', { + type: 'text', + name: 'display_setting_description', + placeholder: ci18n('e.g. MX O55'), + size: 18, + value: display_setting.description || '', + tabindex: 2, + }); + + const ALL_DEVICE_MODES = [ + 'umpire', + 'display' + ]; + render_drop_down(edit_display_setting_container, ci18n('display_setting:devicemode'), 'devicemode', ALL_DEVICE_MODES, display_setting.devicemode || ''); + render_drop_down(edit_display_setting_container, ci18n('display_setting:style'), 'displaymode_style', displaymode.ALL_STYLES, display_setting.displaymode_style || ''); + render_check_box(edit_display_setting_container, ci18n('display_setting:show_pause'), 'd_show_pause', display_setting.d_show_pause); + render_check_box(edit_display_setting_container, ci18n('display_setting:show_court_number'), 'd_show_court_number', display_setting.d_show_court_number); + render_check_box(edit_display_setting_container, ci18n('display_setting:show_competition'), 'd_show_competition', display_setting.d_show_competition); + render_check_box(edit_display_setting_container, ci18n('display_setting:show_round'), 'd_show_round', display_setting.d_show_round); + render_check_box(edit_display_setting_container, ci18n('display_setting:show_middle_name'), 'd_show_middle_name', display_setting.d_show_middle_name); + render_check_box(edit_display_setting_container, ci18n('display_setting:show_doubles_receiving'), 'd_show_doubles_receiving', display_setting.d_show_doubles_receiving); + + const select_color_div = uiu.el(edit_display_setting_container, 'div', { style: 'display: block' }); + const select_color_label = uiu.el(select_color_div, 'label', {}, ci18n('display_setting:colors')); + render_select_color(select_color_label, 'd_c0', display_setting.d_c0); + render_select_color(select_color_label, 'd_c1', display_setting.d_c1); + render_select_color(select_color_label, 'd_cb0', display_setting.d_cb0); + render_select_color(select_color_label, 'd_cb1', display_setting.d_cb1); + render_select_color(select_color_label, 'd_cbg', display_setting.d_cbg); + render_select_color(select_color_label, 'd_cbg2', display_setting.d_cbg2); + render_select_color(select_color_label, 'd_cbg3', display_setting.d_cbg3); + render_select_color(select_color_label, 'd_cbg4', display_setting.d_cbg4); + render_select_color(select_color_label, 'd_cfg', display_setting.d_cfg); + render_select_color(select_color_label, 'd_cfg2', display_setting.d_cfg2); + render_select_color(select_color_label, 'd_cfg3', display_setting.d_cfg3); + render_select_color(select_color_label, 'd_cfg4', display_setting.d_cfg4); + render_select_color(select_color_label, 'd_cfgdark', display_setting.d_cfgdark); + render_select_color(select_color_label, 'd_cexpt', display_setting.d_cexpt); + render_select_color(select_color_label, 'd_ct', display_setting.d_ct); + render_select_color(select_color_label, 'd_cborder', display_setting.d_cborder); + render_select_color(select_color_label, 'd_cserv', display_setting.d_cserv); + render_select_color(select_color_label, 'd_cserv2', display_setting.d_cserv2); + render_select_color(select_color_label, 'd_crecv', display_setting.d_crecv); + render_select_color(select_color_label, 'd_ctim_blue', display_setting.d_ctim_blue); + render_select_color(select_color_label, 'd_ctim_active', display_setting.d_ctim_active); + render_check_box(edit_display_setting_container, ci18n('display_setting:use_team_colors'), 'd_team_colors', display_setting.d_team_colors); + render_select_number(edit_display_setting_container, ci18n('display_setting:scale'), 'd_scale', display_setting.d_scale, 20, 500); + + const ALL_BUP_LANGUAGES = [ + ci18n('display_setting:language_automatic'), + ci18n('display_setting:language_en'), + ci18n('display_setting:language_de'), + ci18n('display_setting:language_de-AT'), + ci18n('display_setting:language_de-CH'), + ci18n('display_setting:language_fr-CH'), + ci18n('display_setting:language_nl-BE'), + ] + + const SHORT_BUP_LANGUAGES = [ + 'auto', + 'en', + 'de', + 'de-AT', + 'de-CH', + 'fr-CH', + 'nl-BE' + ] + + // let current_language = ''; + + // for (const [i, value] of SHORT_BUP_LANGUAGES.entries()) { + // if ((display_setting.language || '') == value) { + // current_language = ALL_BUP_LANGUAGES[i]; + // break; + // } + // } + + render_drop_down(edit_display_setting_container, ci18n('display_setting:language'), 'language', SHORT_BUP_LANGUAGES, display_setting.language, ALL_BUP_LANGUAGES); + + + const ALL_ASK_FULLSCREAN_MODES = [ + 'always', + 'auto', + 'never', + ]; + render_drop_down(edit_display_setting_container, ci18n('display_setting:fullscreen_ask'), 'fullscreen_ask', ALL_ASK_FULLSCREAN_MODES, display_setting.fullscreen_ask || ''); + + + const ALL_ANNOUNCEMENT_MODES = [ + 'none', + 'all', + 'except-first', + ]; + render_drop_down(edit_display_setting_container, ci18n('display_setting:show_announcements'), 'show_announcements', ALL_ANNOUNCEMENT_MODES, display_setting.show_announcements || ''); + + render_select_number(edit_display_setting_container, ci18n('display_setting:scale'), 'd_scale', display_setting.d_scale, 20, 500); + render_select_number(edit_display_setting_container, ci18n('display_setting:button_block_timeout'), 'button_block_timeout', display_setting.button_block_timeout, 0, 5000); + + render_check_box(edit_display_setting_container, ci18n('display_setting:negative_timers'), 'negative_timers', display_setting.negative_timers); + render_check_box(edit_display_setting_container, ci18n('display_setting:shuttle_counter'), 'shuttle_counter', display_setting.shuttle_counter); + render_check_box(edit_display_setting_container, ci18n('display_setting:editmode_doubleclick'), 'editmode_doubleclick', display_setting.editmode_doubleclick); + + const ALL_CLICK_MODES = [ + 'auto', + 'click', + 'touchstart', + 'touchend', + ]; + render_drop_down(edit_display_setting_container, ci18n('display_setting:click_mode'), 'click_mode', ALL_CLICK_MODES, display_setting.click_mode || ''); + + const ALL_STYLE_MODES = [ + 'default', + 'complete', + 'clean', + 'focus', + 'hidden', + ]; + render_drop_down(edit_display_setting_container, ci18n('display_setting:settings_style'), 'style', ALL_STYLE_MODES, display_setting.settings_style || ''); + render_select_number(edit_display_setting_container, ci18n('display_setting:network_timeout'), 'network_timeout', display_setting.network_timeout, 1, 600000); + render_select_number(edit_display_setting_container, ci18n('display_setting:network_update_interval'), 'network_update_interval', display_setting.network_update_interval, 1, 600000); + } + + function render_drop_down(container, label_text, select_name, values, curval, labels) { + if(!labels) { + labels = values; + } + + const div = uiu.el(container, 'div'); + uiu.el(div, 'span', 'label', label_text); + const select = uiu.el(div, 'select', { + name: select_name, + size: 1, + }); + uiu.empty(select); + for (const [i, s] of values.entries()) { + const attrs = { + value: s, + label: labels[i] || s, + }; + if (s === curval) { + attrs.selected = 'selected'; + } + uiu.el(select, 'option', attrs, s); + } + } + + function render_check_box(container, label_text, checkbox_name, is_checked) { + const div = uiu.el(container, 'div'); + const label = uiu.el(div, 'label'); + const attrs = { + type: 'checkbox', + name: checkbox_name, + }; + + if (is_checked) { + attrs.checked = 'checked'; + } + + uiu.el(label, 'input', attrs); + uiu.el(label, 'span', 'display_setting_label', label_text); + } + + function render_select_color(container, field_name, value) { + const input = uiu.el(container, 'input', { + type: 'color', + name: field_name, + value: value || '#000000', + }); + } + + function render_select_number(container, label_text, input_name, value, min_value, max_value) { + const div = uiu.el(container, 'div'); + const label = uiu.el(div, 'span', 'label', label_text); + uiu.el(label, 'input', { + type: 'number', + name: input_name, + min: min_value || 0, + max: max_value || 0, + value: value || 0, + }); + } + + function create_displaysettings_object(d) { + const displaysetting = { + id: d.display_setting_id, + description: d.display_setting_description || '', + devicemode: d.devicemode || 'display', + displaymode_style: d.displaymode_style || 'tournamentcourt', + d_show_pause: d.d_show_pause == 'on' ? true : false, + d_show_court_number: d.d_show_court_number == 'on' ? true : false, + d_show_competition: d.d_show_competition == 'on' ? true : false, + d_show_round: d.d_show_round == 'on' ? true : false, + d_show_middle_name: d.d_show_middle_name == 'on' ? true : false, + d_show_doubles_receiving: d.d_show_doubles_receiving == 'on' ? true : false, + d_c0: d.d_c0 || '#50e87d', + d_c1: d.d_c1 || '#f76a23', + d_cb0: d.d_cb0 || '#000000', + d_cb1: d.d_cb1 || '#000000', + d_cbg: d.d_cbg || '#000000', + d_cbg2: d.d_cbg2 || '#d9d9d9', + d_cbg3: d.d_cbg3 || '#252525', + d_cbg4: d.d_cbg4 || '#404040', + d_cfg: d.d_cfg || '#ffffff', + d_cfg2: d.d_cfg2 || '#aaaaaa', + d_cfg3: d.d_cfg3 || '#cccccc', + d_cfg4: d.d_cfg4 || '#000000', + d_cfgdark: d.d_cfgdark || '#000000', + d_cexpt: d.d_cexpt || '#000000', + d_ct: d.d_ct || '#80ff00', + d_cborder: d.d_cborder || '#444444', + d_cserv: d.d_cserv || '#fff200', + d_cserv2: d.d_cserv2 || '#dba766', + d_crecv: d.d_crecv || '#707676', + d_ctim_blue: d.d_ctim_blue || '#0070c0', + d_ctim_active: d.d_ctim_active || '#ffc000', + d_team_colors: d.d_team_colors == 'on' ? true : false, + d_scale: d.d_scale || '100', + fullscreen_ask: d.fullscreen_ask || 'auto', + show_announcements: d.show_announcements || 'all', + button_block_timeout: d.button_block_timeout || '100', + negative_timers: d.negative_timers == 'on' ? true : false, + shuttle_counter: d.shuttle_counter == 'on' ? true : false, + editmode_doubleclick: d.editmode_doubleclick == 'on' ? true : false, + click_mode: d.click_mode || 'auto', + style: d.style || 'complete', + network_timeout: d.network_timeout || '10000', + network_update_interval: d.network_update_interval || '10000', + language: d.language || 'auto', + } + + // + + return displaysetting; + } + function update_general_displaysettings(c) { - const general_displaysettings_div = uiu.qs('.general_displaysettings') - general_displaysettings_div.innerHTML = ''; - render_general_displaysettings(general_displaysettings_div); + //const general_displaysettings_div = uiu.qs('.general_displaysettings'); + const general_displaysettings_div = document.querySelector(".general_displaysettings"); + if(general_displaysettings_div) { + general_displaysettings_div.innerHTML = ''; + console.log(general_displaysettings_div); + render_general_displaysettings(general_displaysettings_div); + } } function render_displaysettings(general_displaysettings_div) { @@ -1644,6 +1990,7 @@ var ctournament = (function() { const attrs = { 'data-display-setting-id': selected_id, value: item.id, + label: item.description, } if ((selected_id === item.id)) { attrs.selected = 'selected'; @@ -1936,6 +2283,7 @@ if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var utils = require('../bup/bup/js/utils.js'); var save_file = require('../bup/bup/js/save_file.js'); var timezones = require('./timezones.js'); + var displaymode = require('../bup/js/displaymode'); var JSZip = null; // External library From 667b799f5a8e3ebd810cb166a02c146a4f24184f Mon Sep 17 00:00:00 2001 From: tengmel Date: Fri, 17 Jan 2025 17:45:22 +0100 Subject: [PATCH 178/224] Add Court announcement if court is given --- static/js/announcements.js | 12 +++++++++++- static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/static/js/announcements.js b/static/js/announcements.js index edfe539..324b3ea 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -19,6 +19,7 @@ function announcePreparationMatch(matchSetup) { if(!(window.localStorage.getItem('enable_announcements') === 'true')) { return; } + const field = createFieldPreparationAnnouncement(matchSetup); var preparation = createPreparationAnnouncement(); var matchNumber = createMatchNumberAnnouncement(matchSetup); var eventName = createEventAnnouncement(matchSetup); @@ -31,7 +32,7 @@ function announcePreparationMatch(matchSetup) { if (curt.preparation_meetingpoint_enabled) { lastPart = createMeetingPointAnnouncement(); } - announce([preparation, matchNumber, eventName, round, teams, umpire, serviceJudge, tabletOperator, lastPart]); + announce([preparation, field, matchNumber, eventName, round, teams, umpire, serviceJudge, tabletOperator, lastPart]); } function announceSecondCallTeamOne(matchSetup) { if(!(window.localStorage.getItem('enable_announcements') === 'true')) { @@ -247,6 +248,15 @@ function createFieldAnnouncement(matchSetup) { return ""; } +} +function createFieldPreparationAnnouncement(matchSetup) { + if (matchSetup.court_id) { + var court = matchSetup.court_id.split("_")[1]; + return ci18n('announcements:for_court') + court + "!"; + } else { + return ""; + } + } function createPreparationAnnouncement() { diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index e52418b..1465467 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -63,6 +63,7 @@ var ci18n_de = { 'announcements:preparation': 'In Vorbereitung:', 'announcements:meetingpoint': 'Treffen am Meetingpoint!', 'announcements:on_court': 'Auf Spielfeld ', + 'announcements:for_court': 'Für Spielfeld ', 'announcements:match_number': 'Spiel Nummer ', 'announcements:boys_singles': 'Jungeneinzel', 'announcements:boys_doubles': 'Jungendoppel', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 478ec74..50a0e17 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -63,6 +63,7 @@ var ci18n_en = { 'announcements:preparation': 'In preparation:', 'announcements:meetingpoint': 'Come to the meetingpoint!', 'announcements:on_court': 'On Court ', + 'announcements:for_court': 'For Court ', 'announcements:match_number': 'Match number ', 'announcements:boys_singles': 'Boys singles', 'announcements:boys_doubles': 'Boys double', From 9750796f52a0aad81566ab383cf34fb879b513b6 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 24 Jan 2025 00:55:00 +0100 Subject: [PATCH 179/224] The changes in #65 caused btp_conn.fetch() to be called without parameters. This is now intercepted and entered in the update_que.Fix: The changes in #65 --- bts/btp_conn.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bts/btp_conn.js b/bts/btp_conn.js index b18da5c..5dc0d62 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -116,6 +116,12 @@ class BTPConn { } fetch(connection) { + // In case of fetch() is called by btp_manager without connection + if (!connection) { + update_queue.instance().execute(this.fetch, this); + return; + } + const ir = btp_proto.get_info_request(connection.password); connection.send(ir, response => { btp_sync.sync_btp_data(connection.app, connection.tkey, response); From 2245edfae5bee1b428236e2ea4093aaca5167090 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 24 Jan 2025 01:33:05 +0100 Subject: [PATCH 180/224] =?UTF-8?q?Fix:=20#93=20has=20caused=20games=20whe?= =?UTF-8?q?re=20the=20side=20choice=20is=20performed=20on=20the=20tablet?= =?UTF-8?q?=20(match.setup.state=20=3D=3D=20=E2=80=9Cblocked=E2=80=9D)=20t?= =?UTF-8?q?o=20be=20hidden=20on=20the=20displays=20on=20the=20field=20and?= =?UTF-8?q?=20in=20the=20stream.=20Matches=20with=20the=20state=20?= =?UTF-8?q?=E2=80=9Cblocked=E2=80=9D=20are=20now=20also=20transmitted=20by?= =?UTF-8?q?=20bupws.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bts/bupws.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bts/bupws.js b/bts/bupws.js index 57922bb..756e6b4 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -638,7 +638,7 @@ function matches_handler(app, ws, tournament_key, court_id) { if (!court_id) { matches = matches.filter(m => m.setup.now_on_court); } - matches = matches.filter(m => m.setup.state == 'oncourt' || m.setup.state == 'finished'); + matches = matches.filter(m => m.setup.state == 'oncourt' || m.setup.state == 'finished' || m.setup.state == 'blocked'); db_courts.sort(utils.cmp_key('num')); const courts = db_courts.map(function (dc) { From cda058e005a3b260642ef6c6b1037826233a31a7 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 1 Nov 2024 02:02:31 +0100 Subject: [PATCH 181/224] Add the option to call the first player's state association in the match as a tablet operator.Do Tabletcalls like Thomas like it. --- bts/admin.js | 1 + bts/match_utils.js | 19 +++++++++++++++++-- static/js/change.js | 1 + static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/ctournament.js | 3 +++ 6 files changed, 24 insertions(+), 2 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index e4c022c..4e5309c 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -74,6 +74,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'tabletoperator_enabled', 'tabletoperator_break_seconds', 'announcement_speed','announcement_pause_time_ms', 'tabletoperator_set_break_after_tabletservice','tabletoperator_with_state_enabled', + 'tabletoperator_with_state_from_match_enabled', 'tabletoperator_winner_of_quaterfinals_enabled','tabletoperator_split_doubles', 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', 'annoncement_include_event', 'annoncement_include_round','annoncement_include_matchnumber', diff --git a/bts/match_utils.js b/bts/match_utils.js index f3de411..f0c6c41 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -149,7 +149,20 @@ async function add_tabletoperators(app, tournament, match, callback) { try { if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { if (!setup.tabletoperators || setup.tabletoperators == null) { - const value = await fetch_tabletoperator(admin, app, tournament.key, court_id); + + const fetch_result = await fetch_tabletoperator(admin, app, tournament.key, court_id); + let value = []; + if (tournament.tabletoperator_with_state_from_match_enabled && typeof(fetch_result) == "undefined") { + value.push({ + asian_name: false, + name: setup.teams[0].players[0].state, + firstname: "", + lastname: "", + btp_id: -1}); + } else { + value = fetch_result; + } + if (!setup.umpire || !setup.umpire.name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { setup.tabletoperators = value; } @@ -495,7 +508,9 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ team = cur_match.setup.teams[index]; } - // TODO: 'tabletoperator_with_state_enabled' + if (tournament.tabletoperator_with_state_from_match_enabled) { + return; + } if (team && typeof team.players !== 'undefined') { var teams = []; diff --git a/static/js/change.js b/static/js/change.js index 4f4c58f..279640c 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -62,6 +62,7 @@ function default_handler(rerender, special_funcs) { 'is_team', 'is_nation_competition', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_with_state_enabled', + 'tabletoperator_with_state_from_match_enabled', 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds', 'announcement_speed', 'announcement_pause_time_ms', diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 1465467..2eb2934 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -161,6 +161,7 @@ var ci18n_de = { 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Gewinner der Viertelfinals bedienen das Tablet', 'tournament:edit:tabletoperator_split_doubles': 'Doppelpaarungen für Tabletbdienung teilen', 'tournament:edit:tabletoperator_with_state_enabled': 'Landesverband anstelle Spieler aufrufen', + 'tournament:edit:tabletoperator_with_state_from_match_enabled': 'Landesverband des ersten Spielers im Match aufrufen', 'tournament:edit:tabletoperator_set_break_after_tabletservice': 'Pausenzeit für Tabletbedienung setzen', 'tournament:edit:tabletoperator_break_seconds': 'Pausenzeit nach Tabletbedienung (sec)', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 50a0e17..e5a845d 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -154,6 +154,7 @@ var ci18n_en = { 'tournament:edit:tabletoperator_break_seconds': 'Pausenzeit nach Tabletbedienung (sec)', 'tournament:edit:tabletoperator_split_doubles': 'Split Doubles for Tablet Service.', 'tournament:edit:tabletoperator_with_state_enabled': 'Call up national association instead of player', + 'tournament:edit:tabletoperator_with_state_from_match_enabled': 'Call up national association of first player in match', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', 'tournament:edit:annoncement_include_event': 'Announce event', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 0c22fd6..f007ce1 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -861,6 +861,7 @@ var ctournament = (function() { input.tabletoperator_use_manual_counting_boards_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_use_manual_counting_boards_enabled'); input.tabletoperator_split_doubles = create_checkbox(curt, tablet_fieldset, 'tabletoperator_split_doubles'); input.tabletoperator_with_state_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_state_enabled'); + input.tabletoperator_with_state_from_match_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_state_from_match_enabled'); input.tabletoperator_set_break_after_tabletservice = create_checkbox(curt, tablet_fieldset, 'tabletoperator_set_break_after_tabletservice'); if (!curt.tabletoperator_break_seconds) { @@ -1109,6 +1110,7 @@ var ctournament = (function() { tabletoperator_winner_of_quaterfinals_enabled: input.tabletoperator_winner_of_quaterfinals_enabled.checked, tabletoperator_split_doubles: input.tabletoperator_split_doubles.checked, tabletoperator_with_state_enabled: input.tabletoperator_with_state_enabled.checked, + tabletoperator_with_state_from_match_enabled: input.tabletoperator_with_state_from_match_enabled.checked, tabletoperator_set_break_after_tabletservice: input.tabletoperator_set_break_after_tabletservice.checked, tabletoperator_use_manual_counting_boards_enabled: input.tabletoperator_use_manual_counting_boards_enabled.checked, tabletoperator_break_seconds: input.tabletoperator_break_seconds.value, @@ -1173,6 +1175,7 @@ var ctournament = (function() { tabletoperator_winner_of_quaterfinals_enabled: input.tabletoperator_winner_of_quaterfinals_enabled.checked, tabletoperator_split_doubles: input.tabletoperator_split_doubles.checked, tabletoperator_with_state_enabled: input.tabletoperator_with_state_enabled.checked, + tabletoperator_with_state_from_match_enabled: input.tabletoperator_with_state_from_match_enabled.checked, tabletoperator_set_break_after_tabletservice: input.tabletoperator_set_break_after_tabletservice.checked, tabletoperator_use_manual_counting_boards_enabled: input.tabletoperator_use_manual_counting_boards_enabled.checked, tabletoperator_break_seconds: input.tabletoperator_break_seconds.value, From e443f800824ec9523d3d327063f2c30b383655bc Mon Sep 17 00:00:00 2001 From: tengmel Date: Fri, 24 Jan 2025 09:40:58 +0100 Subject: [PATCH 182/224] #65: Server.Core: Automatic call of a match which is in preparation when a match is finished. Handle Racecondition if btp sync is active and games will finshed more strict --- bts/btp_conn.js | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 5dc0d62..71e0e88 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -115,17 +115,28 @@ class BTPConn { }); } - fetch(connection) { - // In case of fetch() is called by btp_manager without connection - if (!connection) { - update_queue.instance().execute(this.fetch, this); - return; - } - - const ir = btp_proto.get_info_request(connection.password); - connection.send(ir, response => { - btp_sync.sync_btp_data(connection.app, connection.tkey, response); - connection.schedule_fetch(); + async fetch(connection) { + + //if (!connection) { + // update_queue.instance().execute(this.fetch, this); + // return; + //} + + return new Promise((resolve, reject) => { + try { + const ir = btp_proto.get_info_request(connection.password); + connection.send(ir, async (response) => { + try { + const value = await btp_sync.sync_btp_data(connection.app, connection.tkey, response); + connection.schedule_fetch(); + resolve(value); + } catch (innerError) { + reject(innerError); + } + }); + } catch (e) { + reject(e); + } }); } From f7702a19be5f41564fffd21bc127a8a796984554 Mon Sep 17 00:00:00 2001 From: tengmel Date: Fri, 24 Jan 2025 14:00:36 +0100 Subject: [PATCH 183/224] #65: Server.Core: Automatic call of a match which is in preparation when a match is finished. Handle Racecondition if btp sync is active and games will finshed more strict --- bts/btp_conn.js | 18 +++++++++--------- bts/btp_manager.js | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 71e0e88..91ecde7 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -110,25 +110,25 @@ class BTPConn { this.pushall(); if (this.enabled_autofetch) { - update_queue.instance().execute(this.fetch, this); + update_queue.instance().execute(this.fetch, this,true); } }); } - async fetch(connection) { - - //if (!connection) { - // update_queue.instance().execute(this.fetch, this); - // return; - //} + sync_data() { + update_queue.instance().execute(this.fetch, this, false); + } + async fetch(connection, reschedule_fetch ) { return new Promise((resolve, reject) => { try { const ir = btp_proto.get_info_request(connection.password); connection.send(ir, async (response) => { try { const value = await btp_sync.sync_btp_data(connection.app, connection.tkey, response); - connection.schedule_fetch(); + if (reschedule_fetch == true) { + connection.schedule_fetch(); + } resolve(value); } catch (innerError) { reject(innerError); @@ -149,7 +149,7 @@ class BTPConn { } this.autofetch_timeout = setTimeout(() => { - update_queue.instance().execute(this.fetch, this); + update_queue.instance().execute(this.fetch,this,true); }, AUTOFETCH_TIMEOUT); } diff --git a/bts/btp_manager.js b/bts/btp_manager.js index 1045ad6..c1e25d1 100644 --- a/bts/btp_manager.js +++ b/bts/btp_manager.js @@ -33,7 +33,7 @@ function fetch(tkey) { return; } - conn.fetch(); + conn.sync_data(); } function update_score(app, match) { From bd4826161b9804dbb5b98d268402262ca3fdb8b9 Mon Sep 17 00:00:00 2001 From: tengmel Date: Fri, 24 Jan 2025 18:17:22 +0100 Subject: [PATCH 184/224] #65: Server.Core: Automatic call of a match which is in preparation when a match is finished. Handle Racecondition if btp sync is active and games will finshed more strict --- bts/bupws.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index 756e6b4..922c77a 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -202,6 +202,7 @@ async function handle_score_update(app, ws, msg) { (match, cb) => { if (match) { if (finish_confirmed) { + btp_manager.update_score(app, match); update_queue.instance().execute(match_utils.reset_player_tabletoperator, app, tournament_key, match_id, update.end_ts) .then(() => { cb(null, match); @@ -241,12 +242,6 @@ async function handle_score_update(app, ws, msg) { } cb(null, match, changed_court); }, - (match, changed_court, cb) => { - if (match) { - btp_manager.update_score(app, match); - } - cb(null, match, changed_court); - }, (match, changed_court, cb) => { if (match && match.setup.highlight && match.setup.highlight == 6 && From 8fdce4186a48f2b0a9fa13e6be006fdce9195ad4 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 25 Jan 2025 10:47:32 +0100 Subject: [PATCH 185/224] Add the option to call the first player's state association in the match as a tablet operator.Do Tabletcalls like Thomas like it. --- bts/match_utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bts/match_utils.js b/bts/match_utils.js index f0c6c41..931dadc 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -509,7 +509,7 @@ function add_player_to_tabletoperator_list_by_match(app, tournament, tournament_ } if (tournament.tabletoperator_with_state_from_match_enabled) { - return; + return callback(null); } if (team && typeof team.players !== 'undefined') { From 25f313b2be6192b0d81615c12439960533c2b14d Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 25 Jan 2025 14:03:21 +0100 Subject: [PATCH 186/224] Make it possible to configure the timout bteween two sync actions for fetching the data from btp --- bts/admin.js | 2 +- bts/btp_conn.js | 26 ++++++++++++++------------ bts/btp_manager.js | 2 +- static/js/change.js | 1 + static/js/ci18n_de.js | 1 + static/js/ci18n_en.js | 1 + static/js/ctournament.js | 7 +++++++ 7 files changed, 26 insertions(+), 14 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 4e5309c..4f5e82d 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -65,7 +65,7 @@ function handle_tournament_edit_props(app, ws, msg) { const props = utils.pluck(msg.props, [ 'name','tguid', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', - 'btp_ip', 'btp_password', + 'btp_ip', 'btp_password','btp_autofetch_timeout_intervall', 'is_team', 'is_nation_competition', 'warmup', 'warmup_ready', 'warmup_start', 'upcoming_matches_animation_speed', 'upcoming_matches_max_count','upcoming_matches_animation_pause', diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 91ecde7..8e2dd6c 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -71,7 +71,7 @@ function send_request(ip, port, xml_req, timeZone, callback) { class BTPConn { - constructor(app, ip, password, tkey, enabled_autofetch, readonly, is_team, timeZone) { + constructor(app, ip, password, tkey, enabled_autofetch, readonly, is_team, timeZone, autofetch_timeout_intervall) { this.app = app; this.last_status = 'Activated'; this.ip = ip; @@ -83,6 +83,7 @@ class BTPConn { this.autofetch_timeout = null; this.readonly = readonly; this.is_team = is_team; + this.autofetch_timeout_intervall = autofetch_timeout_intervall ? autofetch_timeout_intervall : AUTOFETCH_TIMEOUT; this.connect(); } @@ -121,15 +122,19 @@ class BTPConn { async fetch(connection, reschedule_fetch ) { return new Promise((resolve, reject) => { - try { + try { const ir = btp_proto.get_info_request(connection.password); connection.send(ir, async (response) => { try { - const value = await btp_sync.sync_btp_data(connection.app, connection.tkey, response); - if (reschedule_fetch == true) { - connection.schedule_fetch(); + if (response && response != null) { + const value = await btp_sync.sync_btp_data(connection.app, connection.tkey, response); + if (reschedule_fetch == true) { + connection.schedule_fetch(); + } + resolve(value); + } else { + resolve(null); } - resolve(value); } catch (innerError) { reject(innerError); } @@ -147,10 +152,9 @@ class BTPConn { if (!this.enabled_autofetch) { return; } - this.autofetch_timeout = setTimeout(() => { update_queue.instance().execute(this.fetch,this,true); - }, AUTOFETCH_TIMEOUT); + }, this.autofetch_timeout_intervall); } terminate() { @@ -159,16 +163,14 @@ class BTPConn { } send(xml_req, success_cb) { - if (this.terminated) return; - + if (this.terminated) return success_cb(null); const port = this.is_team ? BLP_PORT : BTP_PORT; send_request(this.ip, port, xml_req, this.timeZone, (err, response) => { if (err) { this.report_status('error', 'Connection error: ' + err.message); this.schedule_reconnect(); - return; + return success_cb(null); } - success_cb(response); }); } diff --git a/bts/btp_manager.js b/bts/btp_manager.js index c1e25d1..7a6c4cc 100644 --- a/bts/btp_manager.js +++ b/bts/btp_manager.js @@ -22,7 +22,7 @@ function reconfigure(app, t) { app, t.btp_ip, t.btp_password, t.key, t.btp_autofetch_enabled, t.btp_readonly, - t.is_team, t.btp_timezone); + t.is_team, t.btp_timezone, t.btp_autofetch_timeout_intervall); conns_by_tkey.set(t.key, conn); } diff --git a/static/js/change.js b/static/js/change.js index 279640c..167a7b1 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -39,6 +39,7 @@ function default_handler(rerender, special_funcs) { curt.tguid = c.val.tguid; curt.is_nation_competition = c.val.is_nation_competition; curt.btp_timezone = c.val.btp_timezone; + curt.btp_autofetch_timeout_intervall = c.val.btp_autofetch_timeout_intervall; curt.warmup = c.val.warmup; curt.warmup_ready = c.val.warmup_ready; curt.warmup_start = c.val.warmup_start; diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 2eb2934..fa7c14b 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -152,6 +152,7 @@ var ci18n_de = { 'tournament:edit:warmup_start': 'Spielstart nach Sekunden:', 'tournament:edit:btp:enabled': 'BTP-Anbindung aktivieren', 'tournament:edit:btp:autofetch_enabled': 'Automatisch synchronisieren', + 'tournament:edit:btp_autofetch_timeout_intervall': 'Aktualisierungsintervall (ms)', 'tournament:edit:btp:readonly': 'Nur lesen', 'tournament:edit:btp:ip': 'IP:', 'tournament:edit:btp:password': 'BTP-Passwort:', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index e5a845d..460243a 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -139,6 +139,7 @@ var ci18n_en = { 'tournament:edit:warmup_start': 'Game starts after seconds:', 'tournament:edit:btp:enabled': 'Enable BTP synchronization', 'tournament:edit:btp:autofetch_enabled': 'Automatic synchronization', + 'tournament:edit:btp_autofetch_timeout_intervall': 'Synchronization intervall (ms)', 'tournament:edit:btp:readonly': 'Read only', 'tournament:edit:btp:ip': 'IP address:', 'tournament:edit:btp:password': 'BTP password:', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index f007ce1..54d2943 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -711,6 +711,11 @@ var ctournament = (function() { if (curt.btp_readonly) { bro_attrs.checked = 'checked'; } + if (!curt['btp_autofetch_timeout_intervall']) { + curt['btp_autofetch_timeout_intervall'] = 30000; + } + input.btp_autofetch_timeout_intervall = create_input(curt, "number", btp_connection_div, 'btp_autofetch_timeout_intervall') + input.btp_readonly = uiu.el(btp_readonly_label, 'input', bro_attrs); uiu.el(btp_readonly_label, 'span', {}, ci18n('tournament:edit:btp:readonly')); @@ -1097,6 +1102,7 @@ var ctournament = (function() { btp_ip: input.btp_ip.value, btp_password: input.btp_password.value, btp_timezone: input.btp_timezone.value, + btp_autofetch_timeout_intervall: input.btp_autofetch_timeout_intervall.value, dm_style: input.dm_style.value, displaysettings_general: input.displaysettings_general.value, warmup: input.warmup.value, @@ -1155,6 +1161,7 @@ var ctournament = (function() { is_nation_competition: input.is_nation_competition.checked, btp_enabled: input.btp_enabled.checked, btp_autofetch_enabled: input.btp_autofetch_enabled.checked, + btp_autofetch_timeout_intervall: input.btp_autofetch_timeout_intervall.value, btp_readonly: input.btp_readonly.checked, btp_ip: input.btp_ip.value, btp_password: input.btp_password.value, From a2ba591f94eb03a9ff3232cd15c7975638a4cfd4 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sat, 25 Jan 2025 19:55:04 +0100 Subject: [PATCH 187/224] #65: Server.Core: Automatic call of a match which is in preparation when a match is finished. Add potentially missing callbacks --- bts/btp_sync.js | 2 +- bts/match_utils.js | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 4a69395..2236658 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -931,7 +931,7 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { app.db.umpires.update({ tournament_key, btp_id }, { $set: { btp_id, firstname, surname, name, country } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { if (err) { console.error(err); - return; + return cb(err); } const admin = require('./admin'); admin.notify_change(app, tournament_key, 'umpire_updated', changed_umpire); diff --git a/bts/match_utils.js b/bts/match_utils.js index 931dadc..a35e5c0 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -40,7 +40,7 @@ async function uncall_match(app, tournament, match, old_court, callback) { async function call_match(app, tournament, match, old_court, callback) { if (!match.setup.court_id || !match._id) { - return; // TODO in async we would assert both to be true + return callback("Match cannot be called court_id or _id not given."); } if (match_completly_initialized(match.setup) == false) { return callback("Match cannot be called one or more Teams are not set."); @@ -67,10 +67,10 @@ async function call_match(app, tournament, match, old_court, callback) { async function switch_court(app, tournament, match, old_court, callback) { if (!match.setup.court_id || !match._id) { - return; // TODO in async we would assert both to be true + return callback("Match cannot be switched to another court: court_id or _id not given."); } if (match_completly_initialized(match.setup) == false) { - return callback("Match cannot be called one or more Teams are not set."); + return callback("Match cannot be switched to another court: one or more Teams are not set."); } async.waterfall([ (wcb) => add_tabletoperators(app, tournament, match, wcb), @@ -141,7 +141,7 @@ async function add_tabletoperators(app, tournament, match, callback) { const match_id = match._id; if (!court_id || !match_id) { - return; // TODO in async we would assert both to be true + return callback(null); } const setup = match.setup; @@ -185,7 +185,7 @@ async function set_umpires_on_court(app, tournament, match, callback) { const setup = match.setup; const court_id = setup.court_id; if (!court_id) { - return; // TODO in async we would assert both to be true + return callback(null); } if (setup.umpire) { @@ -864,10 +864,11 @@ function update_umpire(app, tkey, umpire, status, last_time_on_court_ts, court_i app.db.umpires.update({ tournament_key: tkey, name: umpire.name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { if (err) { console.error(err); - return; + return callback(err); } const admin = require('./admin'); admin.notify_change(app, tkey, 'umpire_updated', changed_umpire); + return callback(null); }); } From 01c08aca90984605e8899c61adca89879e29d9dc Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 26 Jan 2025 17:46:00 +0100 Subject: [PATCH 188/224] #65: Server.Core: Automatic call of a match which is in preparation when a match is finished. Add potentially missing callbacks --- bts/match_utils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bts/match_utils.js b/bts/match_utils.js index a35e5c0..ded4faf 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -860,15 +860,15 @@ function set_umpire_to_standby(app, tournament_key, setup) { -function update_umpire(app, tkey, umpire, status, last_time_on_court_ts, court_id, callback) { +function update_umpire(app, tkey, umpire, status, last_time_on_court_ts, court_id) { app.db.umpires.update({ tournament_key: tkey, name: umpire.name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { if (err) { console.error(err); - return callback(err); + return; } const admin = require('./admin'); admin.notify_change(app, tkey, 'umpire_updated', changed_umpire); - return callback(null); + return; }); } From 0d2098e568400d6d7f26ad1185599311d9c1f8da Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 26 Jan 2025 17:55:45 +0100 Subject: [PATCH 189/224] #65: Server.Core: Automatic call of a match which is in preparation when a match is finished. Add potentially missing callbacks --- bts/btp_conn.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 8e2dd6c..417e2fc 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -94,15 +94,13 @@ class BTPConn { this.report_status('connecting','Try to establish connection to BTP.'); this.send(btp_proto.login_request(this.password), response => { - if (!response.Action || !response.Action[0] || !response.Action[0].ID[0] || (response.Action[0].ID[0] !== 'REPLY')) { + if (!response || response == null || !response.Action || !response.Action[0] || !response.Action[0].ID[0] || (response.Action[0].ID[0] !== 'REPLY')) { this.report_status('error','Invalid reply to login request'); - this.schedule_reconnect(); return; } if (response.Action[0].Result[0] !== 1) { this.report_status('error', 'Invalid password'); - this.schedule_reconnect(); return; } From 407a048e4d0f4f02da0fce272e9d6ba90db90ae3 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 28 Jan 2025 00:14:09 +0100 Subject: [PATCH 190/224] Fix: Fix typo in Settings dialog. --- static/js/ctournament.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 54d2943..25a2592 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -1188,7 +1188,7 @@ var ctournament = (function() { tabletoperator_break_seconds: input.tabletoperator_break_seconds.value, annoncement_include_event: input.annoncement_include_event.checked, annoncement_include_round: input.annoncement_include_round.checked, - annoncement_include_matchnumber: input.annoncement_include_matchnumber.checkt, + annoncement_include_matchnumber: input.annoncement_include_matchnumber.checked, announcement_speed: input.announcement_speed.value, announcement_pause_time_ms: input.announcement_pause_time_ms.value, preparation_meetingpoint_enabled: input.preparation_meetingpoint_enabled.checked, From 5f8cc7254ff07a2c9a25c9b493bbaa9840eec664 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 28 Jan 2025 01:19:20 +0100 Subject: [PATCH 191/224] Fix: Try to make the resolving of the hostname more staeble. Got Error in Case of ::1 (ip_v6 localhost) before. --- bts/bupws.js | 104 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index 922c77a..dc83793 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -3,9 +3,10 @@ const async = require('async'); const serror = require('./serror'); const utils = require('./utils'); const admin = require('./admin'); -const dns = require('dns'); -const cp = require('child_process'); -const os = require('os'); +const cp = require("child_process"); +const os = require("os"); +const dns = require("dns"); +const net = require("net"); const btp_manager = require('./btp_manager'); const btp_conn = require('./btp_conn'); @@ -382,47 +383,78 @@ async function initialize_client(ws, app, tournament_key, court_id, displaysetti } function getComputerName() { - switch (process.platform) { - case "win32": - return process.env.COMPUTERNAME; - case "darwin": - return cp.execSync("scutil --get ComputerName").toString().trim(); - case "linux": - const prettyname = cp.execSync("hostnamectl --pretty").toString().trim(); - return prettyname === "" ? os.hostname() : prettyname; - default: + try { + switch (process.platform) { + case "win32": + return process.env.COMPUTERNAME || os.hostname(); + case "darwin": + return cp.execSync("scutil --get ComputerName").toString().trim(); + case "linux": + const prettyname = cp.execSync("hostnamectl --pretty").toString().trim(); + return prettyname || os.hostname(); + default: + return os.hostname(); + } + } catch (err) { + console.error("Error getting computer name:", err); return os.hostname(); } - } +} async function determine_client_hostname(ws) { - return new Promise((resolve, reject)=> { - if(!ws.hostname) { - const remote_adress_seqments_v6 = ws._socket.remoteAddress.split(':'); - const ip_v4 = remote_adress_seqments_v6[remote_adress_seqments_v6.length - 1]; - - // catch ip for localhost - if(ip_v4 == '127.0.0.1') { - ws.hostname = getComputerName(); - resolve(ws.hostname); - return; - } - dns.reverse(ip_v4, (err, hostnames) => { - if (err) { - resolve("N/N") - } - if (hostnames && hostnames.length >= 1) { - ws.hostname = hostnames[0].split('.')[0]; - } - resolve(ws.hostname); + if (ws.hostname) { + return ws.hostname; + } + + const remoteAddress = ws._socket.remoteAddress; + + // Verifizieren, ob die Adresse gültig ist + if (net.isIPv4(remoteAddress)) { + // Lokale IP-Adressen abfangen + if (remoteAddress === "127.0.0.1") { + console.log("Localhost"); + ws.hostname = getComputerName(); + return ws.hostname; + } + + // Rückwärtssuche für IPv4 + try { + const hostnames = await new Promise((resolve, reject) => { + dns.reverse(remoteAddress, (err, hostnames) => { + if (err) reject(err); + else resolve(hostnames); + }); }); - } - else { - resolve(ws.hostname); + + if (hostnames && hostnames.length > 0) { + ws.hostname = hostnames[0].split(".")[0]; + } else { + ws.hostname = "N/N"; + } + } catch (err) { + console.error("DNS reverse lookup failed:", err); + ws.hostname = "N/N"; } - }); + + return ws.hostname; + } else if (net.isIPv6(remoteAddress)) { + // IPv6-Adresse prüfen (z. B. `::1` für localhost) + if (remoteAddress === "::1") { + ws.hostname = getComputerName(); + return ws.hostname; + } + + // Bei IPv6 keine spezielle Verarbeitung (z. B. Reverse-Lookup) + ws.hostname = remoteAddress; + return ws.hostname; + } else { + console.error("Invalid IP address:", remoteAddress); + ws.hostname = "N/N"; + return ws.hostname; + } } + function determine_client_id(ws) { if (!ws.client_id) { ws.client_id = determine_client_id_from_ip (ws._socket.remoteAddress); From 122cb6c3a923a7b86802c04578b041e29a06497d Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 31 Jan 2025 18:43:15 +0100 Subject: [PATCH 192/224] If matches use followed by: Use the Match number to order the matches. --- static/js/cmatch.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 9052c5c..4d3f371 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -1010,8 +1010,10 @@ function cmp_match_order(m1, m2) { const cmp1 = cbts_utils.cmp(m1.setup.scheduled_date, m2.setup.scheduled_date); if (cmp1 != 0) return cmp1; - - if (time_str1 === '00:00' && time_str2 !== '00:00') { + + if (time_str1 === '00:00' && time_str2 === '00:00') { + return cbts_utils.cmp(m1.setup.match_num, m2.setup.match_num); + } else if (time_str1 === '00:00' && time_str2 !== '00:00') { return 1; } else if (time_str2 === '00:00' && time_str1 !== '00:00') { return -1; From 51479d5142f69ddd30de208c4495906058511866 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 6 Feb 2025 21:39:41 +0100 Subject: [PATCH 193/224] Add scripts to update or reset bts (service) and bup --- blank_tournament/courts | 9 ++++++ .../display_court_displaysettings | 28 +++++++++++++++++++ blank_tournament/displaysettings | 3 ++ blank_tournament/event | 0 blank_tournament/normalizations | 4 +++ blank_tournament/tournaments | 2 ++ reset-bts.sh | 7 +++++ update-bts.sh | 6 ++++ 8 files changed, 59 insertions(+) create mode 100644 blank_tournament/courts create mode 100644 blank_tournament/display_court_displaysettings create mode 100644 blank_tournament/displaysettings create mode 100644 blank_tournament/event create mode 100644 blank_tournament/normalizations create mode 100644 blank_tournament/tournaments create mode 100755 reset-bts.sh create mode 100755 update-bts.sh diff --git a/blank_tournament/courts b/blank_tournament/courts new file mode 100644 index 0000000..84f822b --- /dev/null +++ b/blank_tournament/courts @@ -0,0 +1,9 @@ +{"_id":"default_1","tournament_key":"default","btp_id":9,"num":1,"name":"1","match_id":"btp_default_U17 MD_545"} +{"_id":"default_2","tournament_key":"default","btp_id":10,"num":2,"name":"2","match_id":"btp_default_U19 JE_654"} +{"_id":"default_3","tournament_key":"default","btp_id":11,"num":3,"name":"3","match_id":"btp_default_U19 JE_649"} +{"_id":"default_4","tournament_key":"default","btp_id":12,"num":4,"name":"4","match_id":"btp_default_U19 ME_715"} +{"_id":"default_5","tournament_key":"default","btp_id":13,"num":5,"name":"5","match_id":"btp_default_U17 JD_467"} +{"_id":"default_6","tournament_key":"default","btp_id":14,"num":6,"name":"6","match_id":"btp_default_U17 JD_469"} +{"_id":"default_7","tournament_key":"default","btp_id":15,"num":7,"name":"7","match_id":"btp_default_U17 JE_13"} +{"_id":"default_8","tournament_key":"default","btp_id":16,"num":8,"name":"8","match_id":"btp_default_U19 JE_653"} +{"$$indexCreated":{"fieldName":"tournament_key","unique":false,"sparse":false}} diff --git a/blank_tournament/display_court_displaysettings b/blank_tournament/display_court_displaysettings new file mode 100644 index 0000000..54e339d --- /dev/null +++ b/blank_tournament/display_court_displaysettings @@ -0,0 +1,28 @@ +{"client_id":"253","court_id":"default_2","displaysetting_id":"default","_id":"M3VrcixFrRE51rMY"} +{"client_id":"130","court_id":"default_5","displaysetting_id":"default","_id":"M3VrcixFrRE52rMY"} +{"client_id":"101","court_id":"default_3","displaysetting_id":"nur_spieler","_id":"bSSxTeYudRTkf1Ni"} +{"client_id":"102","court_id":"default_2","displaysetting_id":"default","_id":"bSSxTeYudRTkf2Ni"} +{"client_id":"103","court_id":"default_3","displaysetting_id":"nur_spieler","_id":"bSSxTeYudRTkf3Ni"} +{"client_id":"104","court_id":"default_3","displaysetting_id":"default","_id":"bSSxTeYudRTkf4Ni"} +{"client_id":"105","court_id":"default_4","displaysetting_id":"nur_spieler","_id":"bSSxTeYudRTkf5Ni"} +{"client_id":"106","court_id":"default_5","displaysetting_id":"default","_id":"bSSxTeYudRTkf6Ni"} +{"client_id":"107","court_id":"default_5","displaysetting_id":"default","_id":"bSSxTeYudRTkf7Ni"} +{"client_id":"108","court_id":"default_6","displaysetting_id":"default","_id":"bSSxTeYudRTkf8Ni"} +{"client_id":"109","court_id":"default_7","displaysetting_id":"default","_id":"bSSxTeYudRTkf9Ni"} +{"client_id":"110","court_id":"default_8","displaysetting_id":"default","_id":"bSSxTeYudRTkg1Ni"} +{"client_id":"111","court_id":"default_3","displaysetting_id":"default","_id":"bSSxTeYudRTkg2Ni"} +{"client_id":"112","court_id":"default_4","displaysetting_id":"default","_id":"bSSxTeYudRTkg3Ni"} +{"client_id":"113","court_id":"default_5","displaysetting_id":"default","_id":"bSSxTeYudRTkg4Ni"} +{"client_id":"114","court_id":"default_6","displaysetting_id":"default","_id":"bSSxTeYudRTkg5Ni"} +{"client_id":"120","court_id":"default_7","displaysetting_id":"default","_id":"bSSxTeYudRTkg6Ni"} +{"client_id":"121","court_id":"default_8","displaysetting_id":"default","_id":"bSSxTeYudRTkg7Ni"} +{"client_id":"130","court_id":"default_8","displaysetting_id":"default","_id":"bSSxTeYudRTkg8Ni"} +{"client_id":"131","court_id":"default_1","displaysetting_id":"default","_id":"bSSxTeYudRTkg9Ni"} +{"client_id":"132","court_id":"default_2","displaysetting_id":"default","_id":"bSSxTeYudRTkh1Ni"} +{"client_id":"133","court_id":"default_3","displaysetting_id":"default","_id":"bSSxTeYudRTkh2Ni"} +{"client_id":"134","court_id":"default_4","displaysetting_id":"default","_id":"bSSxTeYudRTkh3Ni"} +{"client_id":"135","court_id":"default_5","displaysetting_id":"default","_id":"bSSxTeYudRTkh4Ni"} +{"client_id":"136","court_id":"default_6","displaysetting_id":"default","_id":"bSSxTeYudRTkh5Ni"} +{"client_id":"137","court_id":"default_7","displaysetting_id":"default","_id":"bSSxTeYudRTkh6Ni"} +{"client_id":"138","court_id":"default_8","displaysetting_id":"default","_id":"bSSxTeYudRTkh7Ni"} +{"client_id":"139","court_id":"default_9","displaysetting_id":"default","_id":"bSSxTeYudRTkh8Ni"} \ No newline at end of file diff --git a/blank_tournament/displaysettings b/blank_tournament/displaysettings new file mode 100644 index 0000000..e7782eb --- /dev/null +++ b/blank_tournament/displaysettings @@ -0,0 +1,3 @@ +{"id":"nur_spieler","displaymode_style":"onlyplayers","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_2","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_2","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"settings_style":"clean","_id":"9t2vgoCBMN0Rs5kA"} +{"id":"nur_spielstand","displaymode_style":"onlyscore","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_2","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_2","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"settings_style":"clean","_id":"eddY0hYOZPvnQxMX"} +{"id":"default","displaymode_style":"tournamentcourt","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_7","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_7","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"settings_style":"clean","_id":"fKYy03Qs6635VWRO"} diff --git a/blank_tournament/event b/blank_tournament/event new file mode 100644 index 0000000..e69de29 diff --git a/blank_tournament/normalizations b/blank_tournament/normalizations new file mode 100644 index 0000000..1f1e570 --- /dev/null +++ b/blank_tournament/normalizations @@ -0,0 +1,4 @@ +{"origin":"oluek","replace":"oluaek","language":"de-DE","_id":"jgK0c7WA6ZJyhgr6"} +{"origin":"Roschild","replace":"Rohschild","language":"de-DE","_id":"jgK0c7WA6ZJyhgr7"} +{"origin":" Ly","replace":" Lee","language":"de-DE","_id":"jgK0c7WA6ZJyhgr8"} +{"origin":"sy","replace":"si","language":"de-DE","_id":"jgK0c7WA6ZJyhgr9"} diff --git a/blank_tournament/tournaments b/blank_tournament/tournaments new file mode 100644 index 0000000..79dd09e --- /dev/null +++ b/blank_tournament/tournaments @@ -0,0 +1,2 @@ +{"key":"default","_id":"1sy6ogH6y1HRtyqL","name":"Change Me!","btp_enabled":true,"btp_autofetch_enabled":true,"btp_readonly":false,"btp_ip":"192.168.16.253","btp_password":"test#789","is_team":false,"is_nation_competition":true,"only_now_on_court":true,"warmup":"call-down","ticker_enabled":false,"ticker_url":"","ticker_password":"","language":"de","dm_style":"international","btp_settings":{"check_in_per_match":false,"pause_duration_ms":900000},"warmup_ready":"150","warmup_start":"180","tabletoperator_with_umpire_enabled":false,"tabletoperator_enabled":true,"tabletoperator_winner_of_quaterfinals_enabled":true,"tabletoperator_use_manual_counting_boards_enabled":false} +{"$$indexCreated":{"fieldName":"key","unique":true,"sparse":false}} diff --git a/reset-bts.sh b/reset-bts.sh new file mode 100755 index 0000000..c5a42b1 --- /dev/null +++ b/reset-bts.sh @@ -0,0 +1,7 @@ +#!/bin/bash +cd "$(dirname "$(realpath "$0")")" + +sudo systemctl stop bts +rm -rf ./data/* +cp -rf ./blank_tournament/* ./data/ +sudo systemctl start bts diff --git a/update-bts.sh b/update-bts.sh new file mode 100755 index 0000000..c45f387 --- /dev/null +++ b/update-bts.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cd "$(dirname "$(realpath "$0")")" +git pull +cd ./static/bup/dev +git pull +sudo systemctl restart bts From 25ffa448af60441e652ecb5e89e59e4aeec27fbc Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 12 Apr 2025 17:39:17 +0200 Subject: [PATCH 194/224] Improve the Managment of displays in the admin view --- bts/admin.js | 46 +++++++++++++------ bts/bupws.js | 76 +++++++++++++++++++++++-------- bts/utils.js | 3 ++ static/css/admin.css | 39 ++++++++++++++++ static/js/change.js | 25 ++++++++++- static/js/ctournament.js | 96 ++++++++++++++++++++++++++++++++-------- 6 files changed, 232 insertions(+), 53 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 4f5e82d..276943d 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -349,7 +349,7 @@ function handle_advertisement_add(app, ws, msg) { } notify_change(app, msg.tournament_key, 'advertisement_add', { advertisement: inserted_advertisement }); const bupws = require('./bupws'); - bupws.send_advertisement_add(msg.tournament_key,inserted_advertisement); + bupws.send_advertisement_add(app, msg.tournament_key,inserted_advertisement); return; }); } @@ -367,7 +367,7 @@ function handle_advertisement_remove(app, ws, msg) { app.db.advertisements.remove(query, {}, (err) => { notify_change(app, msg.tournament_key, 'advertisement_removed', { advertisement_id: msg.advertisement_id }); const bupws = require('./bupws'); - bupws.send_advertisement_remove(msg.tournament_key,msg.advertisement_id); + bupws.send_advertisement_remove(app, msg.tournament_key,msg.advertisement_id); return; }); } @@ -787,12 +787,34 @@ function handle_second_call_team_one(app, ws, msg) { ws.respond(msg); } -function handle_reset_display(app, ws, msg) { +function handle_display_delete(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'display_client_id'])) { + return; + } + const tournament_key = msg.tournament_key; - const client_id = msg.display_setting_id; + const client_id = msg.display_client_id; + + const query_remove = {client_id: client_id}; + app.db.display_court_displaysettings.remove(query_remove, {}, (err) => { + notify_change(app, tournament_key, 'delete_display', client_id); + }); + + ws.respond(msg); +} + +function handle_display_reset(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'display_client_id'])) { + return; + } + const tournament_key = msg.tournament_key; + const client_id = msg.display_client_id; const bupws = require('./bupws'); + + console.log("Try to reset: " + client_id); + bupws.restart_panel(app, tournament_key, client_id); - ws.respond("Angekommen: " + client_id); + ws.respond(msg); } function handle_edit_display_setting(app, ws, msg) { @@ -826,7 +848,7 @@ async function async_handle_delete_display_setting(app, ws, msg) { notify_change(app, tournament_key, 'delete_display_setting', setting_id); }); - ws.respond("Angekommen: " + setting_id); + ws.respond(msg); } @@ -836,7 +858,7 @@ function handle_relocate_display(app, ws, msg) { const new_court_id = msg.new_court_id; const bupws = require('./bupws'); bupws.restart_panel(app, tournament_key, client_id, new_court_id); - ws.respond("Angekommen: " + client_id); + ws.respond(msg); } function handle_change_display_mode(app, ws, msg) { @@ -846,15 +868,10 @@ function handle_change_display_mode(app, ws, msg) { const bupws = require('./bupws'); bupws.change_display_mode(app, tournament_key, client_id, new_displaysettings_id); notify_change(app, tournament_key, 'update_general_displaysettings', {}); - ws.respond("Angekommen: " + client_id); + ws.respond(msg); } - - - - - function handle_second_call_team_two(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { return; @@ -1045,7 +1062,8 @@ module.exports = { handle_tournament_get, handle_tournament_list, handle_tournament_edit_props, - handle_reset_display, + handle_display_delete, + handle_display_reset, handle_relocate_display, handle_change_display_mode, notify_change, diff --git a/bts/bupws.js b/bts/bupws.js index dc83793..81d662d 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -50,29 +50,30 @@ function generate_default_displaysettings_id(tournament) { return (tournament && tournament.displaysettings_general) ? tournament.displaysettings_general : default_displaysettings_key; } -function notify_change(tournament_key, court_id, ctype, val) { +function notify_change(app, tournament_key, court_id, ctype, val) { for (const panel_ws of all_panels) { - notify_change_ws(panel_ws, tournament_key, court_id, ctype, val); + notify_change_ws(app, panel_ws, tournament_key, court_id, ctype, val); } } -function notify_change_broadcast(tournament_key, ctype, val) { +function notify_change_broadcast(app, tournament_key, ctype, val) { for (const panel_ws of all_panels) { - notify_change_send(panel_ws, tournament_key, ctype, val); + notify_change_send(app, panel_ws, tournament_key, ctype, val); } } -function notify_change_ws(ws, tournament_key, court_id, ctype, val) { +function notify_change_ws(app, ws, tournament_key, court_id, ctype, val) { if (ws == null) { - notify_change(tournament_key, court_id, ctype, val); + notify_change(app, tournament_key, court_id, ctype, val); } else { if (ws.court_id === court_id) { - notify_change_send(ws,tournament_key, ctype, val); + notify_change_send(app, ws, tournament_key, ctype, val); } } } -function notify_change_send(ws,tournament_key, ctype, val) { +function notify_change_send(app, ws, tournament_key, ctype, val) { + admin.notify_change(app, tournament_key, 'display_wait_for_done', {'ctype': ctype, 'val' : val, 'client_id': ws.client_id}); ws.sendmsg({ type: 'change', tournament_key, @@ -83,7 +84,7 @@ function notify_change_send(ws,tournament_key, ctype, val) { function send_courts(app, ws, tournament_key) { stournament.get_courts(app.db, tournament_key, function (err, courts) { - notify_change_ws(ws,tournament_key, ws.court_id, "courts-update", courts); + notify_change_ws(app, ws,tournament_key, ws.court_id, "courts-update", courts); }); } function send_error(ws, tournament_key, msg) { @@ -356,15 +357,15 @@ async function handle_init(app, ws, msg) { } async function send_finshed_confirmed(app, tournament_key, court_id) { - notify_change(tournament_key, court_id, 'confirm-match-finished', {}); + notify_change(app, tournament_key, court_id, 'confirm-match-finished', {}); } -async function send_advertisement_add(tournament_key, advertisement) { - notify_change_broadcast(tournament_key, 'advertisement_add', advertisement); +async function send_advertisement_add(app, tournament_key, advertisement) { + notify_change_broadcast(app, tournament_key, 'advertisement_add', advertisement); } -async function send_advertisement_remove(tournament_key, advertisement_id) { - notify_change_broadcast(tournament_key, 'advertisement_remove', { advertisement_id: advertisement_id }); +async function send_advertisement_remove(app, tournament_key, advertisement_id) { + notify_change_broadcast(app, tournament_key, 'advertisement_remove', { advertisement_id: advertisement_id }); } async function initialize_client(ws, app, tournament_key, court_id, displaysetting) { @@ -375,8 +376,8 @@ async function initialize_client(ws, app, tournament_key, court_id, displaysetti if (display_setting != null) { ws.court_id = display_setting.court_id; court_id = display_setting.court_id; - notify_change_ws(ws, tournament_key, court_id, "settings-update", display_setting); - notify_change_ws(ws, tournament_key, court_id, "settings-update", display_setting); + notify_change_ws(app, ws, tournament_key, court_id, "settings-update", display_setting); + notify_change_ws(app, ws, tournament_key, court_id, "settings-update", display_setting); } } matches_handler(app, ws, tournament_key, ws.court_id); @@ -401,18 +402,48 @@ function getComputerName() { } } +function extractIPv4FromMappedIPv6(ip) { + const match = ip.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/); + return match ? match[1] : null; +} + async function determine_client_hostname(ws) { if (ws.hostname) { return ws.hostname; } const remoteAddress = ws._socket.remoteAddress; + const mappedIPv4 = extractIPv4FromMappedIPv6(remoteAddress); + + if (mappedIPv4) { + // Hier behandeln wir es wie ein echtes IPv4 + if (mappedIPv4 === "127.0.0.1") { + ws.hostname = getComputerName(); + return ws.hostname; + } + + try { + const hostnames = await new Promise((resolve, reject) => { + dns.reverse(mappedIPv4, (err, hostnames) => { + if (err) reject(err); + else resolve(hostnames); + }); + }); + + ws.hostname = hostnames?.[0]?.split(".")[0] || "N/N"; + } catch (err) { + console.error("DNS reverse lookup failed:", err); + ws.hostname = "N/N"; + } + + return ws.hostname; + } + // Verifizieren, ob die Adresse gültig ist if (net.isIPv4(remoteAddress)) { // Lokale IP-Adressen abfangen if (remoteAddress === "127.0.0.1") { - console.log("Localhost"); ws.hostname = getComputerName(); return ws.hostname; } @@ -562,7 +593,9 @@ function get_display_setting(app, tkey, client_id, court_id, displaysetting) { if (err) { return resolve(returnvalue); } - returnvalue.advertisements = advertisements; + if(returnvalue) { + returnvalue.advertisements = advertisements; + } resolve(returnvalue); }); @@ -601,6 +634,10 @@ function get_display_setting(app, tkey, client_id, court_id, displaysetting) { }); } +function handle_command_done(app, ws, msg) { + admin.notify_change(app, msg.tournament_key, 'display_is_done', {'ctype': msg.wait_for_command.ctype, 'val' : msg.wait_for_command.val, 'client_id': ws.client_id}); +} + function handle_score_change(app, tournament_key, court_id) { matches_handler(app, null, tournament_key, court_id); if (all_matches_delivery()) { @@ -690,7 +727,7 @@ function matches_handler(app, ws, tournament_key, court_id) { status: 'ok', event, }; - notify_change_ws(ws, tournament_key, court_id, "score-update", reply) + notify_change_ws(app, ws, tournament_key, court_id, "score-update", reply) } }); } @@ -883,6 +920,7 @@ module.exports = { on_connect, notify_change, handle_init, + handle_command_done, handle_score_change, handle_persist_display_settings, handle_reset_display_settings, diff --git a/bts/utils.js b/bts/utils.js index 7c86f14..2631f21 100644 --- a/bts/utils.js +++ b/bts/utils.js @@ -43,6 +43,9 @@ function cmp_key(key) { return function(o1, o2) { const v1 = o1[key]; const v2 = o2[key]; + if(!isNaN(Number(v1) && !isNaN(v2))) { + return cmp(Number(v1), Number(v2)); + } return cmp(v1, v2); }; } diff --git a/static/css/admin.css b/static/css/admin.css index 1c408af..9786e02 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -298,3 +298,42 @@ h2.edit { color: black; text-align: left; } + +.settings table > tbody > tr { + background-color: #bbb; +} + +.settings table > tbody > tr:nth-of-type(even) { + background-color: #ccc; +} + +.settings .online { + background-color: #97e6b4; +} + +.settings table > tbody > .online:nth-of-type(even) { + background-color: #adffcb; +} + +.settings .wait_for_done { + background-color: #dfe197; +} + +.settings table > tbody > .wait_for_done:nth-of-type(even) { + background-color: #fcffad; +} + + +.settings .offline { + background-color: #e69797 +} + +.settings table > tbody > .offline:nth-of-type(even) { + background-color: #ffadad; +} + +.settings table { + border:none; + border-spacing: 0; + width: 100%; +} diff --git a/static/js/change.js b/static/js/change.js index 167a7b1..474721e 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -88,6 +88,9 @@ function default_handler(rerender, special_funcs) { el.value = curt.ticker_password; }); break;} + case 'court_current_match': + //nothing to do here + break; case 'tabletoperator_add': //nothing to do here break; @@ -101,7 +104,6 @@ function default_handler(rerender, special_funcs) { //nothing todo here break; case 'match_edit': - console.log("match_edit in change.js"); ctournament.update_match(c); break; case 'match_add': @@ -237,7 +239,7 @@ function default_handler(rerender, special_funcs) { break; case 'display_status_changed': const display_setting = c.val.display_court_displaysetting; - const d = utils.find(curt.displays, m => m.client_id === display_setting.client_id); + var d = utils.find(curt.displays, m => m.client_id === display_setting.client_id); var laststatus = false; if (!d) { curt.displays[curt.displays.length] = display_setting; @@ -255,6 +257,25 @@ function default_handler(rerender, special_funcs) { } ctournament.update_display(d); break; + case 'delete_display': + const client_id = c.val.client_id; + const display = utils.find(curt.displays, m => m.client_id === client_id); + utils.remove(curt.displays, display); + ctournament.delete_display(c); + break; + case 'display_wait_for_done': + var d = utils.find(curt.displays, m => m.client_id === c.val.client_id); + d.wait_for_done = true; + d.wait_for_ctype = c.val.ctype; + ctournament.update_display(d); + break; + case 'display_is_done': + var d = utils.find(curt.displays, m => m.client_id === c.val.client_id); + if(d.wait_for_done && d.wait_for_ctype == c.val.ctype) { + d.wait_for_done = false; + ctournament.update_display(d); + } + break; default: cerror.silent('Unsupported change type ' + c.ctype); } diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 25a2592..46459fa 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -559,8 +559,8 @@ var ctournament = (function() { }, (err) => { if (err) { return cerror.net(err); - } - input.closest('form').reset(); + }` + input.closest('form').reset();` }); }; reader.onerror = (e) => { @@ -1369,7 +1369,8 @@ var ctournament = (function() { const display_settings_tbody = uiu.el(display_settings_table, 'tbody'); const tr = uiu.el(display_settings_tbody, 'tr'); uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:setting')); - uiu.el(tr, 'td', {}, ci18n('tournament:edit:displays:description')); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:description')); + uiu.el(tr, 'th', {}, ""); for (const s of curt.displaysettings) { const tr = uiu.el(display_settings_tbody, 'tr'); @@ -1423,15 +1424,15 @@ var ctournament = (function() { function on_edit_display_setting_button_click(e) { const btn = e.target; const display_setting_id = btn.getAttribute('data-display_setting_id'); - console.log(display_setting_id); + //console.log(display_setting_id); ui_edit_display_setting(display_setting_id); } function ui_edit_display_setting(display_setting_id) { - console.log(display_setting_id); - console.log(curt); + //console.log(display_setting_id); + //console.log(curt); const display_setting = structuredClone(utils.find(curt.displaysettings, d => d.id === display_setting_id)); - console.log(display_setting); + //console.log(display_setting); crouting.set('t/' + curt.key + '/edit/s/' + display_setting_id, {}, _cancel_ui_edit_display_setting); @@ -1463,9 +1464,9 @@ var ctournament = (function() { }, ci18n('Change')); form_utils.onsubmit(form, function(d) { - console.log(d); + //console.log(d); const displaysetting = create_displaysettings_object(d); - console.log(displaysetting); + //console.log(displaysetting); send({ type: 'edit_display_setting', @@ -1761,7 +1762,7 @@ var ctournament = (function() { uiu.el(general_displaysettings_div, 'h3', 'edit', ci18n('tournament:edit:displays')); const display_table = uiu.el(general_displaysettings_div, 'table'); - const display_tbody = uiu.el(display_table, 'tbody'); + const display_tbody = uiu.el(display_table, 'tbody', 'display_tbody'); const tr = uiu.el(display_tbody, 'tr'); uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:num')); uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:hostname')); @@ -1769,6 +1770,8 @@ var ctournament = (function() { uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:court')); uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:setting')); uiu.el(tr, 'th', {}, ci18n('tournament:edit:displays:onlinestatus')); + uiu.el(tr, 'th', {}, ""); + uiu.el(tr, 'th', {}, ""); for (const display of curt.displays) { @@ -1778,13 +1781,40 @@ var ctournament = (function() { } function update_display(display) { - uiu.qsEach('[data-display_id=' + JSON.stringify(display.client_id) + ']', function (display_tr) { - display_tr.innerHTML = ''; - render_display(display_tr, display); - }); + // Do this function only if the Display view (in on edit) is open + if(!document.querySelectorAll('.display_tbody').length) { + return; + } + + var nodes = document.querySelectorAll('[data-display_id=' + JSON.stringify(display.client_id) + ']'); + if(nodes.length > 0) { + uiu.qsEach('[data-display_id=' + JSON.stringify(display.client_id) + ']', function (display_tr) { + display_tr.innerHTML = ''; + render_display(display_tr, display); + }); + } + else { + new_display(display); + } + } + + function new_display(display) { + const display_tbody = document.querySelector(".display_tbody"); + const tr = uiu.el(display_tbody, 'tr', { 'data-display_id': display.client_id }); + render_display(tr, display); + + for (const child of display_tbody.children) { + const child_id = child.dataset.display_id; + if(child_id && Number(child_id) > Number(display.client_id)) + { + display_tbody.insertBefore(tr, child); + } + } } + function render_display(tr, display) { + tr.setAttribute('class', (!display.online) ? 'offline' : (display.wait_for_done ? 'wait_for_done' : 'online')); uiu.el(tr, 'th', {}, display.client_id); uiu.el(tr, 'th', {}, display.hostname); var battery_node = uiu.el(tr, 'td', {}, 'N/A'); @@ -1794,19 +1824,40 @@ var ctournament = (function() { uiu.el(tr, 'td', {}, (!display.online) ? 'offline' : 'online'); const actions_td = uiu.el(tr, 'td', {}); const reset_btn = uiu.el(actions_td, 'button', { - 'data-display-setting-id': display.client_id, + 'data-display-client-id': display.client_id, }, 'Restart'); if (!display.online) { reset_btn.setAttribute('disabled', 'disabled'); } reset_btn.addEventListener('click', function (e) { + const rst_btn = e.target; + const display_client_id = rst_btn.getAttribute('data-display-client-id'); + send({ + type: 'display_reset', + tournament_key: curt.key, + display_client_id: display_client_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + + const delete_td = uiu.el(tr, 'td', {}); + const delete_btn = uiu.el(delete_td, 'button', { + 'data-display-client-id': display.client_id, + }, 'Delete'); + if (display.online) { + delete_btn.setAttribute('disabled', 'disabled'); + } + delete_btn.addEventListener('click', function (e) { const del_btn = e.target; - const display_setting_id = del_btn.getAttribute('data-display-setting-id'); + const display_client_id = del_btn.getAttribute('data-display-client-id'); send({ - type: 'reset_display', + type: 'display_delete', tournament_key: curt.key, - display_setting_id: display_setting_id, + display_client_id: display_client_id, }, err => { if (err) { return cerror.net(err); @@ -1814,6 +1865,14 @@ var ctournament = (function() { }); }); } + + function delete_display(c) { + uiu.qsEach('[data-display_id=' + JSON.stringify(c.val) + ']', function (display_tr) { + display_tr.parentNode.removeChild(display_tr); + }); + } + + function render_courts(main) { uiu.el(main, 'h2', 'edit', ci18n('tournament:edit:courts')); @@ -2262,6 +2321,7 @@ var ctournament = (function() { remove_advertisement, add_advertisement, update_general_displaysettings, + delete_display, }; })(); From 0eaab3173917e3d0d8f1263fcd16aa5527ffb718 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 25 Apr 2025 17:06:09 +0200 Subject: [PATCH 195/224] current Version --- bts/admin.js | 184 ++++++- bts/btp_conn.js | 2 +- bts/btp_parse.js | 9 + bts/btp_sync.js | 272 +++++++++- bts/bupws.js | 89 ++++ bts/database.js | 3 + bts/match_utils.js | 12 +- bts/stournament.js | 13 + static/css/admin.css | 55 ++ static/css/cmatch.css | 29 +- static/css/court.css | 55 +- static/icons/confirm_match.svg | 85 ++++ static/icons/court_inactive.svg | 169 +++++++ static/icons/inactive.svg | 123 +++++ static/js/announcements.js | 2 +- static/js/change.js | 91 +++- static/js/ci18n_de.js | 20 +- static/js/ci18n_en.js | 15 + static/js/cmatch.js | 214 +++++++- static/js/cto_stats.js | 3 - static/js/ctournament.js | 857 +++++++++++++++++++++----------- 21 files changed, 1904 insertions(+), 398 deletions(-) create mode 100644 static/icons/confirm_match.svg create mode 100644 static/icons/court_inactive.svg create mode 100644 static/icons/inactive.svg diff --git a/bts/admin.js b/bts/admin.js index 276943d..8f8098b 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -114,6 +114,37 @@ function handle_tournament_edit_props(app, ws, msg) { }); } + +function handle_tournament_edit_logo(app, ws, msg) { + if (! msg.key) { + return ws.respond(msg, {message: 'Missing key'}); + } + if (! msg.props) { + return ws.respond(msg, {message: 'Missing props'}); + } + + const key = msg.key; + const props = utils.pluck(msg.props, [ + 'logo_background_color', 'logo_foreground_color']); + + app.db.tournaments.findOne({ key }, async (err, tournament) => { + if (err || !tournament) { + ws.respond(msg, err); + return; + } + app.db.tournaments.update({ key }, { $set: props }, { returnUpdatedDocs: true }, function (err) { + if (err) { + ws.respond(msg, err); + return; + } + + notify_change(app, key, 'logo_changed', {logo_foreground_color : props.logo_foreground_color, logo_background_color: props.logo_background_color}); + + ws.respond(msg, err); + }); + }); +} + function handle_courts_add(app, ws, msg) { if (! msg.tournament_key) { return ws.respond(msg, {message: 'Missing tournament_key'}); @@ -143,6 +174,65 @@ function handle_courts_add(app, ws, msg) { }); } +function handle_court_edit(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'court_id'])) { + return; + } + + const tournament_key = msg.tournament_key; + const court_id = msg.court_id; + + const query = { + tournament_key, + _id: court_id, + }; + + app.db.courts.findOne(query, async (err, court) => { + if (err || !court) { + ws.respond(msg, err); + return; + } + const is_active = (msg.is_active != undefined ? msg.is_active : court.is_active); + app.db.courts.update(query, { $set: {is_active} }, {}, (err) => { + if(err) { + ws.respond(msg, err); + return; + } + + notify_change(app, msg.tournament_key, 'court_changed', {court_id, is_active}); + ws.respond(msg); + return; + }); + }); +} + +function handle_location_changed(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'location_id', 'highlight', 'preperation_addition', 'meetingpoint_announcement'])) { + return; + } + const location_id = msg.location_id; + console.log(location_id); + const preperation_addition = msg.preperation_addition; + const meetingpoint_announcement = msg.meetingpoint_announcement; + const highlight = msg.highlight; + + const query = { + tournament_key: msg.tournament_key, + _id: msg.location_id, + }; + + app.db.locations.update(query, { $set: {highlight, preperation_addition, meetingpoint_announcement} }, {}, (err) => { + if(err) { + ws.respond(msg, err); + return; + } + + notify_change(app, msg.tournament_key, 'location_changed', {location_id, highlight, preperation_addition, meetingpoint_announcement}); + ws.respond(msg); + return; + }); +} + function generate_tournament_web_url(tournament) { var url = ""; if (tournament.ticker_enabled) { @@ -180,6 +270,12 @@ function handle_tournament_get(app, ws, msg) { { } }, function(cb) { + stournament.get_locations(app.db, tournament.key, function(err, locations) { + + tournament.locations = locations; + cb(err); + }); + }, function(cb) { stournament.get_courts(app.db, tournament.key, function(err, courts) { tournament.courts = courts; cb(err); @@ -692,7 +788,7 @@ function handle_match_player_check_in (app, ws, msg) { - match_utils.match_update(app, match, (err) => { + match_utils.match_update(app, match, undefined, (err) => { ws.respond(msg, err); return; }); @@ -821,17 +917,32 @@ function handle_edit_display_setting(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'displaysetting'])) { return; } + const bupws = require('./bupws'); const querry = {id : msg.displaysetting.id}; const displaysetting = msg.displaysetting; + const tournament_key = msg.tournament_key; - //{_id: msg.id, tournament_key}, {$set: {setup}}, {returnUpdatedDocs: true}, function(err, numAffected, changed_match) app.db.displaysettings.update(querry, {$set: displaysetting}, {returnUpdatedDocs: true}, (err, numAffected, changed_setting) => { - console.log(err); - console.log(numAffected); - console.log(changed_setting); + }); - ws.respond(msg); + notify_change(app, msg.tournament_key, 'update_display_setting', {setting: displaysetting}); + + app.db.display_court_displaysettings.find({}, function(err, all_displays) { + if (err) { + return ws.respond(msg, err); + } + + const updated_displays = all_displays.filter( + m => (m.displaysetting_id == displaysetting.id) + ); + + updated_displays.forEach((display) => { + bupws.change_display_mode(app, tournament_key, display.client_id, displaysetting.id); + }); + + ws.respond(msg); + }); } async function async_handle_delete_display_setting(app, ws, msg) { @@ -867,7 +978,6 @@ function handle_change_display_mode(app, ws, msg) { const new_displaysettings_id = msg.new_displaysettings_id; const bupws = require('./bupws'); bupws.change_display_mode(app, tournament_key, client_id, new_displaysettings_id); - notify_change(app, tournament_key, 'update_general_displaysettings', {}); ws.respond(msg); } @@ -981,7 +1091,7 @@ function on_close(app, ws) { } async function async_handle_tournament_upload_logo(app, ws, msg) { - if (!_require_msg(ws, msg, ['tournament_key', 'data_url'])) { + if (!_require_msg(ws, msg, ['tournament_key', 'data_url', 'name'])) { return; } @@ -1016,12 +1126,62 @@ async function async_handle_tournament_upload_logo(app, ws, msg) { const buf = Buffer.from(logo_b64, 'base64'); const logo_id = uuidv4() + '.' + ext; await promisify(fs.writeFile)(path.join(utils.root_dir(), 'data', 'logos', logo_id), buf); + const logo_name = msg.name; const [_, updated_tournament] = await app.db.tournaments.update_async( // eslint-disable-line no-unused-vars {key: msg.tournament_key}, - {$set: {logo_id}}, + {$set: {logo_id, logo_name}}, + {returnUpdatedDocs: true}); + notify_change(app, msg.tournament_key, 'logo_changed', {logo_id, logo_name}); + + return ws.respond(msg, null, {}); +} + +async function async_handle_tournament_upload_location_logo(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'data_url', 'name', 'location_id'])) { + return; + } + + const tournament = await app.db.tournaments.findOne_async({ + key: msg.tournament_key, + }); + if (!tournament) { + ws.respond(msg, {message: `Could not find tournament ${msg.tournament_key}`}); + return; + } + + const m = /^data:(image\/[a-z+]+)(?:;base64)?,([A-Za-z0-9+/=]+)$/.exec(msg.data_url); + if (!m) { + ws.respond(msg, {message: `Invalid base64 URI, starts with ${msg.data_url.slice(0, 80)}`}); + return; + } + const mime_type = m[1]; + const logo_b64 = m[2]; + + const ext = { + 'image/gif': 'gif', + 'image/png': 'png', + 'image/jpeg': 'jpg', + 'image/svg+xml': 'svg', + 'image/webp': 'webp', + }[mime_type]; + if (!ext) { + ws.respond(msg, {message: `Unsupported mime type ${mime_type}`}); + return; + } + + const buf = Buffer.from(logo_b64, 'base64'); + const logo_id = uuidv4() + '.' + ext; + await promisify(fs.writeFile)(path.join(utils.root_dir(), 'data', 'logos', logo_id), buf); + const logo_name = msg.name; + const location_id = msg.location_id; + const tournament_key = msg.tournament_key + + const [_, updated_tournament] = await app.db.locations.update_async( // eslint-disable-line no-unused-vars + {tournament_key, _id: location_id}, + {$set: {logo_id, logo_name}}, {returnUpdatedDocs: true}); - notify_change(app, msg.tournament_key, 'props', updated_tournament); + notify_change(app, msg.tournament_key, 'location_logo_changed', {location_id, logo_id, logo_name}); return ws.respond(msg, null, {}); } @@ -1031,6 +1191,7 @@ module.exports = { async_handle_delete_display_setting, async_handle_match_delete, async_handle_tournament_upload_logo, + async_handle_tournament_upload_location_logo, handle_begin_to_play_call, handle_announce_match_manually, handle_btp_fetch, @@ -1046,6 +1207,8 @@ module.exports = { handle_fetch_allscoresheets_data, handle_create_tournament, handle_courts_add, + handle_court_edit, + handle_location_changed, handle_match_add, handle_match_edit, handle_match_call_on_court, @@ -1062,6 +1225,7 @@ module.exports = { handle_tournament_get, handle_tournament_list, handle_tournament_edit_props, + handle_tournament_edit_logo, handle_display_delete, handle_display_reset, handle_relocate_display, diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 417e2fc..e74f090 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -331,7 +331,7 @@ class BTPConn { const req = btp_proto.update_players_request(players, this.key_unicode, this.password); this.send(req, response => { const results = response.Action[0].Result; - const rescode = results ? results[0] : 'no-result'; + const rescode = (results && results.length > 0) ? results[0] : 'no-result'; if (rescode === 1) { } else { diff --git a/bts/btp_parse.js b/bts/btp_parse.js index 9d996f6..2d5f823 100644 --- a/bts/btp_parse.js +++ b/bts/btp_parse.js @@ -179,7 +179,10 @@ function get_btp_state(response) { const all_btp_draws = btp_t.Draws ? btp_t.Draws[0].Draw : []; const all_btp_officials = btp_t.Officials ? btp_t.Officials[0].Official : []; const all_btp_courts = btp_t.Courts ? btp_t.Courts[0].Court : []; + const all_btp_locations = btp_t.Locations ? btp_t.Locations[0].Location : []; const all_btp_settings = btp_t.Settings ? btp_t.Settings[0].Setting : []; + const all_btp_clubs = btp_t.Clubs ? btp_t.Clubs[0].Club : []; + const all_btp_districts = btp_t.Districts ? btp_t.Districts[0].District : []; const on_court_match_ids = new Set(); for (const c of all_btp_courts) { @@ -212,6 +215,9 @@ function get_btp_state(response) { const players = utils.make_index(all_btp_players, p => p.ID[0]); const officials = utils.make_index(all_btp_officials, o => o.ID[0]); const courts = utils.make_index(all_btp_courts, c => c.ID[0]); + const locations = utils.make_index(all_btp_locations, l => l.ID[0]); + const clubs = utils.make_index(all_btp_clubs, c => c.ID[0]); + const districts = utils.make_index(all_btp_districts,d => d.ID[0]); const btp_settings = utils.make_index(all_btp_settings, s =>s.ID[0]); for (const bm of matches) { @@ -219,6 +225,7 @@ function get_btp_state(response) { } return { courts, + locations, draws, events, matches, @@ -228,6 +235,8 @@ function get_btp_state(response) { match_types: new Map(Object.entries(MATCH_TYPES)), team_matches, teams, + clubs, + districts, // Testing only _matches_by_pid: matches_by_pid, btp_settings diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 2236658..ec00535 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -18,7 +18,7 @@ function date_str(dt) { return utils.pad(dt.year, 2, '0') + '-' + utils.pad(dt.month, 2, '0') + '-' + utils.pad(dt.day, 2, '0'); } -async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, officials, bm, match_ids_on_court, match_types, is_league) { +async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, officials, clubs, districts, bm, match_ids_on_court, match_types, is_league) { return new Promise((resolve, reject) => { const gtid = event.GameTypeID[0]; @@ -26,9 +26,51 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, const scheduled_time_str = (bm.PlannedTime ? time_str(bm.PlannedTime[0]) : undefined); const scheduled_date = (bm.PlannedTime ? date_str(bm.PlannedTime[0]) : undefined); - const match_name = (bm.RoundName && bm.RoundName[0] ? bm.RoundName[0] : undefined); - const event_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + ' - ' + draw.Name[0]; - const teams = _craft_teams(bm); + var match_name = (bm.RoundName && bm.RoundName[0] ? bm.RoundName[0] : undefined); + var event_name = event.Name[0]; + const teams = _craft_teams(bm, clubs, districts); + + const rounds = new Map(); + rounds.set("Finale", [ 1, 2]); + rounds.set("3/4", [ 3, 4]); + rounds.set("5/6", [ 5, 6]); + rounds.set("7/8", [ 7, 8]); + rounds.set("9/10", [ 9, 10]); + rounds.set("11/12", [11, 12]); + rounds.set("13/14", [13, 14]); + rounds.set("15/16", [15, 16]); + rounds.set("17/18", [17, 18]); + rounds.set("19/20", [19, 20]); + rounds.set("21/22", [21, 22]); + rounds.set("23/24", [23, 24]); + rounds.set("25/26", [25, 26]); + rounds.set("27/28", [27, 28]); + rounds.set("29/30", [29, 30]); + rounds.set("31/32", [31, 32]); + rounds.set("HF", [ 1, 4]); + rounds.set("5/8", [ 5, 8]); + rounds.set("9/12", [ 9, 12]); + rounds.set("13/16", [13, 16]); + rounds.set("17/20", [17, 20]); + rounds.set("21/24", [21, 24]); + rounds.set("25/28", [25, 28]); + rounds.set("29/32", [29, 32]); + rounds.set("VF", [ 1, 8]); + rounds.set("9/16", [ 9, 16]); + rounds.set("17/24", [17, 24]); + rounds.set("25/32", [25, 32]); + rounds.set("R16", [ 1, 16]); + rounds.set("17/32", [17, 32]); + rounds.set("R32", [ 1, 32]); + + if(match_name && draw.Position[0] > 1 && rounds.get(match_name)) { + const best_place = rounds.get(match_name)[0] + draw.Position[0] - 1; + const lowes_place = rounds.get(match_name)[1] + draw.Position[0] - 1; + + match_name = best_place + "/" + lowes_place; + } else { + event_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + ' - ' + draw.Name[0]; + } const btp_player_ids = []; @@ -246,35 +288,64 @@ function _craft_team(par) { pres.checked_in = p.CheckedIn[0]; } - if (p.State) { - switch (p.State[0]) { - case 'NIS': { - pres.state = "Niedersachsen"; - break; - } case 'SLH': { - pres.state = "Schleswig-Holstein"; + const club = this.clubs.get(p.ClubID[0]); + const district = this.districts.get(club.DistrictID[0]); + const state_by_district = district.Name[0].split("-")[0]; + + var state = (state_by_district ? state_by_district : (p.State && p.Satate.length > 0 ? p.State[0] : undefined)); + if (state) { + switch (state) { + case 'BAW' : { + pres.state = "Baden-Württemberg"; break; - } case 'BRE': { - pres.state = "Bremen"; + } case 'BAY' : { + pres.state = "Bayern"; break; } case 'BBB': { - pres.state = "Berlin Brandenburg"; + pres.state = "Berlin-Brandenburg"; break; - } case 'SAH': { - pres.state = "Sachsen Anhalt"; + } case 'BRE': { + pres.state = "Bremen"; break; } case 'HAM': { pres.state = "Hamburg"; break; + } case 'HES': { + pres.state = "Hessen"; + break; } case 'MVP': { - pres.state = "Mecklenburg Vorpommern"; + pres.state = "Mecklenburg-Vorpommern"; + break; + } case 'NIS': { + pres.state = "Niedersachsen"; break; } case 'NRW': { - pres.state = "Nordrhein Westfalen"; + pres.state = "Nordrhein-Westfalen"; break; - } + } case 'RHP': { + pres.state = "Rheinhessen-Pfalz"; + break; + } case 'RHL': { + pres.state = "Rheinland"; + break; + } case 'SAA': { + pres.state = "Saarland"; + break; + } case 'SAC': { + pres.state = "Sachsen"; + break; + } case 'SAH': { + pres.state = "Sachsen-Anhalt"; + break; + } case 'SLH': { + pres.state = "Schleswig-Holstein"; + break; + } case 'THÜ': { + pres.state = "Thüringen"; + break; + } default: - pres.state = p.State[0] + pres.state = state } } fix_player(pres); @@ -294,9 +365,12 @@ function _craft_team(par) { return tres; } -function _craft_teams(bm) { +function _craft_teams(bm, clubs, districts) { assert(bm.bts_players); - return bm.bts_players.map(_craft_team); + assert(clubs); + assert(districts); + + return bm.bts_players.map(_craft_team, {clubs: clubs, districts: districts}); } function _parse_score(bm) { @@ -408,8 +482,11 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { const officials = await get_umpires(app, tkey); const matches_to_add = []; + const matches_player_changed = []; const matches_on_court = []; const matches_incomplete = []; + const clubs = btp_state.clubs; + const districts = btp_state.districts; async.each(btp_state.matches, function (bm, cb) { const draw = draws.get(bm.DrawID[0]); @@ -440,7 +517,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { return; } - craft_match(app, tkey, btp_id, court_map, event, draw, btp_state.links, officials, bm, match_ids_on_court).then(match => { + craft_match(app, tkey, btp_id, court_map, event, draw, btp_state.links, officials, clubs, districts, bm, match_ids_on_court).then(match => { match.setup.state = 'unscheduled'; @@ -558,10 +635,19 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { // equals checked_in changed and check if it was the only change let only_change_check_in = false; let result_enterd_in_btp = false; + let match_player_changed = false; for (let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { + if(cur_match.setup.teams[team_index].players.length < match.setup.teams[team_index].players.length){ + for (let player_index = 0; player_index < match.setup.teams[team_index].players.length; player_index++) { + match_player_changed = true; + } + } for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { cur_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; + if(match.setup.teams[team_index].players[player_index].btp_id != cur_match.setup.teams[team_index].players[player_index].btp_id) { + match_player_changed = true; + } } } @@ -587,6 +673,10 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { only_change_check_in = true; } + if(match_player_changed) { + matches_player_changed.push(match); + } + app.db.matches.update({ _id: cur_match._id }, { $set: match }, {}, (err) => { if (err) { cb(err); @@ -626,6 +716,19 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { console.error(error); } + matches_player_changed.forEach(async (match_player_changed) => { + let match = match_player_changed; + matches_on_court.forEach(async (match_on_court) => { + const changed_match_on_court = await match_utils.calc_match_set_player_on_court(match, match_on_court.setup); + if(changed_match_on_court != null) { + match = changed_match_on_court; + } + const changed_match_tablet_operator = await match_utils.calc_match_set_player_on_tablet(match, match_on_court.setup); + if(changed_match_tablet_operator != null) { + match = changed_match_tablet_operator; + } + }); + }); matches_to_add.forEach(async (match_to_add) => { let match = match_to_add; @@ -667,6 +770,118 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { }); } + +function integrate_locations(app, tournament_key, btp_state, callback) { + const admin = require('./admin'); // avoid dependency cycle + const stournament = require('./stournament'); // avoid dependency cycle + + const locations = Array.from(btp_state.locations.values()); + const res = new Map(); + var changed = false; + + async.eachSeries(locations, (l, cb) => { + const btp_id = l.ID[0]; + const name = l.Name[0]; + const address = (l.Address1 ? l.Address1[0] : ""); + const postal_code = (l.PostalCode ? l.PostalCode[0] : ""); + const city = (l.City ? l.City[0] : ""); + const state = (l.State ? l.State[0] : ""); + const country = (l.Country ? l.Country[0] : ""); + const preperation_addition = ""; + const meetingpoint_announcement = ""; + + const query = { + tournament_key, + btp_id, + name, + address, + postal_code, + city, + state, + country, + }; + + + + + app.db.locations.findOne(query, (err, cur_location) => { + if (err) return cb(err); + if (cur_location) { + return cb(); + } + + const alt_query = { + tournament_key, + btp_id, + }; + + + + + + + + app.db.locations.findOne(alt_query, async (err, cur_location) => { + if (err) return cb(err); + + if (cur_location) { + + //ADD BTP ID + app.db.locations.update(alt_query, { $set: { btp_id, name, address, postal_code, city, state, country, preperation_addition, meetingpoint_announcement} }, {}, (err) => cb(err)); + return; + } + + const highlights = ['highlight_0', 'highlight_1', 'highlight_2', 'highlight_3', 'highlight_4', 'highlight_5', 'highlight_6']; + let highlight = null; + + for (let i = highlights.length - 1; i >= 0; i--) { + const test = await app.db.locations.findOne_async({ tournament_key, highlight: highlights[i] }); + + if (!test) { + highlight = highlights[i]; + break; + } + } + + const location = { + _id: tournament_key + '_' + btp_id, + tournament_key, + btp_id, + name, + address, + postal_code, + city, + state, + country, + preperation_addition, + meetingpoint_announcement, + highlight, + }; + + changed = true; + app.db.locations.insert(location, (err) => cb(err)); + }); + }); + + }, (err) => { + if (err) { + return callback(err); + } + + if (changed) { + stournament.get_locations(app.db, tournament_key, function (err, all_locations) { + admin.notify_change(app, tournament_key, 'location_changed', { all_locations }); + callback(err); + }); + } else { + callback(err); + } + }); +} + + + + // Returns a map btp_court_id => court._id function integrate_courts(app, tournament_key, btp_state, callback) { const admin = require('./admin'); // avoid dependency cycle @@ -675,10 +890,10 @@ function integrate_courts(app, tournament_key, btp_state, callback) { const courts = Array.from(btp_state.courts.values()); const res = new Map(); var changed = false; - async.each(courts, (c, cb) => { const btp_id = c.ID[0]; const name = c.Name[0]; + const location_id = tournament_key + "_" + c.LocationID[0]; let num = parseInt(name, 10) || btp_id; const m = /^Court\s*([0-9]+)$/.exec(name); if (m) { @@ -688,10 +903,11 @@ function integrate_courts(app, tournament_key, btp_state, callback) { btp_id, name, num, + location_id, tournament_key, }; - app.db.courts.findOne(query, (err, cur_court) => { + app.db.courts.findOne(query, async (err, cur_court) => { if (err) return cb(err); if (cur_court) { res.set(btp_id, cur_court._id); @@ -708,14 +924,17 @@ function integrate_courts(app, tournament_key, btp_state, callback) { btp_id, num, name, + location_id, + is_active : true, }; + res.set(btp_id, court._id); app.db.courts.findOne(alt_query, (err, cur_court) => { if (err) return cb(err); if (cur_court) { // Add BTP ID - app.db.courts.update(alt_query, { $set: { btp_id } }, {}, (err) => cb(err)); + app.db.courts.update(alt_query, { $set: { btp_id, location_id } }, {}, (err) => cb(err)); return; } @@ -1049,6 +1268,7 @@ async function sync_btp_data(app, tkey, response) { cb => integrate_btp_settings(app, tkey, btp_state, cb), cb => integrate_player_state(app, tkey, btp_state, cb), cb => integrate_umpires(app, tkey, btp_state, cb), + cb => integrate_locations(app, tkey, btp_state, cb), cb => integrate_courts(app, tkey, btp_state, cb), (court_map, cb) => integrate_matches(app, tkey, btp_state, court_map, cb), cb => integrate_now_on_court(app, tkey, cb), diff --git a/bts/bupws.js b/bts/bupws.js index 81d662d..cbe0df9 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -407,6 +407,7 @@ function extractIPv4FromMappedIPv6(ip) { return match ? match[1] : null; } +/* async function determine_client_hostname(ws) { if (ws.hostname) { return ws.hostname; @@ -483,6 +484,94 @@ async function determine_client_hostname(ws) { ws.hostname = "N/N"; return ws.hostname; } +}*/ + +async function determine_client_hostname(ws) { + if (ws.hostname) { + return ws.hostname; + } + + let remoteAddress = ws._socket.remoteAddress; + let ipv4 = extractIPv4FromMappedIPv6(remoteAddress); + let ip = ipv4 || remoteAddress; + + // Lokale Adressen behandeln + if (ip === "127.0.0.1") { + ws.hostname = getComputerName(); + return ws.hostname; + } + + // Bei ungültiger IP + if (!net.isIP(ip)) { + console.error("Invalid IP address:", remoteAddress); + ws.hostname = "N/N"; + return ws.hostname; + } + + // 1. Falls IPv4 verfügbar → versuchen Reverse-Lookup + if (ipv4) { + try { + const hostnames = await dnsReverseWithTimeout(ipv4, 3000); + ws.hostname = hostnames?.[0]?.split(".")[0] || ipv4; + return ws.hostname; + } catch (err) { + if (err.code !== 'ENOTFOUND') { + console.error("IPv4 DNS reverse lookup failed:", err); + } + // IPv4 Lookup fehlgeschlagen → weiter mit IPv6 versuchen + ip = remoteAddress; // original IPv6 verwenden + } + } + + // 2. Jetzt IPv6 Reverse-Lookup versuchen + if (net.isIPv6(ip)) { + if (ip === "::1") { + ws.hostname = getComputerName(); + return ws.hostname; + } + + try { + const hostnames = await dnsReverseWithTimeout(ip, 3000); + ws.hostname = hostnames?.[0]?.split(".")[0] || ipv4 || ip; + return ws.hostname; + } catch (err) { + if (err.code !== 'ENOTFOUND') { + console.error("IPv6 DNS reverse lookup failed:", err); + } + // 3. Fallback: IPv4-Adresse als Text + ws.hostname = ipv4 || ip; + return ws.hostname; + } + } + + // Sollte nicht vorkommen, aber falls doch: + ws.hostname = ipv4 || ip; + return ws.hostname; +} + +// Hilfsfunktion: extrahiert IPv4 aus gemapptem IPv6, z.B. ::ffff:192.168.0.1 => 192.168.0.1 +function extractIPv4FromMappedIPv6(address) { + if (typeof address !== "string") return null; + const match = address.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/); + return match ? match[1] : null; +} + +// Hilfsfunktion: DNS-Reverse mit Timeout +function dnsReverseWithTimeout(ip, timeoutMs) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error("DNS reverse lookup timeout")); + }, timeoutMs); + + dns.reverse(ip, (err, hostnames) => { + clearTimeout(timer); + if (err) { + reject(err); + } else { + resolve(hostnames); + } + }); + }); } diff --git a/bts/database.js b/bts/database.js index cb805a9..666dff0 100644 --- a/bts/database.js +++ b/bts/database.js @@ -11,6 +11,7 @@ const utils = require('./utils'); const TABLES = [ 'courts', + 'locations', 'event', 'matches', 'tournaments', @@ -61,6 +62,8 @@ function init(callback) { function prepare(db, callback) { db.courts.ensureIndex({fieldName: 'tournament_key', unique: false}); + db.locations.ensureIndex({fieldName: 'tournament_key', unique: false}); + db.locations.ensureIndex({fieldName: 'location_id', unique: false}); db.matches.ensureIndex({fieldName: 'court_id', unique: false}); db.matches.ensureIndex({fieldName: 'tournament_key', unique: false}); db.matches.ensureIndex({fieldName: 'event_key', unique: false}); diff --git a/bts/match_utils.js b/bts/match_utils.js index ded4faf..2f21121 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -812,8 +812,10 @@ function reset_tabletoperator_settings_at_player(app, tkey, tournament, player, const btp_manager = require('./btp_manager'); player.now_tablet_on_court = false; - if (tournament.tabletoperator_set_break_after_tabletservice) { - var offset = 0; + const now = Date.now(); + if (tournament.tabletoperator_set_break_after_tabletservice && + (now + (parseInt(tournament.tabletoperator_break_seconds) * 1000)) >= player.last_time_on_court_ts + tournament.btp_settings.pause_duration_ms) { + var offset = 0; if (tournament.tabletoperator_break_seconds) { offset = (parseInt(tournament.tabletoperator_break_seconds) * 1000) - tournament.btp_settings.pause_duration_ms; } @@ -823,7 +825,11 @@ function reset_tabletoperator_settings_at_player(app, tkey, tournament, player, btp_manager.update_players(app, tkey, [player]); } else { - player.checked_in = true; + if (player.last_time_on_court_ts) { + if ((now - player.last_time_on_court_ts) > tournament.btp_settings.pause_duration_ms) { + player.checked_in = true; + } + } player.tablet_break_active = false; btp_manager.update_players(app, tkey, [player]); } diff --git a/bts/stournament.js b/bts/stournament.js index eb5c7da..dd623d2 100644 --- a/bts/stournament.js +++ b/bts/stournament.js @@ -4,6 +4,18 @@ const utils = require('./utils'); +function get_locations(db, tournament_key, callback) { + db.locations.find({tournament_key}, function(err, locations) { + if (err) return callback(err); + + locations.sort(function(l1, l2) { + return utils.natcmp(('' + l1.btp_id), ('' + l2.btp_id)); + }); + return callback(err, locations); + }); +} + + function get_courts(db, tournament_key, callback) { db.courts.find({tournament_key}, function(err, courts) { if (err) return callback(err); @@ -76,6 +88,7 @@ function get_displaysettings(db, tournament_key, callback) { } module.exports = { + get_locations, get_courts, get_matches, get_umpires, diff --git a/static/css/admin.css b/static/css/admin.css index 9786e02..02efdf5 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -337,3 +337,58 @@ h2.edit { border-spacing: 0; width: 100%; } + +.edit_display_setting_container{ + text-align: left; +} + +.edit_display_setting_container > div{ + margin: 10px; + display: flex; + flex-direction: row; + justify-content: space-between; +} +.edit_display_setting_container > div > input[type=text]{ + width: 400px; +} + +.edit_display_setting_container > div > input{ + width: 200px; +} + +.edit_display_setting_container > div > select{ + width: 300px; +} + + +#meetingpoint_announcement{ + width: -webkit-fill-available; + resize: vertical; + min-height: 5.0em; + height: 5.0em; +} + + +#preperation_addition{ + width: -webkit-fill-available;; + resize: vertical; + min-height: 5.0em; + height: 5.0em; +} + +.courts_table{ + text-align: center; +} + + +.icon_td { + text-align: center; +} + +.upload_filename_location{ + font-style: italic; + color: #555; + max-width: 150px; + inline-size: 150px; + overflow-wrap: break-word; +} \ No newline at end of file diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 9ae403e..9cf381d 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -67,6 +67,14 @@ background-image: url(../icons/rawinfo.svg); } +.match_timer > .match_confirm_button { + min-height: 1.6em; + background-size: cover; + background-color: #00000000; + width: 2.3em; + background-image: url(../icons/confirm_match.svg); +} + .edit_match_container { text-align: left; } @@ -83,7 +91,8 @@ .match_preparation_call_button:hover, .match_begin_to_play_button:hover, .match_manual_call_button:hover, -.match_scoresheet_button:hover { +.match_scoresheet_button:hover, +.match_confirm_button:hover { opacity: 0.5; } @@ -108,6 +117,16 @@ background-color: #00000030; } +.match_table > tbody > tr > td.inactive { + + background-color: #00000040; +} + +.match_table > tbody > tr:nth-of-type(even) > td.inactive { + + background-color: #00000050; +} + .match_table > tbody > tr > td.droppable_active { background-color: #fbe44d44; @@ -231,7 +250,7 @@ /*cursor: none;*/ } -.match_timer > div { +.match_timer > .timer { font-size: 1.2em; font-weight: 900; background-color: #111111; @@ -496,6 +515,10 @@ match_manual_call_button, text-wrap: nowrap; } +th > select { + width: 70px; +} + @media print { .toprow, .main, @@ -608,4 +631,4 @@ h3.section { .complete { cursor: grab; -} \ No newline at end of file +} diff --git a/static/css/court.css b/static/css/court.css index 218ed28..2e8303f 100644 --- a/static/css/court.css +++ b/static/css/court.css @@ -1,10 +1,10 @@ -.court_num { +.court_num, +.court_inactive { display: inline-block; background-size: cover; opacity: 1; color: #fff; text-align: center; - background-image: url(../icons/court.svg); height: 1.0em; min-height: 1.0em; width: 1.50em; @@ -19,32 +19,39 @@ 1px -1px 3px #000, -1px 1px 3px #000, 1px 1px 3px #000; - margin: -0.0em 0.0em 0.05em 0.0em; line-height: 100%; + cursor: pointer; } -.main_upcoming > div > .match_table > tbody > .court_row > .court_number > .court_num , -.main_upcoming > div > .match_table > tbody > .court_row > .court_num { - display: inline-block; - background-size: cover; - opacity: 1; - color: #fff; - text-align: center; + +.court_num { background-image: url(../icons/court.svg); - height: 1.0em; - min-height: 1.0em; - width: 1.2em; - min-width: 1.2em; - font-weight: 900; - font-size: 1.8em; - -webkit-text-stroke-width: 1px; - -webkit-text-stroke-color: black; - text-shadow: - 3px 3px 3px #000, - -1px -1px 3px #000, - 1px -1px 3px #000, - -1px 1px 3px #000, - 1px 1px 3px #000; margin: -0.0em 0.0em 0.05em 0.0em; +} + +.court_inactive { + background-image: url(../icons/court_inactive.svg); + margin: -0.0em 0.0em -3.2px 0.0em; +} + +.court_num:hover, +.court_inactive:hover { + opacity: 0.5; +} + +.main_upcoming > div > .match_table > tbody > .court_row > .court_number > .court_num , +.main_upcoming > div > .match_table > tbody > .court_row > .court_num { + line-height: 100%; + padding: 0.1em 0.2em 0em 0.2em; + width: 80px; + min-width: 80px; + cursor: none; +} + +.main_upcoming > div > .match_table > tbody > .court_row > .court_number > .court_inactive , +.main_upcoming > div > .match_table > tbody > .court_row > .court_inactive { line-height: 100%; padding: 0.1em 0.2em 0em 0.2em; + width: 80px; + min-width: 80px; + cursor: none; } diff --git a/static/icons/confirm_match.svg b/static/icons/confirm_match.svg new file mode 100644 index 0000000..8e2cb1f --- /dev/null +++ b/static/icons/confirm_match.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/static/icons/court_inactive.svg b/static/icons/court_inactive.svg new file mode 100644 index 0000000..16f6f5c --- /dev/null +++ b/static/icons/court_inactive.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/inactive.svg b/static/icons/inactive.svg new file mode 100644 index 0000000..6c8598b --- /dev/null +++ b/static/icons/inactive.svg @@ -0,0 +1,123 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/static/js/announcements.js b/static/js/announcements.js index 324b3ea..6871960 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -160,7 +160,7 @@ function createRoundAnnouncement(matchSetup) { var roundParts = round.split("/") var diff = roundParts[1] - roundParts[0]; if (diff > 1) { - round = ci18n('announcements:intermediate_round'); + round = ci18n('announcements:round_for_places') + roundParts[0] + ci18n('announcements:to') + roundParts[1]; } else { round = ci18n('announcements:game_for_place') + roundParts[0] + ci18n('announcements:and') + roundParts[1]; } diff --git a/static/js/change.js b/static/js/change.js index 474721e..fad3e16 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -31,9 +31,7 @@ function default_handler(rerender, special_funcs) { case 'free_announce': announce([c.val.text]); break; - case 'props': { - console.log(c.val); - + case 'props': { curt.name = c.val.name; curt.is_team = c.val.is_team; curt.tguid = c.val.tguid; @@ -88,6 +86,21 @@ function default_handler(rerender, special_funcs) { el.value = curt.ticker_password; }); break;} + case 'logo_changed': + if(c.val.logo_background_color != undefined) { + curt.logo_background_color = c.val.logo_background_color; + } + if(c.val.logo_foreground_color != undefined) { + curt.logo_foreground_color = c.val.logo_foreground_color; + } + if(c.val.logo_id != undefined) { + curt.logo_id = c.val.logo_id; + } + if(c.val.logo_name != undefined) { + curt.logo_name = c.val.logo_name; + } + ctournament.update_logo(); + break; case 'court_current_match': //nothing to do here break; @@ -131,6 +144,36 @@ function default_handler(rerender, special_funcs) { curt.courts = c.val.all_courts; rerender(); break; + case 'court_changed': + const court = utils.find(curt.courts, court => court._id === c.val.court_id); + if(court) { + court.is_active = c.val.is_active; + } + ctournament.update_court(court); + break; + case 'locations_changed': + curt.locations = c.val.all_locations; + rerender(); + break; + case 'location_changed': + const l = utils.find(curt.locations, l => l._id === c.val.location_id); + console.log(l); + + if(l) { + l.highlight = c.val.highlight; + l.preperation_addition = c.val.preperation_addition; + l.meetingpoint_announcement = c.val.meetingpoint_announcement; + } + ctournament.update_location(c.val.location_id, c.val.highlight, c.val.preperation_addition, c.val.meetingpoint_announcement); + break; + case 'location_logo_changed': + const loc = utils.find(curt.locations, loc => loc._id === c.val.location_id); + if(loc) { + loc.logo_name = c.val.logo_name; + loc.logo_id = c.val.logo_id; + } + ctournament.update_location_logo(c.val.location_id, loc.logo_id, loc.logo_name); + break; case 'match_preparation_call': announcePreparationMatch(c.val.match.setup); ctournament.update_match(c); @@ -178,6 +221,19 @@ function default_handler(rerender, special_funcs) { case 'advertisement_removed': ctournament.remove_advertisement(c); break; + //case 'update_player_status': { + // const cval = c.val; + // const id = cval.match__id; +// + // // Find the match + // const m = utils.find(curt.matches, m => m._id === id); + // if (!m) { + // cerror.silent('Cannot find match to update player status, ID: ' + JSON.stringify(id)); + // return; + // } + // m.btp_winner = cval.btp_winner; + // m.setup = cval.setup;} + // break; case 'umpires_changed': curt.umpires = c.val.all_umpires; uiu.qsEach('select[name="umpire_name"]', function(select) { @@ -224,10 +280,12 @@ function default_handler(rerender, special_funcs) { break; case 'update_display_setting': const updated_setting = c.val.setting; - const s = utils.find(curt.displaysettings, m => m.id === updated_setting.id); - if(!s) { + const index = curt.displaysettings.findIndex(m => m.id === updated_setting.id); + if (index === -1) { curt.displaysettings.push(updated_setting); curt.displaysettings.sort(utils.cmp_key('id')); + } else { + curt.displaysettings[index] = updated_setting; } ctournament.update_general_displaysettings(c); break; @@ -240,22 +298,29 @@ function default_handler(rerender, special_funcs) { case 'display_status_changed': const display_setting = c.val.display_court_displaysetting; var d = utils.find(curt.displays, m => m.client_id === display_setting.client_id); - var laststatus = false; + const last_d = {...d}; if (!d) { curt.displays[curt.displays.length] = display_setting; curt.displays.sort(utils.cmp_key('client_id')); return; } else { - laststatus = d.online; d.court_id = display_setting.court_id; d.displaysetting_id = display_setting.displaysetting_id; d.online = display_setting.online; - d.battery = display_setting.battery; + if(display_setting.battery){ + d.battery = display_setting.battery; + } + + if(d.displaysetting_id != last_d.displaysetting_id){ + ctournament.update_general_displaysettings(c); + } } - if (laststatus != d.online) { + if (last_d.online != d.online) { cerror.silent('Display ' + display_setting.client_id + ' is ' + (display_setting.online ? 'online' : 'offline')); } - ctournament.update_display(d); + if(!utils.deep_equal(d, last_d)){ + ctournament.update_display(d); + } break; case 'delete_display': const client_id = c.val.client_id; @@ -265,9 +330,11 @@ function default_handler(rerender, special_funcs) { break; case 'display_wait_for_done': var d = utils.find(curt.displays, m => m.client_id === c.val.client_id); - d.wait_for_done = true; d.wait_for_ctype = c.val.ctype; - ctournament.update_display(d); + if(!d.wait_for_done) { + d.wait_for_done = true; + ctournament.update_display(d); + } break; case 'display_is_done': var d = utils.find(curt.displays, m => m.client_id === c.val.client_id); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index fa7c14b..8c2e76b 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -51,6 +51,8 @@ var ci18n_de = { 'experimental': '(experimentell)', 'Winner': 'Gewinner', 'Loser': 'Verlierer', + 'activate_court': 'Spielfeld nutzen', + 'inactivate_court': 'Spielfeld sperren', 'announcements:begin_to_play': 'Bitte mit dem Spielen beginnen!', 'announcements:second_call': '"Zweiter Aufruf fuer:"', @@ -79,6 +81,8 @@ var ci18n_de = { 'announcements:semifinal': 'Halbfinale', 'announcements:final': 'Finale', 'announcements:intermediate_round': 'Zwischenrunde', + 'announcements:round_for_places': 'Runde um Platz', + 'announcements:to': 'bis', 'announcements:game_for_place': 'Spiel um Platz', 'announcements:voice': 'Google Deutsch', 'announcements:lang': 'de-DE', @@ -120,14 +124,14 @@ var ci18n_de = { 'display_setting:displaymode_update_interval': 'Displaymode Update Intervall (ms): ', - 'tournament:edit:tournament': 'Turnier:', - 'tournament:edit:tournament_flow': 'Turnier-Ablauf:', - 'tournament:edit:ticker_connection': 'Ticker-Verbindung:', - 'tournament:edit:btp_connection': 'BTP-Verbindung:', - 'tournament:edit:devices': 'Verbundene Geräte:', - 'tournament:edit:calls': 'Aufrufe:', - 'tournament:edit:location': 'Spielort:', - 'tournament:edit': 'Einstellungen Verwalten:', + 'tournament:edit:tournament': 'Turnier', + 'tournament:edit:tournament_flow': 'Turnier-Ablauf', + 'tournament:edit:ticker_connection': 'Ticker-Verbindung', + 'tournament:edit:btp_connection': 'BTP-Verbindung', + 'tournament:edit:devices': 'Verbundene Geräte', + 'tournament:edit:calls': 'Aufrufe', + 'tournament:edit:location': 'Austragungsort', + 'tournament:edit': 'Einstellungen Verwalten', 'tournament:edit:save': 'Speichern', 'tournament:edit:save_and_back': 'Speichern und zur Turnierübersicht', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 460243a..fea1503 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -51,6 +51,8 @@ var ci18n_en = { 'experimental': '(experimental)', 'Winner': 'Winner', 'Loser': 'Loser', + 'activate_court': 'Spielfeld nutzen', + 'inactivate_court': 'Spielfeld sperren', 'announcements:begin_to_play': 'Start to play!', 'announcements:second_call': '"Second call for:"', @@ -79,6 +81,8 @@ var ci18n_en = { 'announcements:semifinal': 'Semifinal', 'announcements:final': 'Final', 'announcements:intermediate_round': 'Intermediate round', + 'announcements:round_for_places': 'Round for places', + 'announcements:to': 'to', 'announcements:game_for_place': 'Game for place ', 'announcements:voice': 'Google UK English Male', 'announcements:lang': 'en-EN', @@ -119,7 +123,18 @@ var ci18n_en = { 'display_setting:network_update_interval': 'Network repeat interval (ms): ', 'display_setting:displaymode_update_interval': 'Displaymode Update Intervall (ms): ', + 'tournament:edit:tournament': 'Tournament', + 'tournament:edit:tournament_flow': 'Tournament flow', + 'tournament:edit:ticker_connection': 'Ticker connection', + 'tournament:edit:btp_connection': 'BTP connection', + 'tournament:edit:devices': 'Connected devices', + 'tournament:edit:calls': 'Calls', + 'tournament:edit:location': 'Location', + 'tournament:edit': 'Manage settings', + 'tournament:edit:save': 'Save', + 'tournament:edit:save_and_back': 'Save and back to the tournament overview', + 'tournament:edit:tournament:type': 'Tournament type:', 'tournament:edit:id': 'Tournament id:', 'tournament:edit:language': 'Language:', 'tournament:edit:language:auto': 'Not set (browser default)', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 4d3f371..b0e9c78 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -232,6 +232,10 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ ); const activeMatch = court && match.btp_winner != undefined; const setup = match.setup; + + tr.setAttribute('data-match_id', match._id); + tr.setAttribute('data-style', style); + if (style === 'default' || style === 'plain' || style === 'unasigned') { const actions_td = uiu.el(tr, 'td', 'actions'); create_match_button(actions_td, 'vlink match_edit_button', 'match:edit', on_edit_button_click, match._id); @@ -253,11 +257,24 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ //uiu.el(tr, 'td', court ? 'court_history' : 'empty_court', court ? court.num : ''); } - if (style === 'plain' || style === 'public') { + if (style === 'plain') { const court_number_td = uiu.el(tr, "td", 'court_number'); if(!court) console.warn('no court'); - uiu.el(court_number_td, "div", 'court_num', court.num); + if(court.is_active) { + create_court_button(court_number_td, 'court_num', 'inactivate_court', on_inactivate_court_button_click, court._id, court.num); + } else { + create_court_button(court_number_td, 'court_inactive', 'activate_court', on_activate_court_button_click, court._id, ''); + } + } + + if(style === 'public') { + const court_number_td = uiu.el(tr, "td", {'class':'court_number', "data-court_id":court._id}); + if(court.is_active){ + uiu.el(court_number_td, "div", 'court_num', court.num); + } else { + uiu.el(court_number_td, "div", 'court_inactive', ""); + } } if (style === 'default' || style === 'plain' || style === 'unasigned') { @@ -470,6 +487,10 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ active_timers.matches[match._id] = preparation_timer; } } + + if(style == 'plain' && match.setup.counting == "3x21" && isMatchOver(match.network_score)) { + create_match_button(timer_td, 'vlink match_confirm_button', 'Confirm_Finish', on_match_confirm_button_click, match._id); + } } if (style === 'default' || style === 'plain' || style === 'unasigned') { @@ -547,6 +568,10 @@ function update_match_score(m) { active_timers.matches[match._id] = preparation_timer; } } + + if(m.setup.counting = "3x21" && isMatchOver(m.network_score)) { + create_match_button(timer_td, 'vlink match_confirm_button', 'Confirm_Finish', on_match_confirm_button_click, m._id); + } }); if( m.network_score && m.network_score.length > 0 && @@ -580,6 +605,66 @@ function update_match_score(m) { }); } +function on_match_confirm_button_click(e) { + const match_id = e.target.getAttribute('data-match_id'); + const match = utils.find(curt.matches, m => m._id === match_id); + if (match) { + send({ + type: 'confirm_match_finished', + match_id: match_id, + tournament_key: match.tournament_key, + court_id: match.setup.court_id + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + } +} + +function isMatchOver(sets) { + if(!sets){ + return false; + } + + let winsA = 0; + let winsB = 0; + + for (let [scoreA, scoreB] of sets) { + if (isSetOver(scoreA, scoreB)) { + if (scoreA > scoreB) { + winsA++; + } else { + winsB++; + } + + if (winsA === 2 || winsB === 2) { + return true; + } + } else { + // Satz ist noch nicht vorbei → Spiel auch nicht + return false; + } + } + + // Falls nicht abgebrochen wurde, prüfen ob überhaupt jemand 2 Sätze gewonnen hat + return winsA === 2 || winsB === 2; +} + +function isSetOver(scoreA, scoreB) { + const maxScore = 30; + const winningScore = 21; + + if (scoreA === maxScore || scoreB === maxScore) return true; + + if ((scoreA >= winningScore || scoreB >= winningScore) && Math.abs(scoreA - scoreB) >= 2) { + return true; + } + + return false; +} + + function render_players_el(parentNode, setup, team_id, match, show_player_status, style) { const team = setup.teams[team_id]; @@ -798,19 +883,13 @@ function update_player(match_id, player, now_on_court, show_player_status) { const main_container = document.getElementsByClassName('main_upcoming'); if (main_container.length > 0){ uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { - const court_number = match_row_el.getElementsByClassName('court_number')[0].children[0].innerHTML; - const c = { _id:m.setup.court_id, - num: court_number}; - + const c = utils.find(curt.courts, c => c._id === m.setup.court_id); match_row_el.innerHTML = ""; render_empty_court_row(match_row_el, c, 'public', false); }); } else { uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(old_section.slice(6, old_section.length)) + ']', (match_row_el) => { - const court_number = match_row_el.getElementsByClassName('court_number')[0].children[0].innerHTML; - const c = { _id:m.setup.court_id, - num: court_number}; - + const c = utils.find(curt.courts, c => c._id === m.setup.court_id); match_row_el.innerHTML = ""; render_empty_court_row(match_row_el, c, 'plain', true); }); @@ -861,7 +940,6 @@ function insert_new_match_row(m, section) { break; default: const court = utils.find(curt.courts, c => c._id === m.setup.court_id); - console.log(court); uiu.qsEach('.court_row[data-court_id=' + JSON.stringify(m.setup.court_id) + ']', (match_row_el) => { match_row_el.innerHTML = ""; const closest = match_row_el.closest('.main_upcoming'); @@ -944,6 +1022,10 @@ function create_timer(timer_state, parent, default_color, exigent_color) { } else { tobj.timeout = null; el.style.display = "none"; + if(!curt.btp_settings.check_in_per_match) { + parent.classList.remove("not_checked_in"); + parent.classList.add("checked_in"); + } } }; @@ -1619,31 +1701,108 @@ function render_courts(container, style) { } function render_empty_court_row(tr, court, style, is_droppable) { + tr.setAttribute("data-style", style); + + const is_active = court.is_active; + if(style != 'public') { - const lead_target_td = uiu.el(tr, 'td', {class: "droppable actions", colspan: 1, "data-court_id":court._id}, ''); + const lead_target_td = uiu.el(tr, 'td', {class: (is_active ? "droppable " : "inactive " ) + "actions", colspan: 1, "data-court_id":court._id, "data-state" : (is_active ? "droppable" : "inactive" )}, ''); - lead_target_td.addEventListener("drop", drop); - lead_target_td.addEventListener("dragover", allowDrop); - } - - let court_number_class = ('court_number'); - let empty_row_class = ('empty_element'); + if(is_active){ + lead_target_td.addEventListener("drop", drop); + lead_target_td.addEventListener("dragover", allowDrop); + } + const court_number_td = uiu.el(tr, "td", {'class':'court_number', "data-court_id":court._id, "data-state" : (is_active ? "droppable" : "inactive")}); + if(is_active) { + create_court_button(court_number_td, 'court_num', 'inactivate_court', on_inactivate_court_button_click, court._id, court.num); + } else { + create_court_button(court_number_td, 'court_inactive', 'activate_court', on_activate_court_button_click, court._id, ''); + } - const court_number_td = uiu.el(tr, "td", {'class':'court_number', "data-court_id":court._id}); - uiu.el(court_number_td, "div", 'court_num', court.num); + const target_td = uiu.el(tr, 'td', {class: 'empty_element', colspan: 11, "data-court_id":court._id}, ''); + if(is_active) { + court_number_td.classList.add('droppable'); + target_td.classList.add('droppable'); + target_td.setAttribute('data-state', 'droppable') - const target_td = uiu.el(tr, 'td', {class: 'empty_element', colspan: 11, "data-court_id":court._id}, ''); - - if( style != 'public') { - court_number_td.classList.add('droppable'); - target_td.classList.add('droppable'); + court_number_td.addEventListener("drop", drop); + court_number_td.addEventListener("dragover", allowDrop); + target_td.addEventListener("drop", drop); + target_td.addEventListener("dragover", allowDrop); + } else { + court_number_td.classList.add('inactive'); + target_td.classList.add('inactive'); + + target_td.setAttribute('data-state', 'inactive') + } + } else { + const court_number_td = uiu.el(tr, "td", {'class':'court_number', "data-court_id":court._id}); + if(is_active){ + uiu.el(court_number_td, "div", 'court_num', court.num); + } else { + uiu.el(court_number_td, "div", 'court_inactive', ""); + } - target_td.addEventListener("drop", drop); - target_td.addEventListener("dragover", allowDrop); + const target_td = uiu.el(tr, 'td', {class: 'empty_element', colspan: 11, "data-court_id":court._id}, ''); } } +function update_court(court) { + const tr = uiu.qs(`tr[data-court_id="${court._id}"]`); + const match_id = tr.getAttribute('data-match_id'); + const style = tr.getAttribute("data-style"); + tr.innerHTML = ""; + if(match_id == null) { + render_empty_court_row(tr, court, style, true); + return; + } + const m = utils.find(curt.matches, m => m._id === match_id); + render_match_row(tr, m, court, style); +} + + +function on_inactivate_court_button_click(e) { + const btn = e.target; + const court_id = btn.getAttribute('data-court_id'); + send({ + type: 'court_edit', + tournament_key: curt.key, + is_active: false, + court_id: court_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); +} + +function on_activate_court_button_click(e) { + const btn = e.target; + const court_id = btn.getAttribute('data-court_id'); + send({ + type: 'court_edit', + tournament_key: curt.key, + is_active: true, + court_id: court_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); +} + + +function create_court_button(targetEl, cssClass, title, listener, court_id, text) { + const btn = uiu.el(targetEl, 'div', { + 'class': cssClass, + 'title': ci18n(title), + 'data-court_id': court_id, + 'data-state': cssClass, + }, text); + btn.addEventListener('click', listener); +} + function allowDrop(ev) { ev.preventDefault(); @@ -2140,6 +2299,7 @@ return { render_courts, render_umpire_options, render_upcoming_matches, + update_court, update_match_score, update_match, remove_match_from_gui, diff --git a/static/js/cto_stats.js b/static/js/cto_stats.js index 2873542..90bebf0 100644 --- a/static/js/cto_stats.js +++ b/static/js/cto_stats.js @@ -22,9 +22,6 @@ function ui_to_stats() { uiu.el(main, 'h2', {}, ci18n('to_stats:header')); uiu.el(main, 'h3', {}, curt.name || curt.key); - // TODO add dates + location here - console.log(curt.matches) - const table = uiu.el(main, 'table', 'table-outlined'); const thead = uiu.el(table, 'thead'); const thead_tr = uiu.el(thead, 'tr'); diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 46459fa..3500450 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -556,6 +556,7 @@ var ctournament = (function() { type: 'tournament_upload_logo', tournament_key: curt.key, data_url: reader.result, + name: e.target.files[0].name, }, (err) => { if (err) { return cerror.net(err); @@ -913,68 +914,7 @@ var ctournament = (function() { const devices_div = uiu.el(form, 'div', 'settings'); uiu.el(devices_div, 'h2', 'edit', ci18n('tournament:edit:devices')); - uiu.el(devices_div, 'h3', 'edit', ci18n('tournament:edit:logo')); - const logo_preview_container = uiu.el(devices_div, 'div', { - style: ( - 'position:relative;text-align:center;' + - 'height: 432px; width: 768px; font-size: 70px;' + - 'background:' + (curt.logo_background_color || '#000000') + ';' + - 'color:' + (curt.logo_foreground_color || '#aaaaaa') + ';' - ), - }); - if (curt.logo_id) { - uiu.el(logo_preview_container, 'img', { - style: 'height: 320px;', - src: '/h/' + encodeURIComponent(curt.key) + '/logo/' + curt.logo_id, - }); - uiu.el(logo_preview_container, 'div', {}, 'Court 42'); - } - - const logo_form = uiu.el(devices_div, 'form'); - const logo_button = uiu.el(logo_form, 'input', { - type: 'file', - accept: 'image/*', - }); - logo_button.addEventListener('change', _upload_logo); - const logo_colors_container = uiu.el(logo_form, 'div', { style: 'display: block' }); - const bg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:background')); - const logo_background_color_input = uiu.el(bg_col_label, 'input', { - type: 'color', - name: 'logo_background_color', - value: curt.logo_background_color || '#000000', - }); - logo_background_color_input.addEventListener('change', (e) => { - send({ - type: 'tournament_edit_props', - key: curt.key, - props: { - logo_background_color: e.target.value, - }, - }, function (err) { - if (err) { - return cerror.net(err); - } - }); - }); - const fg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:foreground')); - const fg_col_input = uiu.el(fg_col_label, 'input', { - type: 'color', - name: 'logo_foreground_color', - value: curt.logo_foreground_color || '#aaaaaa', - }); - fg_col_input.addEventListener('change', (e) => { - send({ - type: 'tournament_edit_props', - key: curt.key, - props: { - logo_foreground_color: e.target.value, - }, - }, function (err) { - if (err) { - return cerror.net(err); - } - }); - }); + render_logo_preview(devices_div); const default_display_fieldset = uiu.el(devices_div, 'fieldset'); // Default display @@ -1019,7 +959,7 @@ var ctournament = (function() { // location-div################################################################################## { const location_div = uiu.el(form, 'div', 'settings'); - uiu.el(location_div, 'h2', 'edit', ci18n('tournament:edit:location')); + render_locations(location_div); render_courts(location_div); } @@ -1085,65 +1025,6 @@ var ctournament = (function() { ui_show(); }); }); - - - - /* - () => { - const props = { - name : input.name.value, - tguid: input.tguid.value, - language: input.language.value, - is_team: input.is_team.checked, - is_nation_competition: input.is_nation_competition.checked, - btp_enabled: input.btp_enabled.checked, - btp_autofetch_enabled: input.btp_autofetch_enabled.checked, - btp_readonly: input.btp_readonly.checked, - btp_ip: input.btp_ip.value, - btp_password: input.btp_password.value, - btp_timezone: input.btp_timezone.value, - btp_autofetch_timeout_intervall: input.btp_autofetch_timeout_intervall.value, - dm_style: input.dm_style.value, - displaysettings_general: input.displaysettings_general.value, - warmup: input.warmup.value, - warmup_ready: input.warmup_ready.value, - warmup_start: input.warmup_start.value, - ticker_enabled: input.ticker_enabled.checked, - ticker_url: input.ticker_url.value, - ticker_password: input.ticker_password.value, - tabletoperator_enabled: input.tabletoperator_enabled.checked, - tabletoperator_with_umpire_enabled: input.tabletoperator_with_umpire_enabled.checked, - tabletoperator_winner_of_quaterfinals_enabled: input.tabletoperator_winner_of_quaterfinals_enabled.checked, - tabletoperator_split_doubles: input.tabletoperator_split_doubles.checked, - tabletoperator_with_state_enabled: input.tabletoperator_with_state_enabled.checked, - tabletoperator_with_state_from_match_enabled: input.tabletoperator_with_state_from_match_enabled.checked, - tabletoperator_set_break_after_tabletservice: input.tabletoperator_set_break_after_tabletservice.checked, - tabletoperator_use_manual_counting_boards_enabled: input.tabletoperator_use_manual_counting_boards_enabled.checked, - tabletoperator_break_seconds: input.tabletoperator_break_seconds.value, - annoncement_include_event: input.annoncement_include_event.checked, - annoncement_include_round: input.annoncement_include_round.checked, - annoncement_include_matchnumber: input.annoncement_include_matchnumber.checkt, - announcement_speed: input.announcement_speed.value, - announcement_pause_time_ms: input.announcement_pause_time_ms.value, - preparation_meetingpoint_enabled: input.preparation_meetingpoint_enabled.checked, - preparation_tabletoperator_setup_enabled: input.preparation_tabletoperator_setup_enabled.checked, - call_preparation_matches_automatically_enabled: input.call_preparation_matches_automatically_enabled.checked, - call_next_possible_scheduled_match_in_preparation: input.call_next_possible_scheduled_match_in_preparation.checked - } - - send({ - type: 'tournament_edit_props', - key: curt.key, - props: props, - }, function (err) { - if (err) { - return cerror.net(err); - } - ui_show(); - }); - - }); - */ } } _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit, change.default_handler(_update_all_ui_elements_edit, { @@ -1219,7 +1100,27 @@ var ctournament = (function() { const tr_input = uiu.el(display_tbody, 'tr'); create_undecorated_input("text", uiu.el(tr_input, 'td', {}), 'normalizations_origin'); create_undecorated_input("text", uiu.el(tr_input, 'td', {}), 'normalizations_replace'); - create_undecorated_input("text", uiu.el(tr_input, 'td', {}), 'normalizations_language'); + + // Tournament language selection + const language_td = uiu.el(tr_input, 'td'); + const language_select = uiu.el(language_td, 'select', { + name: 'language', + required: 'required', + name: 'normalizations_language', + id: 'normalizations_language', + }); + const all_langs = ci18n.get_all_languages(); + for (const l of all_langs) { + const l_attrs = { + value: l['announcements:lang'], + }; + if (l._code === curt.language) { + l_attrs.selected = 'selected'; + } + uiu.el(language_select, 'option', l_attrs, l._name); + } + + //create_undecorated_input("text", uiu.el(tr_input, 'td', {}), 'normalizations_language'); const actions_td = uiu.el(tr_input, 'td', {}); const add_btn = uiu.el(actions_td, 'button', {}, ci18n('tournament:edit:add')); add_btn.addEventListener('click', function (e) { @@ -1363,7 +1264,120 @@ var ctournament = (function() { } } + function render_logo_preview(main) { + uiu.el(main, 'h3', 'edit', ci18n('tournament:edit:logo')); + const logo_preview_container = uiu.el(main, 'div', { + style: ( + 'position:relative;text-align:center;' + + 'height: 432px; width: 768px; font-size: 70px;' + + 'background:' + (curt.logo_background_color || '#000000') + ';' + + 'color:' + (curt.logo_foreground_color || '#aaaaaa') + ';' + ), + name: "logo_preview", + }); + if (curt.logo_id) { + uiu.el(logo_preview_container, 'img', { + style: 'height: 320px;', + src: '/h/' + encodeURIComponent(curt.key) + '/logo/' + curt.logo_id, + name: 'logo_preview_img' + }); + uiu.el(logo_preview_container, 'div', {}, 'Court 42'); + } + + const logo_form = uiu.el(main, 'form', 'logo_form'); + const logo_button_id = 'logo_upload_input'; + + const custom_label = uiu.el(logo_form, 'label', { + for: logo_button_id, + style: ( + 'display:inline-block;padding:3px 8px;cursor:pointer; border:1px solid;' + + 'background:#eeeeee;color:black;border-radius:4px;margin:10px;' + ), + }, 'Logo auswählen'); + + const filename_display = uiu.el(logo_form, 'span', { + id: 'upload_filename', + style: 'font-style: italic; color: #555;', + }, curt.logo_name ? curt.logo_name : 'Noch keine Datei ausgewählt'); + + const logo_button = uiu.el(logo_form, 'input', { + id: logo_button_id, + type: 'file', + accept: 'image/*', + style: 'display:none;', + }); + logo_button.addEventListener('change', (e) => { + _upload_logo(e); + }); + const logo_colors_container = uiu.el(logo_form, 'div', { style: 'display: block' }); + const bg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:background')); + const logo_background_color_input = uiu.el(bg_col_label, 'input', { + type: 'color', + name: 'logo_background_color', + value: curt.logo_background_color || '#000000', + }); + logo_background_color_input.addEventListener('input', (e) => { + send({ + type: 'tournament_edit_logo', + key: curt.key, + props: { + logo_background_color: e.target.value, + }, + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + }); + const fg_col_label = uiu.el(logo_colors_container, 'label', {}, ci18n('tournament:edit:logo:foreground')); + const fg_col_input = uiu.el(fg_col_label, 'input', { + type: 'color', + name: 'logo_foreground_color', + value: curt.logo_foreground_color || '#aaaaaa', + }); + fg_col_input.addEventListener('input', (e) => { + send({ + type: 'tournament_edit_logo', + key: curt.key, + props: { + logo_foreground_color: e.target.value, + }, + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + }); + } + + function update_logo() { + switch (get_admin_subpage()){ + case 'edit': + const logo_preview_container = document.querySelector('[name="logo_preview"]'); + logo_preview_container.style.background = curt.logo_background_color; + logo_preview_container.style.color = curt.logo_foreground_color; + let logo_background_color_input = document.querySelector('[name="logo_background_color"]'); + logo_background_color_input.value = curt.logo_background_color; + let fg_col_input = document.querySelector('[name="logo_foreground_color"]'); + fg_col_input.value = curt.logo_foreground_color; + const logo_preview_img = logo_preview_container.querySelector('[name="logo_preview_img"]'); + logo_preview_img.setAttribute('src', '/h/' + encodeURIComponent(curt.key) + '/logo/' + curt.logo_id); + console.log(logo_preview_img); + const filename_display = document.querySelector('#upload_filename'); + filename_display.textContent = curt.logo_name ? curt.logo_name : 'Noch keine Datei ausgewählt'; + break; + default: + break; + } + return; + } + function render_general_displaysettings(main) { + let used_configs = new Set(); + curt.displays.forEach((d) => { + used_configs.add(d.displaysetting_id); + }); + uiu.el(main, 'h3', 'edit', ci18n('tournament:edit:general_displaysettings')); const display_settings_table = uiu.el(main, 'table'); const display_settings_tbody = uiu.el(display_settings_table, 'tbody'); @@ -1373,41 +1387,46 @@ var ctournament = (function() { uiu.el(tr, 'th', {}, ""); for (const s of curt.displaysettings) { - const tr = uiu.el(display_settings_tbody, 'tr'); - uiu.el(tr, 'th', {}, s.description ||s.id); - const description_td = uiu.el(tr, 'td', {}, s.devicemode + (s.devicemode == 'display' ? ' (' + s.displaymode_style + ')' : '')); - const actions_td = uiu.el(tr, 'td', {}); - const edit_btn = uiu.el(actions_td, 'button', { - 'data-display_setting_id': s.id, - }, 'Edit'); + const tr = uiu.el(display_settings_tbody, 'tr', { 'data-displaysetting_id': s.id }); + render_general_displaysetting_line(tr, s, used_configs); + } + } - edit_btn.addEventListener('click', (e) => { - on_edit_display_setting_button_click(e); - }); + function render_general_displaysetting_line(parrent, s, used_configs) { + uiu.el(parrent, 'th', {}, s.description ||s.id); + const description_td = uiu.el(parrent, 'td', {}, s.devicemode + (s.devicemode == 'display' ? ' (' + s.displaymode_style + ')' : '')); + const actions_td = uiu.el(parrent, 'td', {}); + const edit_btn = uiu.el(actions_td, 'button', { + 'data-display_setting_id': s.id, + }, 'Edit'); + edit_btn.addEventListener('click', (e) => { + on_edit_display_setting_button_click(e); + }); - const delete_btn = uiu.el(actions_td, 'button', { - 'data-display-setting-id': s.id, - }, 'Delete'); + const delete_btn = uiu.el(actions_td, 'button', { + 'data-display-setting-id': s.id, + }, 'Delete'); + if (used_configs.has(s.id)) { + delete_btn.setAttribute('disabled', 'disabled'); + } - delete_btn.addEventListener('click', (e) => { - const del_btn = e.target; - const setting_id = del_btn.getAttribute('data-display-setting-id'); + delete_btn.addEventListener('click', (e) => { + const del_btn = e.target; + const setting_id = del_btn.getAttribute('data-display-setting-id'); - send({ - type: 'delete_display_setting', - tournament_key: curt.key, - setting_id: setting_id, - }, err => { - if (err) { - return cerror.net(err); - } - }); + send({ + type: 'delete_display_setting', + tournament_key: curt.key, + setting_id: setting_id, + }, err => { + if (err) { + return cerror.net(err); + } }); - - } + }); } function _cancel_ui_edit_display_setting() { @@ -1418,21 +1437,17 @@ var ctournament = (function() { cbts_utils.esc_stack_pop(); uiu.remove(dlg); - crouting.set('t/:key/edit/', { key: curt.key }); + ui_edit(); } function on_edit_display_setting_button_click(e) { const btn = e.target; const display_setting_id = btn.getAttribute('data-display_setting_id'); - //console.log(display_setting_id); ui_edit_display_setting(display_setting_id); } function ui_edit_display_setting(display_setting_id) { - //console.log(display_setting_id); - //console.log(curt); const display_setting = structuredClone(utils.find(curt.displaysettings, d => d.id === display_setting_id)); - //console.log(display_setting); crouting.set('t/' + curt.key + '/edit/s/' + display_setting_id, {}, _cancel_ui_edit_display_setting); @@ -1464,9 +1479,7 @@ var ctournament = (function() { }, ci18n('Change')); form_utils.onsubmit(form, function(d) { - //console.log(d); const displaysetting = create_displaysettings_object(d); - //console.log(displaysetting); send({ type: 'edit_display_setting', @@ -1476,7 +1489,6 @@ var ctournament = (function() { if (err) { return cerror.net(err); } - console.log("call _cancel_ui_edit_display_setting()"); _cancel_ui_edit_display_setting(); }); }); @@ -1525,40 +1537,51 @@ var ctournament = (function() { 'umpire', 'display' ]; - render_drop_down(edit_display_setting_container, ci18n('display_setting:devicemode'), 'devicemode', ALL_DEVICE_MODES, display_setting.devicemode || ''); - render_drop_down(edit_display_setting_container, ci18n('display_setting:style'), 'displaymode_style', displaymode.ALL_STYLES, display_setting.displaymode_style || ''); - render_check_box(edit_display_setting_container, ci18n('display_setting:show_pause'), 'd_show_pause', display_setting.d_show_pause); - render_check_box(edit_display_setting_container, ci18n('display_setting:show_court_number'), 'd_show_court_number', display_setting.d_show_court_number); - render_check_box(edit_display_setting_container, ci18n('display_setting:show_competition'), 'd_show_competition', display_setting.d_show_competition); - render_check_box(edit_display_setting_container, ci18n('display_setting:show_round'), 'd_show_round', display_setting.d_show_round); - render_check_box(edit_display_setting_container, ci18n('display_setting:show_middle_name'), 'd_show_middle_name', display_setting.d_show_middle_name); - render_check_box(edit_display_setting_container, ci18n('display_setting:show_doubles_receiving'), 'd_show_doubles_receiving', display_setting.d_show_doubles_receiving); + + + const calculated_style = (display_setting.devicemode === 'umpire' ? 'umpire' : display_setting.displaymode_style); + + + render_drop_down(edit_display_setting_container, ci18n('display_setting:devicemode'), 'devicemode', true, ALL_DEVICE_MODES, display_setting.devicemode || ''); + const displaystyle_select = render_drop_down(edit_display_setting_container, ci18n('display_setting:style'), 'displaymode_style', (display_setting.devicemode === 'umpire' ? 'umpire' : true), displaymode.ALL_STYLES, display_setting.displaymode_style || ''); + + displaystyle_select.addEventListener('change', (e) => { + const style = e.target; + update_edit_display_setting(style.value); + }); + + render_check_box(edit_display_setting_container, ci18n('display_setting:show_pause'), 'show_pause', calculated_style, display_setting.d_show_pause); + render_check_box(edit_display_setting_container, ci18n('display_setting:show_court_number'), 'show_court_number', calculated_style, display_setting.d_show_court_number); + render_check_box(edit_display_setting_container, ci18n('display_setting:show_competition'), 'show_competition', calculated_style, display_setting.d_show_competition); + render_check_box(edit_display_setting_container, ci18n('display_setting:show_round'), 'show_round', calculated_style, display_setting.d_show_round); + render_check_box(edit_display_setting_container, ci18n('display_setting:show_middle_name'), 'show_middle_name', calculated_style, display_setting.d_show_middle_name); + render_check_box(edit_display_setting_container, ci18n('display_setting:show_doubles_receiving'), 'show_doubles_receiving', calculated_style, display_setting.d_show_doubles_receiving); const select_color_div = uiu.el(edit_display_setting_container, 'div', { style: 'display: block' }); const select_color_label = uiu.el(select_color_div, 'label', {}, ci18n('display_setting:colors')); - render_select_color(select_color_label, 'd_c0', display_setting.d_c0); - render_select_color(select_color_label, 'd_c1', display_setting.d_c1); - render_select_color(select_color_label, 'd_cb0', display_setting.d_cb0); - render_select_color(select_color_label, 'd_cb1', display_setting.d_cb1); - render_select_color(select_color_label, 'd_cbg', display_setting.d_cbg); - render_select_color(select_color_label, 'd_cbg2', display_setting.d_cbg2); - render_select_color(select_color_label, 'd_cbg3', display_setting.d_cbg3); - render_select_color(select_color_label, 'd_cbg4', display_setting.d_cbg4); - render_select_color(select_color_label, 'd_cfg', display_setting.d_cfg); - render_select_color(select_color_label, 'd_cfg2', display_setting.d_cfg2); - render_select_color(select_color_label, 'd_cfg3', display_setting.d_cfg3); - render_select_color(select_color_label, 'd_cfg4', display_setting.d_cfg4); - render_select_color(select_color_label, 'd_cfgdark', display_setting.d_cfgdark); - render_select_color(select_color_label, 'd_cexpt', display_setting.d_cexpt); - render_select_color(select_color_label, 'd_ct', display_setting.d_ct); - render_select_color(select_color_label, 'd_cborder', display_setting.d_cborder); - render_select_color(select_color_label, 'd_cserv', display_setting.d_cserv); - render_select_color(select_color_label, 'd_cserv2', display_setting.d_cserv2); - render_select_color(select_color_label, 'd_crecv', display_setting.d_crecv); - render_select_color(select_color_label, 'd_ctim_blue', display_setting.d_ctim_blue); - render_select_color(select_color_label, 'd_ctim_active', display_setting.d_ctim_active); - render_check_box(edit_display_setting_container, ci18n('display_setting:use_team_colors'), 'd_team_colors', display_setting.d_team_colors); - render_select_number(edit_display_setting_container, ci18n('display_setting:scale'), 'd_scale', display_setting.d_scale, 20, 500); + render_select_color(select_color_label, 'c0', calculated_style, display_setting.d_c0); + render_select_color(select_color_label, 'c1', calculated_style, display_setting.d_c1); + render_select_color(select_color_label, 'cb0', calculated_style, display_setting.d_cb0); + render_select_color(select_color_label, 'cb1', calculated_style, display_setting.d_cb1); + render_select_color(select_color_label, 'cbg', calculated_style, display_setting.d_cbg); + render_select_color(select_color_label, 'cbg2', calculated_style, display_setting.d_cbg2); + render_select_color(select_color_label, 'cbg3', calculated_style, display_setting.d_cbg3); + render_select_color(select_color_label, 'cbg4', calculated_style, display_setting.d_cbg4); + render_select_color(select_color_label, 'cfg', calculated_style, display_setting.d_cfg); + render_select_color(select_color_label, 'cfg2', calculated_style, display_setting.d_cfg2); + render_select_color(select_color_label, 'cfg3', calculated_style, display_setting.d_cfg3); + render_select_color(select_color_label, 'cfg4', calculated_style, display_setting.d_cfg4); + render_select_color(select_color_label, 'cfgdark', calculated_style, display_setting.d_cfgdark); + render_select_color(select_color_label, 'cexp', calculated_style, display_setting.d_cexp); + render_select_color(select_color_label, 'ct', calculated_style, display_setting.d_ct); + render_select_color(select_color_label, 'cborder', calculated_style, display_setting.d_cborder); + render_select_color(select_color_label, 'cserv', calculated_style, display_setting.d_cserv); + render_select_color(select_color_label, 'cserv2', calculated_style, display_setting.d_cserv2); + render_select_color(select_color_label, 'crecv', calculated_style, display_setting.d_crecv); + render_select_color(select_color_label, 'ctim_blue', calculated_style, display_setting.d_ctim_blue); + render_select_color(select_color_label, 'ctim_active', calculated_style, display_setting.d_ctim_active); + render_check_box(edit_display_setting_container, ci18n('display_setting:use_team_colors'), 'team_colors', calculated_style, display_setting.d_team_colors); + render_select_number(edit_display_setting_container, ci18n('display_setting:scale'), 'scale', calculated_style, display_setting.d_scale, 20, 500); const ALL_BUP_LANGUAGES = [ ci18n('display_setting:language_automatic'), @@ -1589,7 +1612,7 @@ var ctournament = (function() { // } // } - render_drop_down(edit_display_setting_container, ci18n('display_setting:language'), 'language', SHORT_BUP_LANGUAGES, display_setting.language, ALL_BUP_LANGUAGES); + render_drop_down(edit_display_setting_container, ci18n('display_setting:language'), 'language', true, SHORT_BUP_LANGUAGES, display_setting.language, ALL_BUP_LANGUAGES); const ALL_ASK_FULLSCREAN_MODES = [ @@ -1597,7 +1620,7 @@ var ctournament = (function() { 'auto', 'never', ]; - render_drop_down(edit_display_setting_container, ci18n('display_setting:fullscreen_ask'), 'fullscreen_ask', ALL_ASK_FULLSCREAN_MODES, display_setting.fullscreen_ask || ''); + render_drop_down(edit_display_setting_container, ci18n('display_setting:fullscreen_ask'), 'fullscreen_ask', true, ALL_ASK_FULLSCREAN_MODES, display_setting.fullscreen_ask || ''); const ALL_ANNOUNCEMENT_MODES = [ @@ -1605,14 +1628,13 @@ var ctournament = (function() { 'all', 'except-first', ]; - render_drop_down(edit_display_setting_container, ci18n('display_setting:show_announcements'), 'show_announcements', ALL_ANNOUNCEMENT_MODES, display_setting.show_announcements || ''); + render_drop_down(edit_display_setting_container, ci18n('display_setting:show_announcements'), 'show_announcements', calculated_style, ALL_ANNOUNCEMENT_MODES, display_setting.show_announcements || ''); - render_select_number(edit_display_setting_container, ci18n('display_setting:scale'), 'd_scale', display_setting.d_scale, 20, 500); - render_select_number(edit_display_setting_container, ci18n('display_setting:button_block_timeout'), 'button_block_timeout', display_setting.button_block_timeout, 0, 5000); + render_select_number(edit_display_setting_container, ci18n('display_setting:button_block_timeout'), 'button_block_timeout', calculated_style, display_setting.button_block_timeout, 0, 5000); - render_check_box(edit_display_setting_container, ci18n('display_setting:negative_timers'), 'negative_timers', display_setting.negative_timers); - render_check_box(edit_display_setting_container, ci18n('display_setting:shuttle_counter'), 'shuttle_counter', display_setting.shuttle_counter); - render_check_box(edit_display_setting_container, ci18n('display_setting:editmode_doubleclick'), 'editmode_doubleclick', display_setting.editmode_doubleclick); + render_check_box(edit_display_setting_container, ci18n('display_setting:negative_timers'), 'negative_timers', calculated_style, display_setting.negative_timers); + render_check_box(edit_display_setting_container, ci18n('display_setting:shuttle_counter'), 'shuttle_counter', calculated_style, display_setting.shuttle_counter); + render_check_box(edit_display_setting_container, ci18n('display_setting:editmode_doubleclick'), 'editmode_doubleclick', calculated_style, display_setting.editmode_doubleclick); const ALL_CLICK_MODES = [ 'auto', @@ -1620,7 +1642,7 @@ var ctournament = (function() { 'touchstart', 'touchend', ]; - render_drop_down(edit_display_setting_container, ci18n('display_setting:click_mode'), 'click_mode', ALL_CLICK_MODES, display_setting.click_mode || ''); + render_drop_down(edit_display_setting_container, ci18n('display_setting:click_mode'), 'click_mode', calculated_style, ALL_CLICK_MODES, display_setting.click_mode || ''); const ALL_STYLE_MODES = [ 'default', @@ -1629,17 +1651,17 @@ var ctournament = (function() { 'focus', 'hidden', ]; - render_drop_down(edit_display_setting_container, ci18n('display_setting:settings_style'), 'style', ALL_STYLE_MODES, display_setting.settings_style || ''); - render_select_number(edit_display_setting_container, ci18n('display_setting:network_timeout'), 'network_timeout', display_setting.network_timeout, 1, 600000); - render_select_number(edit_display_setting_container, ci18n('display_setting:network_update_interval'), 'network_update_interval', display_setting.network_update_interval, 1, 600000); + render_drop_down(edit_display_setting_container, ci18n('display_setting:settings_style'), 'style', calculated_style, ALL_STYLE_MODES, display_setting.settings_style || ''); + render_select_number(edit_display_setting_container, ci18n('display_setting:network_timeout'), 'network_timeout', true, display_setting.network_timeout, 1, 600000); + render_select_number(edit_display_setting_container, ci18n('display_setting:network_update_interval'), 'network_update_interval', true, display_setting.network_update_interval, 1, 600000); } - function render_drop_down(container, label_text, select_name, values, curval, labels) { + function render_drop_down(container, label_text, select_name, displaystyle, values, curval, labels) { if(!labels) { labels = values; } - const div = uiu.el(container, 'div'); + const div = uiu.el(container, 'div', {field_name: select_name}); uiu.el(div, 'span', 'label', label_text); const select = uiu.el(div, 'select', { name: select_name, @@ -1656,10 +1678,14 @@ var ctournament = (function() { } uiu.el(select, 'option', attrs, s); } + + uiu.visible(div, (displaystyle === true || displaymode.option_applies(displaystyle, select_name))); + + return select; } - function render_check_box(container, label_text, checkbox_name, is_checked) { - const div = uiu.el(container, 'div'); + function render_check_box(container, label_text, checkbox_name, displaystyle, is_checked) { + const div = uiu.el(container, 'div', {field_name: checkbox_name}); const label = uiu.el(div, 'label'); const attrs = { type: 'checkbox', @@ -1672,26 +1698,34 @@ var ctournament = (function() { uiu.el(label, 'input', attrs); uiu.el(label, 'span', 'display_setting_label', label_text); + + uiu.visible(div, (displaystyle === true || displaymode.option_applies(displaystyle, checkbox_name))); } - function render_select_color(container, field_name, value) { + function render_select_color(container, field_name, displaystyle, value) { const input = uiu.el(container, 'input', { type: 'color', name: field_name, + title: field_name, + field_name: field_name, value: value || '#000000', }); + + uiu.visible(input, (displaystyle === true ||displaymode.option_applies(displaystyle, field_name))); } - function render_select_number(container, label_text, input_name, value, min_value, max_value) { - const div = uiu.el(container, 'div'); + function render_select_number(container, label_text, input_name, displaystyle, value, min_value, max_value) { + const div = uiu.el(container, 'div', {field_name: input_name}); const label = uiu.el(div, 'span', 'label', label_text); - uiu.el(label, 'input', { + uiu.el(div, 'input', { type: 'number', name: input_name, min: min_value || 0, max: max_value || 0, value: value || 0, }); + + uiu.visible(div, (displaystyle === true ||displaymode.option_applies(displaystyle, input_name))); } function create_displaysettings_object(d) { @@ -1700,35 +1734,35 @@ var ctournament = (function() { description: d.display_setting_description || '', devicemode: d.devicemode || 'display', displaymode_style: d.displaymode_style || 'tournamentcourt', - d_show_pause: d.d_show_pause == 'on' ? true : false, - d_show_court_number: d.d_show_court_number == 'on' ? true : false, - d_show_competition: d.d_show_competition == 'on' ? true : false, - d_show_round: d.d_show_round == 'on' ? true : false, - d_show_middle_name: d.d_show_middle_name == 'on' ? true : false, - d_show_doubles_receiving: d.d_show_doubles_receiving == 'on' ? true : false, - d_c0: d.d_c0 || '#50e87d', - d_c1: d.d_c1 || '#f76a23', - d_cb0: d.d_cb0 || '#000000', - d_cb1: d.d_cb1 || '#000000', - d_cbg: d.d_cbg || '#000000', - d_cbg2: d.d_cbg2 || '#d9d9d9', - d_cbg3: d.d_cbg3 || '#252525', - d_cbg4: d.d_cbg4 || '#404040', - d_cfg: d.d_cfg || '#ffffff', - d_cfg2: d.d_cfg2 || '#aaaaaa', - d_cfg3: d.d_cfg3 || '#cccccc', - d_cfg4: d.d_cfg4 || '#000000', - d_cfgdark: d.d_cfgdark || '#000000', - d_cexpt: d.d_cexpt || '#000000', - d_ct: d.d_ct || '#80ff00', - d_cborder: d.d_cborder || '#444444', - d_cserv: d.d_cserv || '#fff200', - d_cserv2: d.d_cserv2 || '#dba766', - d_crecv: d.d_crecv || '#707676', - d_ctim_blue: d.d_ctim_blue || '#0070c0', - d_ctim_active: d.d_ctim_active || '#ffc000', - d_team_colors: d.d_team_colors == 'on' ? true : false, - d_scale: d.d_scale || '100', + d_show_pause: d.show_pause == 'on' ? true : false, + d_show_court_number: d.show_court_number == 'on' ? true : false, + d_show_competition: d.show_competition == 'on' ? true : false, + d_show_round: d.show_round == 'on' ? true : false, + d_show_middle_name: d.show_middle_name == 'on' ? true : false, + d_show_doubles_receiving: d.show_doubles_receiving == 'on' ? true : false, + d_c0: d.c0 || '#50e87d', + d_c1: d.c1 || '#f76a23', + d_cb0: d.cb0 || '#000000', + d_cb1: d.cb1 || '#000000', + d_cbg: d.cbg || '#000000', + d_cbg2: d.cbg2 || '#d9d9d9', + d_cbg3: d.cbg3 || '#252525', + d_cbg4: d.cbg4 || '#404040', + d_cfg: d.cfg || '#ffffff', + d_cfg2: d.cfg2 || '#aaaaaa', + d_cfg3: d.cfg3 || '#cccccc', + d_cfg4: d.cfg4 || '#000000', + d_cfgdark: d.cfgdark || '#000000', + d_cexp: d.cexp || '#000000', + d_ct: d.ct || '#80ff00', + d_cborder: d.cborder || '#444444', + d_cserv: d.cserv || '#fff200', + d_cserv2: d.cserv2 || '#dba766', + d_crecv: d.crecv || '#707676', + d_ctim_blue: d.ctim_blue || '#0070c0', + d_ctim_active: d.ctim_active || '#ffc000', + d_team_colors: d.team_colors == 'on' ? true : false, + d_scale: d.scale || '100', fullscreen_ask: d.fullscreen_ask || 'auto', show_announcements: d.show_announcements || 'all', button_block_timeout: d.button_block_timeout || '100', @@ -1747,13 +1781,26 @@ var ctournament = (function() { return displaysetting; } - function update_general_displaysettings(c) + function update_edit_display_setting(displaystyle) { + const names = [ 'show_pause', 'show_court_number', 'show_competition', 'show_round', 'show_middle_name', 'show_doubles_receiving', + 'c0', 'c1', 'cb0', 'cb1', 'cbg', 'cbg2', 'cbg3', 'cbg4', 'cfg', 'cfg2', 'cfg3', 'cfg4', 'cfgdark', 'cexp', 'ct', + 'cborder', 'cserv', 'cserv2', 'crecv', 'ctim_blue', 'ctim_active', 'team_colors', 'scale', + 'show_announcements', 'button_block_timeout', 'negative_timers', 'shuttle_counter', 'editmode_doubleclick', + 'click_mode', 'style', 'language']; + + names.forEach((field_name) => { + const update = uiu.qs('[field_name='+field_name+']'); + uiu.visible(update, (displaystyle === true || displaymode.option_applies(displaystyle, field_name))); + }); + } + + function update_general_displaysettings(c) + { //const general_displaysettings_div = uiu.qs('.general_displaysettings'); const general_displaysettings_div = document.querySelector(".general_displaysettings"); if(general_displaysettings_div) { general_displaysettings_div.innerHTML = ''; - console.log(general_displaysettings_div); render_general_displaysettings(general_displaysettings_div); } } @@ -1872,16 +1919,242 @@ var ctournament = (function() { }); } + function render_locations(main) { + const location_div = uiu.el(main, 'div', 'locations_div'); + uiu.el(location_div, 'h2', 'edit', ci18n('tournament:edit:location')); + + const locations_table = uiu.el(location_div, 'table', 'locations_table'); + const locations_tbody = uiu.el(locations_table, 'tbody'); + + const tr = uiu.el(locations_tbody, 'tr'); + uiu.el(tr, 'th', {}, ci18n('tournament:edit:location')); + uiu.el(tr, 'th', {}, 'In Vorbereitungd Ergänzung'); + uiu.el(tr, 'th', {}, 'Meetingpoint durchsage'); + uiu.el(tr, 'th', {}, 'In Vorbereitung Icon'); + uiu.el(tr, 'th', {}, ''); + + let highlight_in_use = []; + for (const l of curt.locations) { + if(l.highlight) { + highlight_in_use.push(l.highlight); + } + } + + + for (const l of curt.locations) { + const tr = uiu.el(locations_tbody, 'tr'); + const name_th = uiu.el(tr, 'th', {}); + uiu.el(name_th, 'div', {}, l.name); + + console.log(highlight_in_use); + const content = ['highlight_1', 'highlight_2', 'highlight_3', 'highlight_4', 'highlight_5', 'highlight_6']; + const selected = l.highlight; + const select_color = uiu.el(name_th, 'select', {id: 'select_highlight', class: selected, 'data-location_id': l._id}); + for (const item of content) { + //if(!highlight_in_use.includes(item) || selected === item) { + const attrs = { + 'data-display-setting-id': item, + value: item, + class: item, + style: (highlight_in_use.includes(item) && selected !== item) ? 'display:none;' : '', + } + if ((selected === item)) { + attrs.selected = 'selected'; + } + uiu.el(select_color, 'option', attrs); + //} + } + + select_color.addEventListener('change', (e) => { + e.target.classList = [e.target.value]; + console.log(e.target.getAttribute('data-location-id')); + send_location_to_admin(e.target.parentNode.parentNode, e.target.getAttribute('data-location_id')); + }); + + const preperation_td = uiu.el(tr, 'td', {}); + const preperation_input = create_textarea_input("textarea", preperation_td, 'preperation_addition'); + preperation_input.value = l.preperation_addition; + preperation_input.setAttribute('data-location-id', l._id); + preperation_input.setAttribute('maxlength', 175); + preperation_input.addEventListener('focusout', (e) => { + send_location_to_admin(e.target.parentNode.parentNode, e.target.getAttribute('data-location-id')); + }); + const meetinpoint_td = uiu.el(tr, 'td', {}); + const meetingpoint_input = create_textarea_input("textarea", meetinpoint_td, 'meetingpoint_announcement'); + meetingpoint_input.value = l.meetingpoint_announcement; + meetingpoint_input.setAttribute('data-location-id', l._id); + meetingpoint_input.setAttribute('maxlength', 175); + meetingpoint_input.addEventListener('focusout', (e) => { + send_location_to_admin(e.target.parentNode.parentNode, e.target.getAttribute('data-location-id')); + }); + const icon_td = uiu.el(tr, 'td', 'icon_td'); + uiu.el(icon_td, 'img', { + style: l.logo_id != null ? 'height: 40px;' : 'display: none;', + src: '/h/' + encodeURIComponent(curt.key) + '/logo/' + (l.logo_id ? l.logo_id : ''), + name: 'location_logo_img', + 'data-location_id': l._id + }); + + const logo_form = uiu.el(icon_td, 'form', 'logo_form'); + const logo_button_id = l._id +'_logo_upload_input'; + + const filename_display = uiu.el(logo_form, 'div', { + class: 'upload_filename_location', + 'data-location_id': l._id, + }, l.logo_name ? l.logo_name : 'Noch keine Datei ausgewählt'); + + const custom_label = uiu.el(logo_form, 'label', { + for: logo_button_id, + style: ( + 'display:inline-block;padding:3px 8px;cursor:pointer; border:1px solid;' + + 'background:#eeeeee;color:black;border-radius:4px;margin:5px;font-size:small;' + ), + }, 'ändern'); + + const logo_button = uiu.el(logo_form, 'input', { + id: logo_button_id, + type: 'file', + accept: 'image/*', + style: 'display:none;', + 'data-location_id': l._id, + }); + logo_button.addEventListener('change', (e) => { + _upload_location_logo(e); + }); + + const actions_td = uiu.el(tr, 'td', {}); + const del_btn = uiu.el(actions_td, 'button', { + 'data-location-id': l._id, + }, 'Delete'); + del_btn.addEventListener('click', function (e) { + const del_btn = e.target; + const location_id = del_btn.getAttribute('data-location-id'); + if (confirm('Do you really want to delete ' + location_id + '? (Will not do anything yet!)')) { + debug.log('TODO: would now delete court'); + } + }); + } + } + + function _upload_location_logo(e) { + const input = e.target; + const location_id = e.target.getAttribute('data-location_id'); + if (!input.files.length) return; + + const reader = new FileReader(); + reader.readAsDataURL(input.files[0]); + reader.onload = () => { + send({ + type: 'tournament_upload_location_logo', + tournament_key: curt.key, + data_url: reader.result, + name: e.target.files[0].name, + location_id + }, (err) => { + if (err) { + return cerror.net(err); + }` + input.closest('form').reset();` + }); + }; + reader.onerror = (e) => { + alert('Failed to upload: ' + e); + }; + } + + function update_location_logo(location_id, logo_id, logo_name) { + switch (get_admin_subpage()){ + case 'edit': + const location_logo_img = document.querySelector(`[name="location_logo_img"][data-location_id="${location_id}"]`); + location_logo_img.setAttribute('src', '/h/' + encodeURIComponent(curt.key) + '/logo/' + logo_id); + location_logo_img.style = 'height: 40px;'; + const filename_display = document.querySelector(`.upload_filename_location[data-location_id="${location_id}"]`); + filename_display.textContent = logo_name; + break; + default: + break; + } + return; + } + + function send_location_to_admin(parent, location_id) { + const highlight = parent.querySelector("#select_highlight").value; + const preperation_addition = parent.querySelector("#preperation_addition").value; + const meetingpoint_announcement = parent.querySelector("#meetingpoint_announcement").value; + + send({ + type: 'location_changed', + tournament_key: curt.key, + location_id, + highlight, + preperation_addition, + meetingpoint_announcement, + }, function (err, response) { + if (err) { + return cerror.net(err); + } + }); + } + + function update_location(location_id, highlight, preperation_addition, meetingpoint_announcement) { + switch (get_admin_subpage()){ + case 'edit': + const locations_table = document.querySelector('.locations_table'); + const location_div = locations_table.parentElement; + location_div.innerHTML=""; + render_locations(location_div); + + break; + default: + break; + } + return; + }; function render_courts(main) { uiu.el(main, 'h2', 'edit', ci18n('tournament:edit:courts')); - const courts_table = uiu.el(main, 'table'); + const courts_table = uiu.el(main, 'table', 'courts_table'); const courts_tbody = uiu.el(courts_table, 'tbody'); + const tr = uiu.el(courts_tbody, 'tr'); + uiu.el(tr, 'th', {}, 'Spielort'); + uiu.el(tr, 'th', {}, 'Nummer'); + //uiu.el(tr, 'th', {}, 'Name'); + uiu.el(tr, 'th', {}, 'Aktiv'); + uiu.el(tr, 'th', {}, 'Schiedsrichter'); + uiu.el(tr, 'th', {}, 'Aufschlagrichter'); + uiu.el(tr, 'th', {}, ''); + + var l = {_id : ''}; + for (const c of curt.courts) { const tr = uiu.el(courts_tbody, 'tr'); + if(l._id != c.location_id) { + l = utils.find(curt.locations, l => l._id === c.location_id); + } + + uiu.el(tr, 'th', {}, l.name); uiu.el(tr, 'th', {}, c.num); - uiu.el(tr, 'td', {}, c.name || ''); + //uiu.el(tr, 'td', {}, c.name || ''); + const active_td = uiu.el(tr, 'td', {}); + const active_cb = create_simple_checkbox(active_td, {'name' : 'active_cb', 'data-court-id': c._id,}, c.is_active); + active_cb.addEventListener('change', (e) => { + const court_id = e.target.getAttribute('data-court-id'); + send({ + type: 'court_edit', + tournament_key: curt.key, + is_active: e.target.checked, + court_id: court_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); + const umpire_td = uiu.el(tr, 'td', {}); + const umpire_cb = create_simple_checkbox(umpire_td, {'name' : 'umpire_cb', 'data-court-id': c._id, 'disabled': true,}, true); + const service_judge_td = uiu.el(tr, 'td', {}); + const service_judge_cb = create_simple_checkbox(service_judge_td, {'name' : 'service_judge_cb', 'data-court-id': c._id, 'disabled': true,}, true); const actions_td = uiu.el(tr, 'td', {}); const del_btn = uiu.el(actions_td, 'button', { 'data-court-id': c._id, @@ -1897,39 +2170,28 @@ var ctournament = (function() { const nums = curt.courts.map(c => parseInt(c.num)); const maxnum = Math.max(0, Math.max.apply(null, nums)); + } - const courts_add_form = uiu.el(main, 'form'); - uiu.el(courts_add_form, 'input', { - type: 'number', - name: 'count', - min: 1, - max: 99, - value: 1, - }); - const courts_add_button = uiu.el(courts_add_form, 'button', { - role: 'button', - }, 'Add Courts'); - form_utils.onsubmit(courts_add_form, function (data) { - courts_add_button.setAttribute('disabled', 'disabled'); - const court_count = parseInt(data.count); - const nums = []; - for (let court_num = maxnum + 1; court_num <= maxnum + court_count; court_num++) { - nums.push(court_num); - } + function create_simple_checkbox(parant_el, attrs, is_checked) { + attrs.type = 'checkbox'; + if(is_checked){ + attrs.checked = 'checked'; + } + const result = uiu.el(parant_el, 'input', attrs); + return result; + } - send({ - type: 'courts_add', - tournament_key: curt.key, - nums, - }, function (err, response) { - if (err) { - courts_add_button.removeAttribute('disabled'); - return cerror.net(err); - } - Array.prototype.push.apply(curt.courts, response.added_courts); - ui_edit(); - }); - }); + function update_court(court) { + switch (get_admin_subpage()){ + case 'edit': + const courts_table = uiu.qs('.courts_table'); + const checkbox = courts_table.querySelector(`[name="active_cb"][data-court-id="${court._id}"]`); + checkbox.checked = court.is_active; + break; + default: + cmatch.update_court(court); + break; + } } function create_checkbox(curt, parent_el, filed_id) { @@ -1958,12 +2220,25 @@ var ctournament = (function() { } function create_undecorated_input(type, parent_el, filed_id) { - uiu.el(parent_el, 'input', { - type: type, - name: filed_id, - id: filed_id, - value: '', - }); + return ( + uiu.el(parent_el, 'input', { + type: type, + name: filed_id, + id: filed_id, + value: '', + }) + ); + } + + function create_textarea_input(type, parent_el, filed_id) { + return ( + uiu.el(parent_el, 'textarea', { + type: type, + name: filed_id, + id: filed_id, + value: '', + }) + ); } function create_numeric_input(curt, parent_el, filed_id, min_value, max_value, default_value, step_value) { @@ -2224,6 +2499,25 @@ var ctournament = (function() { }, '/bupdev/'); } + function get_admin_subpage() { + const path = window.location.pathname; + const parts = path.split('/').filter(Boolean); // Entfernt leere Einträge (z. B. durch führendes '/') + + // Erwartet: ['admin', 't', 'TurnierName', 'subpage?'] + if (parts.length < 3 || parts[0] !== 'admin' || parts[1] !== 't') { + return null; // Nicht im erwarteten Admin-Pfad + } + + const subpage = parts[3]; // Kann undefined sein + + switch (subpage) { + case undefined: + return 'tournament-control'; + default: + return subpage; + } + } + function ui_allscoresheets() { crouting.set('t/' + curt.key + '/allscoresheets', {}, _cancel_ui_allscoresheets); @@ -2312,7 +2606,11 @@ var ctournament = (function() { add_match, update_match, update_upcoming_match, + update_logo, update_display, + update_location, + update_location_logo, + update_court, btp_status_changed, ticker_status_changed, bts_status_changed, @@ -2353,7 +2651,6 @@ if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var utils = require('../bup/bup/js/utils.js'); var save_file = require('../bup/bup/js/save_file.js'); var timezones = require('./timezones.js'); - var displaymode = require('../bup/js/displaymode'); var JSZip = null; // External library From 56299c5dbc6722b91ee459f06c268a25d6c3fb9d Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 2 May 2025 17:04:14 +0200 Subject: [PATCH 196/224] current version with multi location support --- bts/admin.js | 55 +++++++++++++++++++++++++++++++++----- bts/btp_sync.js | 37 +++++++++++-------------- bts/match_utils.js | 36 +++++++++++++++++++------ bts/utils.js | 1 + static/css/cmatch.css | 7 +++-- static/js/announcements.js | 27 ++++++++++++++----- static/js/change.js | 20 ++++++++++++-- static/js/ci18n_de.js | 2 +- static/js/ci18n_en.js | 2 +- static/js/cmatch.js | 44 +++++++++++++++++++++++++++--- static/js/ctournament.js | 28 +++++++++---------- 11 files changed, 190 insertions(+), 69 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 8f8098b..2a04913 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -216,20 +216,60 @@ function handle_location_changed(app, ws, msg) { const meetingpoint_announcement = msg.meetingpoint_announcement; const highlight = msg.highlight; + console.log(highlight); + const query = { tournament_key: msg.tournament_key, _id: msg.location_id, }; - app.db.locations.update(query, { $set: {highlight, preperation_addition, meetingpoint_announcement} }, {}, (err) => { + app.db.locations.findOne(query, async (err, old_location) => { if(err) { ws.respond(msg, err); return; } - notify_change(app, msg.tournament_key, 'location_changed', {location_id, highlight, preperation_addition, meetingpoint_announcement}); - ws.respond(msg); - return; + app.db.locations.update(query, { $set: {highlight, preperation_addition, meetingpoint_announcement} }, {}, (err) => { + if(err) { + ws.respond(msg, err); + return; + } + + notify_change(app, msg.tournament_key, 'location_changed', {location_id, highlight, preperation_addition, meetingpoint_announcement}); + notify_change(app, msg.tournament_key, 'location_highlight_changed', {old_location_highlight: old_location.highlight, new_location_highlight: highlight}); + + + const match_querry = { + tournament_key: msg.tournament_key, + 'setup.highlight': old_location.highlight, + }; + app.db.matches.update( + match_querry, + { $set: { 'setup.highlight': highlight } }, + { multi: true, returnUpdatedDocs: true }, + (err, numAffected, affectedDocs) => { + if (err) { + ws.respond(msg, err); + return; + } + + const btp_manager = require('./btp_manager'); + + // Wenn mehrere Matches aktualisiert wurden: + if (Array.isArray(affectedDocs)) { + for (const match of affectedDocs) { + btp_manager.update_highlight(app, match); + } + } else if (affectedDocs) { + // Falls nur ein Match betroffen war + btp_manager.update_highlight(app, affectedDocs); + } + + ws.respond(msg); + return; + } + ); + }); }); } @@ -721,6 +761,9 @@ function handle_match_edit(app, ws, msg) { ws.respond(msg, new Error(errmsg)); return; } + + console.waarn('WARN: Hier '); + notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, match: changed_match}); if (msg.btp_update) { btp_manager.update_score(app, changed_match); @@ -738,7 +781,7 @@ function handle_match_preparation_call(app, ws, msg) { const match_utils = require('./match_utils'); - if (!_require_msg(ws, msg, ['tournament_key', 'id', 'setup'])) { + if (!_require_msg(ws, msg, ['tournament_key', 'match_id', 'location_id', 'setup'])) { return; } if (match_utils.match_completly_initialized(msg.setup) == false) { @@ -752,7 +795,7 @@ function handle_match_preparation_call(app, ws, msg) { } const setup = _extract_setup(msg.setup); - match_utils.call_match_in_preparation(app, tournament, msg.id, setup, (err) => { + match_utils.call_match_in_preparation(app, tournament, msg.match_id, msg.location_id, setup, (err) => { ws.respond(msg, err); return; }); diff --git a/bts/btp_sync.js b/bts/btp_sync.js index ec00535..cf6c9f5 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -63,7 +63,7 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, rounds.set("17/32", [17, 32]); rounds.set("R32", [ 1, 32]); - if(match_name && draw.Position[0] > 1 && rounds.get(match_name)) { + if(match_name && draw.Position[0] >= 1 && rounds.get(match_name)) { const best_place = rounds.get(match_name)[0] + draw.Position[0] - 1; const lowes_place = rounds.get(match_name)[1] + draw.Position[0] - 1; @@ -487,6 +487,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { const matches_incomplete = []; const clubs = btp_state.clubs; const districts = btp_state.districts; + let changes = false; async.each(btp_state.matches, function (bm, cb) { const draw = draws.get(bm.DrawID[0]); @@ -559,7 +560,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { if (cur_match.setup.preparation_call_timestamp) { // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. match.setup.preparation_call_timestamp = cur_match.setup.preparation_call_timestamp; - match.setup.state = 'preparartion'; + match.setup.state = 'preparation'; } if (cur_match.setup.tabletoperators) { @@ -686,6 +687,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { // render onli if is_match flag is set. else it's nessasary to have the game (it's a link) in the db, but not to rerender if (match.setup.is_match) { if (!only_change_check_in || result_enterd_in_btp) { + changes = true; admin.notify_change(app, match.tournament_key, 'match_edit', { match__id: match._id, match: match @@ -702,7 +704,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { cb(null); return; } - + changes = true; matches_to_add.push(match); cb(null) return; @@ -751,19 +753,19 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { if (err) { console.error(err); } - admin.notify_change(app, tkey, 'match_add', { match }); }); }); - - setTimeout(function(){ - matches_incomplete.forEach(match => { - admin.notify_change(app, match.tournament_key, 'match_edit', { - match__id: match._id, - match: match + if(changes){ + setTimeout(function(){ + matches_incomplete.forEach(match => { + admin.notify_change(app, match.tournament_key, 'match_edit', { + match__id: match._id, + match: match + }); }); - }); - }, 500); + }, 500); + }; callback(null); @@ -801,9 +803,6 @@ function integrate_locations(app, tournament_key, btp_state, callback) { country, }; - - - app.db.locations.findOne(query, (err, cur_location) => { if (err) return cb(err); if (cur_location) { @@ -815,12 +814,6 @@ function integrate_locations(app, tournament_key, btp_state, callback) { btp_id, }; - - - - - - app.db.locations.findOne(alt_query, async (err, cur_location) => { if (err) return cb(err); @@ -831,7 +824,7 @@ function integrate_locations(app, tournament_key, btp_state, callback) { return; } - const highlights = ['highlight_0', 'highlight_1', 'highlight_2', 'highlight_3', 'highlight_4', 'highlight_5', 'highlight_6']; + const highlights = [0, 1, 2, 3, 4, 5, 6]; let highlight = null; for (let i = highlights.length - 1; i >= 0; i--) { diff --git a/bts/match_utils.js b/bts/match_utils.js index 2f21121..aa1199c 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -115,10 +115,27 @@ function remove_called_timestamp(match, callback) { return callback(null); } -function add_preparation_call_timestamp(setup) { - setup.highlight = 6; - setup.preparation_call_timestamp = Date.now(); - setup.state = 'preparartion'; +function add_preparation_call_timestamp(db, tournament_key, setup, location_id) { + return new Promise((resolve) => { + const stournament = require('./stournament'); + + stournament.get_locations(db, tournament_key, (err, all_locations) => { + for (const location of all_locations) { + if (location._id == location_id) { + console.log(location); + setup.highlight = location.highlight; + setup.preparation_location_id = location_id; + setup.preparation_call_timestamp = Date.now(); + setup.state = 'preparation'; + resolve(setup); + return; + } + } + serror.silent("Can't call a match in preparation for location ' + location_id."); + setup.highlight = 0; + resolve(setup); + }); + }); } function remove_preparation_call_timestamp(setup) { @@ -964,7 +981,7 @@ async function call_next_possible_match_for_preparation(app, tournament_key, cal } } if (possible) { - call_match_in_preparation(app, tournament,match._id, match.setup, callback); + call_match_in_preparation(app, tournament,match._id, null, match.setup, callback); break; } } @@ -979,11 +996,14 @@ async function call_next_possible_match_for_preparation(app, tournament_key, cal } -async function call_match_in_preparation(app, tournament, match_id, setup, callback) { - add_preparation_call_timestamp(setup); +async function call_match_in_preparation(app, tournament, match_id, location_id, setup, callback) { const tournament_key = tournament.key; const admin = require('./admin'); + await add_preparation_call_timestamp(app.db, tournament_key, setup, location_id); + + console.log(setup); + if (tournament.preparation_tabletoperator_setup_enabled) { if (!setup.umpire || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { if (!setup.tabletoperators || setup.tabletoperators == null) { @@ -1006,7 +1026,7 @@ async function call_match_in_preparation(app, tournament, match_id, setup, callb return callback(new Error(errmsg)); } - admin.notify_change(app, tournament_key, 'match_preparation_call', { match__id: match_id, match: changed_match }); + admin.notify_change(app, tournament_key, 'match_preparation_call', { match__id: match_id, match: changed_match}); const btp_manager = require('./btp_manager'); btp_manager.update_highlight(app, changed_match); return callback (null); diff --git a/bts/utils.js b/bts/utils.js index 2631f21..30807ef 100644 --- a/bts/utils.js +++ b/bts/utils.js @@ -140,6 +140,7 @@ function plucked_deep_equal(x, y, keys) { for (var i = 0;i < keys.length;i++) { var k = keys[i]; if (! deep_equal(x[k], y[k])) { + console.log(k + ' is not equal'); return false; } } diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 9cf381d..1c52dd0 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -32,11 +32,10 @@ } .match_preparation_call_button { - height: 1.2em; - min-height: 1em; - width: 1.4em; + height: 100%; + width: auto; margin: 0 0 -0.1em 0.3em; - background-image: url(../icons/preperation.svg); + /*background-image: url(../icons/preperation.svg);*/ } .match_manual_call_button { height: 1.5em; diff --git a/static/js/announcements.js b/static/js/announcements.js index 6871960..36a6490 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -20,7 +20,7 @@ function announcePreparationMatch(matchSetup) { return; } const field = createFieldPreparationAnnouncement(matchSetup); - var preparation = createPreparationAnnouncement(); + var preparation = createPreparationAnnouncement(matchSetup); var matchNumber = createMatchNumberAnnouncement(matchSetup); var eventName = createEventAnnouncement(matchSetup); var round = createRoundAnnouncement(matchSetup); @@ -30,7 +30,7 @@ function announcePreparationMatch(matchSetup) { const tabletOperator = createTabletOperator(matchSetup); var lastPart = preparation; if (curt.preparation_meetingpoint_enabled) { - lastPart = createMeetingPointAnnouncement(); + lastPart = createMeetingPointAnnouncement(matchSetup); } announce([preparation, field, matchNumber, eventName, round, teams, umpire, serviceJudge, tabletOperator, lastPart]); } @@ -259,12 +259,27 @@ function createFieldPreparationAnnouncement(matchSetup) { } -function createPreparationAnnouncement() { - return ci18n('announcements:preparation'); +function createPreparationAnnouncement(matchSetup) { + let addition = ""; + if(matchSetup.preparation_location_id) { + const l = utils.find(curt.locations, l => l._id === matchSetup.preparation_location_id); + if(l) { + addition = ' ' + l.preperation_addition; + } + } + return ci18n('announcements:preparation') + addition; } -function createMeetingPointAnnouncement() { - return ci18n('announcements:meetingpoint'); +function createMeetingPointAnnouncement(matchSetup) { + let result = ci18n('announcements:meetingpoint'); + if(matchSetup.preparation_location_id) { + const l = utils.find(curt.locations, l => l._id === matchSetup.preparation_location_id); + if(l) { + result = l.meetingpoint_announcement; + } + } + + return result; } function announce(callArray) { diff --git a/static/js/change.js b/static/js/change.js index fad3e16..7c258be 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -157,8 +157,6 @@ function default_handler(rerender, special_funcs) { break; case 'location_changed': const l = utils.find(curt.locations, l => l._id === c.val.location_id); - console.log(l); - if(l) { l.highlight = c.val.highlight; l.preperation_addition = c.val.preperation_addition; @@ -166,6 +164,24 @@ function default_handler(rerender, special_funcs) { } ctournament.update_location(c.val.location_id, c.val.highlight, c.val.preperation_addition, c.val.meetingpoint_announcement); break; + case 'location_highlight_changed': + const old_location_highlight = c.val.old_location_highlight; + const new_location_highlight = c.val.new_location_highlight; + + curt.matches.forEach((match) => { + console.log(match.setup.state); + console.log(match.setup.highlight); + + console.log(old_location_highlight); + console.log(new_location_highlight); + if(match.setup.highlight == old_location_highlight && match.setup.state === 'preparation'){ + match.setup.highlight = new_location_highlight; + c.val = { match__id: match._id, match} + ctournament.update_match(c); + ctournament.update_upcoming_match(c); + } + }); + break; case 'location_logo_changed': const loc = utils.find(curt.locations, loc => loc._id === c.val.location_id); if(loc) { diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 8c2e76b..ae78c96 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -62,7 +62,7 @@ var ci18n_de = { 'announcements:umpire': 'Schiedsrichter:', 'announcements:service_judge': 'Aufschlagrichter:', 'announcements:and': ' und ', - 'announcements:preparation': 'In Vorbereitung:', + 'announcements:preparation': 'In Vorbereitung', 'announcements:meetingpoint': 'Treffen am Meetingpoint!', 'announcements:on_court': 'Auf Spielfeld ', 'announcements:for_court': 'Für Spielfeld ', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index fea1503..18acf78 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -62,7 +62,7 @@ var ci18n_en = { 'announcements:umpire': 'Umpire:', 'announcements:service_judge': 'Servicejudge:', 'announcements:and': ' and ', - 'announcements:preparation': 'In preparation:', + 'announcements:preparation': 'In preparation', 'announcements:meetingpoint': 'Come to the meetingpoint!', 'announcements:on_court': 'On Court ', 'announcements:for_court': 'For Court ', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index b0e9c78..f505d5d 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -497,7 +497,10 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ const call_td = uiu.el(tr, 'td', 'call_td'); if (style === 'unasigned' && completeMatch) { - create_match_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id); + const locations = curt.locations; + locations.forEach((l)=> { + create_match_prepparation_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id, l); + }); } else if ((style === 'default' || style === 'plain') && court) { create_match_button(call_td, 'vlink match_manual_call_button', 'match:manualcall', on_announce_match_manually_button_click, match._id); create_match_button(call_td, 'vlink match_begin_to_play_button', 'match:begintoplay', on_begin_to_play_button_click, match._id); @@ -547,6 +550,26 @@ function create_match_button(targetEl, cssClass, title, listener, matchId,) { }); btn.addEventListener('click', listener); } + +function create_match_prepparation_button(targetEl, cssClass, title, listener, matchId, location){ + const btn = uiu.el(targetEl, 'div', { + 'class': cssClass, + 'title': ci18n(title) + (location.preperation_addition ? ' ' + location.preperation_addition : ''), + 'data-match_id': matchId, + 'data-location_id': location._id, + }); + + uiu.el(btn, 'img', { + style: 'height: 1.2em; margin-top: 0.2em;', + src: location.logo_id ? '/h/' + encodeURIComponent(curt.key) + '/logo/' + location.logo_id : '/static/icons/preperation.svg', + name: 'location_logo_img', + 'data-match_id': matchId, + 'data-location_id': location._id + }); + + btn.addEventListener('click', listener); +} + function update_match_score(m) { uiu.qsEach('.match_score[data-match_id=' + JSON.stringify(m._id) + ']', function(score_el) { uiu.text(score_el, calc_score_str(m)); @@ -1064,7 +1087,7 @@ function _extract_preparation_timer_state(match) { s.timer.start = (match.setup.preparation_call_timestamp ? match.setup.preparation_call_timestamp : false); s.timer.upwards = true; s.timer.exigent = false; - s.bgColor = "#C56BFF"; + s.bgColor = "#00000033"; return s; } @@ -1134,10 +1157,12 @@ function on_scoresheet_button_click(e) { } function on_announce_preparation_matchbutton_click(e) { const match = fetchMatchFromEvent(e); - if (match != null) { + const location = fetchLocationFromEvent(e); + if (match != null && location != null) { send({ type: 'match_preparation_call', - id: match._id, + match_id: match._id, + location_id : location._id, tournament_key: match.tournament_key, setup: match.setup, }, function (err) { @@ -1270,6 +1295,17 @@ function fetchMatchFromEvent(e) { return match; } } +function fetchLocationFromEvent(e) { + const btn = e.target; + const location_id = btn.getAttribute('data-location_id'); + const location = utils.find(curt.locations, l => l._id === location_id); + if (!location) { + cerror.silent('Location ' + location_id + ' konnte nicht gefunden werden'); + return null; + } else { + return location; + } +} function _nation_team_name(nat0, nat1) { if (nat1 && nat0 && (nat0 != nat1)) { return countries.lookup(nat0) + ' / ' + countries.lookup(nat1); diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 3500450..88348ae 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -1281,8 +1281,8 @@ var ctournament = (function() { src: '/h/' + encodeURIComponent(curt.key) + '/logo/' + curt.logo_id, name: 'logo_preview_img' }); - uiu.el(logo_preview_container, 'div', {}, 'Court 42'); } + uiu.el(logo_preview_container, 'div', {}, 'Court 42'); const logo_form = uiu.el(main, 'form', 'logo_form'); const logo_button_id = 'logo_upload_input'; @@ -1932,7 +1932,7 @@ var ctournament = (function() { uiu.el(tr, 'th', {}, 'Meetingpoint durchsage'); uiu.el(tr, 'th', {}, 'In Vorbereitung Icon'); uiu.el(tr, 'th', {}, ''); - + let highlight_in_use = []; for (const l of curt.locations) { if(l.highlight) { @@ -1940,22 +1940,19 @@ var ctournament = (function() { } } - for (const l of curt.locations) { const tr = uiu.el(locations_tbody, 'tr'); const name_th = uiu.el(tr, 'th', {}); uiu.el(name_th, 'div', {}, l.name); - console.log(highlight_in_use); - const content = ['highlight_1', 'highlight_2', 'highlight_3', 'highlight_4', 'highlight_5', 'highlight_6']; + const content = [1, 2, 3, 4, 5, 6]; const selected = l.highlight; - const select_color = uiu.el(name_th, 'select', {id: 'select_highlight', class: selected, 'data-location_id': l._id}); - for (const item of content) { - //if(!highlight_in_use.includes(item) || selected === item) { + const select_color = uiu.el(name_th, 'select', {id: 'select_highlight', class: 'highlight_' + selected, 'data-location_id': l._id}); + for (const item of content) { const attrs = { 'data-display-setting-id': item, value: item, - class: item, + class: 'highlight_' + item, style: (highlight_in_use.includes(item) && selected !== item) ? 'display:none;' : '', } if ((selected === item)) { @@ -1989,8 +1986,8 @@ var ctournament = (function() { }); const icon_td = uiu.el(tr, 'td', 'icon_td'); uiu.el(icon_td, 'img', { - style: l.logo_id != null ? 'height: 40px;' : 'display: none;', - src: '/h/' + encodeURIComponent(curt.key) + '/logo/' + (l.logo_id ? l.logo_id : ''), + style: 'height: 40px;', + src: l.logo_id ? '/h/' + encodeURIComponent(curt.key) + '/logo/' + l.logo_id : '/static/icons/preperation.svg', name: 'location_logo_img', 'data-location_id': l._id }); @@ -2001,7 +1998,7 @@ var ctournament = (function() { const filename_display = uiu.el(logo_form, 'div', { class: 'upload_filename_location', 'data-location_id': l._id, - }, l.logo_name ? l.logo_name : 'Noch keine Datei ausgewählt'); + }, l.logo_name ? l.logo_name : 'preperation.svg'); const custom_label = uiu.el(logo_form, 'label', { for: logo_button_id, @@ -2067,7 +2064,6 @@ var ctournament = (function() { case 'edit': const location_logo_img = document.querySelector(`[name="location_logo_img"][data-location_id="${location_id}"]`); location_logo_img.setAttribute('src', '/h/' + encodeURIComponent(curt.key) + '/logo/' + logo_id); - location_logo_img.style = 'height: 40px;'; const filename_display = document.querySelector(`.upload_filename_location[data-location_id="${location_id}"]`); filename_display.textContent = logo_name; break; @@ -2078,15 +2074,17 @@ var ctournament = (function() { } function send_location_to_admin(parent, location_id) { - const highlight = parent.querySelector("#select_highlight").value; + const highlight = parseInt(parent.querySelector("#select_highlight").value, 10); const preperation_addition = parent.querySelector("#preperation_addition").value; const meetingpoint_announcement = parent.querySelector("#meetingpoint_announcement").value; + console.log(highlight); + send({ type: 'location_changed', tournament_key: curt.key, location_id, - highlight, + highlight: highlight, preperation_addition, meetingpoint_announcement, }, function (err, response) { From ff1de80b6046563a24f44543deda0678dc63dea0 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 9 May 2025 12:41:50 +0200 Subject: [PATCH 197/224] current version --- blank_tournament/displaysettings | 6 +- bts/admin.js | 7 --- bts/btp_sync.js | 6 ++ bts/bupws.js | 94 +++----------------------------- bts/match_utils.js | 3 - bts/utils.js | 1 - static/js/change.js | 5 -- static/js/ctournament.js | 11 +--- 8 files changed, 20 insertions(+), 113 deletions(-) diff --git a/blank_tournament/displaysettings b/blank_tournament/displaysettings index e7782eb..516d955 100644 --- a/blank_tournament/displaysettings +++ b/blank_tournament/displaysettings @@ -1,3 +1,3 @@ -{"id":"nur_spieler","displaymode_style":"onlyplayers","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_2","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_2","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"settings_style":"clean","_id":"9t2vgoCBMN0Rs5kA"} -{"id":"nur_spielstand","displaymode_style":"onlyscore","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_2","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_2","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"settings_style":"clean","_id":"eddY0hYOZPvnQxMX"} -{"id":"default","displaymode_style":"tournamentcourt","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_7","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_7","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"settings_style":"clean","_id":"fKYy03Qs6635VWRO"} +{"id":"nur_spieler","displaymode_style":"onlyplayers","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_2","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_2","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"style":"clean","_id":"9t2vgoCBMN0Rs5kA"} +{"id":"nur_spielstand","displaymode_style":"onlyscore","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_2","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_2","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"style":"clean","_id":"eddY0hYOZPvnQxMX"} +{"id":"default","displaymode_style":"tournamentcourt","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_7","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_7","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"style":"clean","_id":"fKYy03Qs6635VWRO"} diff --git a/bts/admin.js b/bts/admin.js index 2a04913..bd89af3 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -211,13 +211,10 @@ function handle_location_changed(app, ws, msg) { return; } const location_id = msg.location_id; - console.log(location_id); const preperation_addition = msg.preperation_addition; const meetingpoint_announcement = msg.meetingpoint_announcement; const highlight = msg.highlight; - console.log(highlight); - const query = { tournament_key: msg.tournament_key, _id: msg.location_id, @@ -762,8 +759,6 @@ function handle_match_edit(app, ws, msg) { return; } - console.waarn('WARN: Hier '); - notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, match: changed_match}); if (msg.btp_update) { btp_manager.update_score(app, changed_match); @@ -950,8 +945,6 @@ function handle_display_reset(app, ws, msg) { const client_id = msg.display_client_id; const bupws = require('./bupws'); - console.log("Try to reset: " + client_id); - bupws.restart_panel(app, tournament_key, client_id); ws.respond(msg); } diff --git a/bts/btp_sync.js b/bts/btp_sync.js index cf6c9f5..1a1cb4a 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -1239,6 +1239,12 @@ async function integrate_now_on_court(app, tkey, callback) { match_utils.call_match(app, tournament, match, undefined, (err) => { if (err) console.log(err); }); + } else { + const query = { + tournament_key: tkey, + _id: court_id, + }; + app.db.courts.update(query, {$set: {match_id}}); } })); callback(null); diff --git a/bts/bupws.js b/bts/bupws.js index cbe0df9..20115e1 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -143,7 +143,11 @@ async function handle_score_update(app, ws, msg) { const score_data = msg.score; const match_id = score_data.match_id; - var match = await match_utils.fetch_match(app, tournament_key, match_id); + try{ + var match = await match_utils.fetch_match(app, tournament_key, match_id); + } catch { + var match = null; + } if (match == null || match.setup.now_on_court == false) { send_error(ws, tournament_key, "Match not found or not on court actualy."); return; @@ -368,16 +372,15 @@ async function send_advertisement_remove(app, tournament_key, advertisement_id) notify_change_broadcast(app, tournament_key, 'advertisement_remove', { advertisement_id: advertisement_id }); } -async function initialize_client(ws, app, tournament_key, court_id, displaysetting) { +async function initialize_client(ws, app, tournament_key, court_id, displaysetting_id) { const client_id = determine_client_id(ws); const hostname = await determine_client_hostname(ws); if (client_id) { - let display_setting = await get_display_setting(app, tournament_key, client_id, court_id, displaysetting) + let display_setting = await get_display_setting(app, tournament_key, client_id, court_id, displaysetting_id) if (display_setting != null) { ws.court_id = display_setting.court_id; court_id = display_setting.court_id; notify_change_ws(app, ws, tournament_key, court_id, "settings-update", display_setting); - notify_change_ws(app, ws, tournament_key, court_id, "settings-update", display_setting); } } matches_handler(app, ws, tournament_key, ws.court_id); @@ -407,85 +410,6 @@ function extractIPv4FromMappedIPv6(ip) { return match ? match[1] : null; } -/* -async function determine_client_hostname(ws) { - if (ws.hostname) { - return ws.hostname; - } - - const remoteAddress = ws._socket.remoteAddress; - const mappedIPv4 = extractIPv4FromMappedIPv6(remoteAddress); - - if (mappedIPv4) { - // Hier behandeln wir es wie ein echtes IPv4 - if (mappedIPv4 === "127.0.0.1") { - ws.hostname = getComputerName(); - return ws.hostname; - } - - try { - const hostnames = await new Promise((resolve, reject) => { - dns.reverse(mappedIPv4, (err, hostnames) => { - if (err) reject(err); - else resolve(hostnames); - }); - }); - - ws.hostname = hostnames?.[0]?.split(".")[0] || "N/N"; - } catch (err) { - console.error("DNS reverse lookup failed:", err); - ws.hostname = "N/N"; - } - - return ws.hostname; - } - - - // Verifizieren, ob die Adresse gültig ist - if (net.isIPv4(remoteAddress)) { - // Lokale IP-Adressen abfangen - if (remoteAddress === "127.0.0.1") { - ws.hostname = getComputerName(); - return ws.hostname; - } - - // Rückwärtssuche für IPv4 - try { - const hostnames = await new Promise((resolve, reject) => { - dns.reverse(remoteAddress, (err, hostnames) => { - if (err) reject(err); - else resolve(hostnames); - }); - }); - - if (hostnames && hostnames.length > 0) { - ws.hostname = hostnames[0].split(".")[0]; - } else { - ws.hostname = "N/N"; - } - } catch (err) { - console.error("DNS reverse lookup failed:", err); - ws.hostname = "N/N"; - } - - return ws.hostname; - } else if (net.isIPv6(remoteAddress)) { - // IPv6-Adresse prüfen (z. B. `::1` für localhost) - if (remoteAddress === "::1") { - ws.hostname = getComputerName(); - return ws.hostname; - } - - // Bei IPv6 keine spezielle Verarbeitung (z. B. Reverse-Lookup) - ws.hostname = remoteAddress; - return ws.hostname; - } else { - console.error("Invalid IP address:", remoteAddress); - ws.hostname = "N/N"; - return ws.hostname; - } -}*/ - async function determine_client_hostname(ws) { if (ws.hostname) { return ws.hostname; @@ -956,14 +880,14 @@ async function change_default_display_mode(app, tournament, old_displaysettings_ -function reinitialize_panel(app, tournament_key, client_id, new_court_id, displaysetting) { +function reinitialize_panel(app, tournament_key, client_id, new_court_id, displaysetting_id) { for (const panel_ws of all_panels) { const ws_client_id = determine_client_id(panel_ws); if (client_id == ws_client_id) { if (new_court_id != null) { panel_ws.court_id = new_court_id; } - initialize_client(panel_ws, app, tournament_key, panel_ws.court_id, displaysetting); + initialize_client(panel_ws, app, tournament_key, panel_ws.court_id, displaysetting_id); return true; } } diff --git a/bts/match_utils.js b/bts/match_utils.js index aa1199c..11f511b 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -122,7 +122,6 @@ function add_preparation_call_timestamp(db, tournament_key, setup, location_id) stournament.get_locations(db, tournament_key, (err, all_locations) => { for (const location of all_locations) { if (location._id == location_id) { - console.log(location); setup.highlight = location.highlight; setup.preparation_location_id = location_id; setup.preparation_call_timestamp = Date.now(); @@ -1002,8 +1001,6 @@ async function call_match_in_preparation(app, tournament, match_id, location_id, await add_preparation_call_timestamp(app.db, tournament_key, setup, location_id); - console.log(setup); - if (tournament.preparation_tabletoperator_setup_enabled) { if (!setup.umpire || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { if (!setup.tabletoperators || setup.tabletoperators == null) { diff --git a/bts/utils.js b/bts/utils.js index 30807ef..2631f21 100644 --- a/bts/utils.js +++ b/bts/utils.js @@ -140,7 +140,6 @@ function plucked_deep_equal(x, y, keys) { for (var i = 0;i < keys.length;i++) { var k = keys[i]; if (! deep_equal(x[k], y[k])) { - console.log(k + ' is not equal'); return false; } } diff --git a/static/js/change.js b/static/js/change.js index 7c258be..99011fb 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -169,11 +169,6 @@ function default_handler(rerender, special_funcs) { const new_location_highlight = c.val.new_location_highlight; curt.matches.forEach((match) => { - console.log(match.setup.state); - console.log(match.setup.highlight); - - console.log(old_location_highlight); - console.log(new_location_highlight); if(match.setup.highlight == old_location_highlight && match.setup.state === 'preparation'){ match.setup.highlight = new_location_highlight; c.val = { match__id: match._id, match} diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 88348ae..ea6e241 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -1032,8 +1032,6 @@ var ctournament = (function() { })); function send_props(input, callback) { - console.log("send_props()"); - const props = { name : input.name.value, tguid: input.tguid.value, @@ -1362,7 +1360,6 @@ var ctournament = (function() { fg_col_input.value = curt.logo_foreground_color; const logo_preview_img = logo_preview_container.querySelector('[name="logo_preview_img"]'); logo_preview_img.setAttribute('src', '/h/' + encodeURIComponent(curt.key) + '/logo/' + curt.logo_id); - console.log(logo_preview_img); const filename_display = document.querySelector('#upload_filename'); filename_display.textContent = curt.logo_name ? curt.logo_name : 'Noch keine Datei ausgewählt'; break; @@ -1448,7 +1445,6 @@ var ctournament = (function() { function ui_edit_display_setting(display_setting_id) { const display_setting = structuredClone(utils.find(curt.displaysettings, d => d.id === display_setting_id)); - crouting.set('t/' + curt.key + '/edit/s/' + display_setting_id, {}, _cancel_ui_edit_display_setting); cbts_utils.esc_stack_push(_cancel_ui_edit_display_setting); @@ -1507,7 +1503,6 @@ var ctournament = (function() { })); function render_edit_display_setting(form, display_setting) { - const edit_display_setting_container = uiu.el(form, 'div', 'edit_display_setting_container'); const id_div = uiu.el(edit_display_setting_container, 'div'); uiu.el(id_div, 'span', 'display_setting_id', ci18n('display_setting:id')); @@ -1651,7 +1646,8 @@ var ctournament = (function() { 'focus', 'hidden', ]; - render_drop_down(edit_display_setting_container, ci18n('display_setting:settings_style'), 'style', calculated_style, ALL_STYLE_MODES, display_setting.settings_style || ''); + + render_drop_down(edit_display_setting_container, ci18n('display_setting:settings_style'), 'style', calculated_style, ALL_STYLE_MODES, display_setting.style || ''); render_select_number(edit_display_setting_container, ci18n('display_setting:network_timeout'), 'network_timeout', true, display_setting.network_timeout, 1, 600000); render_select_number(edit_display_setting_container, ci18n('display_setting:network_update_interval'), 'network_update_interval', true, display_setting.network_update_interval, 1, 600000); } @@ -1964,7 +1960,6 @@ var ctournament = (function() { select_color.addEventListener('change', (e) => { e.target.classList = [e.target.value]; - console.log(e.target.getAttribute('data-location-id')); send_location_to_admin(e.target.parentNode.parentNode, e.target.getAttribute('data-location_id')); }); @@ -2078,8 +2073,6 @@ var ctournament = (function() { const preperation_addition = parent.querySelector("#preperation_addition").value; const meetingpoint_announcement = parent.querySelector("#meetingpoint_announcement").value; - console.log(highlight); - send({ type: 'location_changed', tournament_key: curt.key, From d70b38157828b831c5afe39af4610211f3be3292 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 3 Jun 2025 00:51:35 +0200 Subject: [PATCH 198/224] Implement better support for Tournaments that are played in multiple locations --- bts/btp_parse.js | 52 ++++++++- bts/btp_sync.js | 193 ++++++++++++++++++++++++--------- bts/bupws.js | 4 + static/css/cmatch.css | 19 ++++ static/js/announcements.js | 33 +++--- static/js/cmatch.js | 49 ++++++++- static/js/ctournament.js | 214 ++++++++++++++++++++++++++++++++----- 7 files changed, 470 insertions(+), 94 deletions(-) diff --git a/bts/btp_parse.js b/bts/btp_parse.js index 2d5f823..5dd9171 100644 --- a/bts/btp_parse.js +++ b/bts/btp_parse.js @@ -39,7 +39,11 @@ function filter_matches(all_btp_matches, is_league) { // bts_players: Array of array of players participating. // Only for matches, not individual players // bts_winners: Array of players who have won this match. -function _calc_match_players(matches_by_pid, entries, players, bm, is_league) { +function _calc_match_players(matches_by_pid, entries, stage_entries, draws, players, bm, is_league) { + const draw = draws.get(bm.DrawID[0]); + const stage_id = draw.StageID[0]; + let entry_status = ""; + if (bm.bts_winners) { return; } @@ -50,12 +54,46 @@ function _calc_match_players(matches_by_pid, entries, players, bm, is_league) { throw new Error('Cannot find entry ' + bm.EntryID[0]); } + for (const [key, value] of stage_entries) { + if (value.StageID[0] === stage_id && value.EntryID[0] === bm.EntryID[0]) { + //console.log(`Gefunden: Key = ${key}, Value =`, value); + switch (value.Status[0]){ + case 108: + entry_status = "DNS"; + break; + case 109: + entry_status = "WDN"; + break; + + + } + break; + } + } + + const p1 = players.get(e.Player1ID[0]); assert(p1); + + if(!p1.entries) { + p1.entries = new Map([[draw.ID[0], ""]]); + } + + if(entry_status != "") { + p1.entries.set(draw.ID[0], entry_status); + } + const res = [p1]; if (e.Player2ID) { const p2 = players.get(e.Player2ID[0]); assert(p2); + if(!p2.entries) { + p2.entries = new Map([[draw.ID[0], ""]]); + } + + if(entry_status != "") { + p2.entries.set(draw.ID[0], entry_status); + } res.push(p2); } bm.bts_winners = res; @@ -67,6 +105,7 @@ function _calc_match_players(matches_by_pid, entries, players, bm, is_league) { // Normal match let p1ar, p2ar; + let p1es, p2es; if (is_league) { if (!bm.Team1Player1ID) return; @@ -88,17 +127,20 @@ function _calc_match_players(matches_by_pid, entries, players, bm, is_league) { assert(bm.From1[0]); const m1 = matches_by_pid.get(bm.DrawID[0] + '_' + bm.From1[0], is_league); assert(m1); - _calc_match_players(matches_by_pid, entries, players, m1); + _calc_match_players(matches_by_pid, entries, stage_entries, draws, players, m1); p1ar = m1.bts_winners; assert(bm.From2); assert(bm.From2[0]); const m2 = matches_by_pid.get(bm.DrawID[0] + '_' + bm.From2[0], is_league); assert(m2); - _calc_match_players(matches_by_pid, entries, players, m2, is_league); + _calc_match_players(matches_by_pid, entries, stage_entries, draws, players, m2, is_league); p2ar = m2.bts_winners; + } bm.bts_players = [p1ar, p2ar]; + bm.bts_players_entry_status = [p1es, p2es]; + if (p1ar && p2ar) { bm.bts_complete = true; if (bm.Winner) { @@ -174,6 +216,7 @@ function get_btp_state(response) { const matches_by_pid = utils.make_index(all_btp_matches, bm => _calc_match_id(bm, is_league)); const all_btp_entries = btp_t.Entries ? btp_t.Entries[0].Entry : []; + const all_btp_stage_entries = btp_t.StageEntries ? btp_t.StageEntries[0].StageEntry : []; const all_btp_events = btp_t.Events ? btp_t.Events[0].Event : []; const all_btp_players = btp_t.Players ? btp_t.Players[0].Player : []; const all_btp_draws = btp_t.Draws ? btp_t.Draws[0].Draw : []; @@ -194,6 +237,7 @@ function get_btp_state(response) { } const entries = utils.make_index(all_btp_entries, e => e.ID[0]); + const stage_entries = utils.make_index(all_btp_stage_entries, s => s.ID[0]); const events = utils.make_index(all_btp_events, e => e.ID[0]); const draws = utils.make_index(all_btp_draws, d => d.ID[0]); @@ -221,7 +265,7 @@ function get_btp_state(response) { const btp_settings = utils.make_index(all_btp_settings, s =>s.ID[0]); for (const bm of matches) { - _calc_match_players(matches_by_pid, entries, players, bm, is_league); + _calc_match_players(matches_by_pid, entries, stage_entries, draws, players, bm, is_league); } return { courts, diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 1a1cb4a..bc95861 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -18,8 +18,9 @@ function date_str(dt) { return utils.pad(dt.year, 2, '0') + '-' + utils.pad(dt.month, 2, '0') + '-' + utils.pad(dt.day, 2, '0'); } -async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, officials, clubs, districts, bm, match_ids_on_court, match_types, is_league) { +async function craft_match(app, tkey, btp_id, location_map, court_map, event, draw, btp_links, officials, clubs, districts, bm, match_ids_on_court, match_types, is_league) { return new Promise((resolve, reject) => { + const stournament = require('./stournament'); // avoid dependency cycle const gtid = event.GameTypeID[0]; assert((gtid === 1) || (gtid === 2)); @@ -31,45 +32,49 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, const teams = _craft_teams(bm, clubs, districts); const rounds = new Map(); - rounds.set("Finale", [ 1, 2]); - rounds.set("3/4", [ 3, 4]); - rounds.set("5/6", [ 5, 6]); - rounds.set("7/8", [ 7, 8]); - rounds.set("9/10", [ 9, 10]); - rounds.set("11/12", [11, 12]); - rounds.set("13/14", [13, 14]); - rounds.set("15/16", [15, 16]); - rounds.set("17/18", [17, 18]); - rounds.set("19/20", [19, 20]); - rounds.set("21/22", [21, 22]); - rounds.set("23/24", [23, 24]); - rounds.set("25/26", [25, 26]); - rounds.set("27/28", [27, 28]); - rounds.set("29/30", [29, 30]); - rounds.set("31/32", [31, 32]); - rounds.set("HF", [ 1, 4]); - rounds.set("5/8", [ 5, 8]); - rounds.set("9/12", [ 9, 12]); - rounds.set("13/16", [13, 16]); - rounds.set("17/20", [17, 20]); - rounds.set("21/24", [21, 24]); - rounds.set("25/28", [25, 28]); - rounds.set("29/32", [29, 32]); - rounds.set("VF", [ 1, 8]); - rounds.set("9/16", [ 9, 16]); - rounds.set("17/24", [17, 24]); - rounds.set("25/32", [25, 32]); - rounds.set("R16", [ 1, 16]); - rounds.set("17/32", [17, 32]); - rounds.set("R32", [ 1, 32]); - - if(match_name && draw.Position[0] >= 1 && rounds.get(match_name)) { + if(draw.Position[0] > 1) { + rounds.set("Finale", [ 1, 2]); + rounds.set("HF", [ 1, 4]); + rounds.set("VF", [ 1, 8]); + rounds.set("R16", [ 1, 16]); + rounds.set("R32", [ 1, 32]); + } + rounds.set("3/4", [ 3, 4]); + rounds.set("5/6", [ 5, 6]); + rounds.set("7/8", [ 7, 8]); + rounds.set("9/10", [ 9, 10]); + rounds.set("11/12", [11, 12]); + rounds.set("13/14", [13, 14]); + rounds.set("15/16", [15, 16]); + rounds.set("17/18", [17, 18]); + rounds.set("19/20", [19, 20]); + rounds.set("21/22", [21, 22]); + rounds.set("23/24", [23, 24]); + rounds.set("25/26", [25, 26]); + rounds.set("27/28", [27, 28]); + rounds.set("29/30", [29, 30]); + rounds.set("31/32", [31, 32]); + rounds.set("5/8", [ 5, 8]); + rounds.set("9/12", [ 9, 12]); + rounds.set("13/16", [13, 16]); + rounds.set("17/20", [17, 20]); + rounds.set("21/24", [21, 24]); + rounds.set("25/28", [25, 28]); + rounds.set("29/32", [29, 32]); + rounds.set("9/16", [ 9, 16]); + rounds.set("17/24", [17, 24]); + rounds.set("25/32", [25, 32]); + rounds.set("17/32", [17, 32]); + rounds.set("CP- R16", [ 5, 16]); + rounds.set("CP- VF", [ 5, 12]); + + if(match_name && rounds.get(match_name)) { const best_place = rounds.get(match_name)[0] + draw.Position[0] - 1; const lowes_place = rounds.get(match_name)[1] + draw.Position[0] - 1; match_name = best_place + "/" + lowes_place; } else { - event_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + ' - ' + draw.Name[0]; + event_name = (event.Name[0] === draw.Name[0]) ? draw.Name[0] : event.Name[0] + (draw.DrawTypeID[0] > 1 ? ' - ' + draw.Name[0] : ""); } const btp_player_ids = []; @@ -189,6 +194,20 @@ async function craft_match(app, tkey, btp_id, court_map, event, draw, btp_links, assert(court_id); setup.court_id = court_id; setup.now_on_court = match_ids_on_court.has(bm.ID[0]); + } + if(bm.LocationID) { + const btp_location_id = bm.LocationID[0]; + const location_id = location_map.get(btp_location_id); + assert(location_id); + setup.location_id = location_id; + } + if(setup.highlight != 0) { + stournament.get_locations(app.db, tkey, function (err, all_locations) { + const location = all_locations.find(loc => loc.highlight === setup.highlight); + if(location) { + setup.location_id = location._id; + } + }); } if (bm.Official1ID) { const o = get_umpire(app, tkey, officials, bm.Official1ID[0]); @@ -273,6 +292,10 @@ function _craft_team(par) { pres.nationality = p.Country[0]; } + //if (p.entries) { + // pres.entries = p.entries; + //} + if (p.LastTimeOnCourt && p.LastTimeOnCourt[0]) { let date = new Date(p.LastTimeOnCourt[0].year, p.LastTimeOnCourt[0].month - 1, @@ -370,7 +393,9 @@ function _craft_teams(bm, clubs, districts) { assert(clubs); assert(districts); - return bm.bts_players.map(_craft_team, {clubs: clubs, districts: districts}); + let res = bm.bts_players.map(_craft_team, {clubs: clubs, districts: districts}); + + return res; } function _parse_score(bm) { @@ -472,7 +497,7 @@ function get_umpire(app, tkey, umpires , btp_id) { return returnValue; } -async function integrate_matches(app, tkey, btp_state, court_map, callback) { +async function integrate_matches(app, tkey, btp_state, location_map, court_map, callback) { const admin = require('./admin'); // avoid dependency cycle const match_utils = require('./match_utils'); const { draws, events } = btp_state; @@ -489,7 +514,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { const districts = btp_state.districts; let changes = false; - async.each(btp_state.matches, function (bm, cb) { + async.each(btp_state.matches, function (bm, cb) { const draw = draws.get(bm.DrawID[0]); assert(draw); @@ -518,7 +543,7 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { return; } - craft_match(app, tkey, btp_id, court_map, event, draw, btp_state.links, officials, clubs, districts, bm, match_ids_on_court).then(match => { + craft_match(app, tkey, btp_id, location_map, court_map, event, draw, btp_state.links, officials, clubs, districts, bm, match_ids_on_court).then(match => { match.setup.state = 'unscheduled'; @@ -772,6 +797,70 @@ async function integrate_matches(app, tkey, btp_state, court_map, callback) { }); } +function generateHallAbbreviation(name) { + const wordRegex = /([A-Za-zÄÖÜäöüß0-9]+)([\s\-]*)/g; + let match; + let abbreviation = ''; + let parts = []; + let foundAcronym = false; + + // Zerlege in Wortteile + Trennzeichen + while ((match = wordRegex.exec(name)) !== null) { + parts.push({ + word: match[1], + sep: match[2] || '' + }); + } + + let i = 0; + while (i < parts.length) { + const { word, sep } = parts[i]; + + // Zahlen mit optionalem Buchstaben (z.B. "12A") + if (/^\d+[A-Z]?$/.test(word)) { + abbreviation += sep + word; + + // Sonderregel: nächstes Wort beginnt mit Großbuchstabe → ersten Buchstaben übernehmen + if (i + 1 < parts.length && /^[A-ZÄÖÜ]/.test(parts[i + 1].word)) { + const next = parts[i + 1]; + abbreviation += next.sep + next.word[0].toUpperCase(); + i++; // zusätzliches Wort verarbeitet + } + + i++; + continue; + } + + // Großbuchstaben-Akronym + if (!foundAcronym && /^[A-ZÄÖÜ]{2,}$/.test(word)) { + abbreviation += word; + + if (i + 1 < parts.length) { + abbreviation += sep + parts[i + 1].word[0].toUpperCase(); + } else { + abbreviation += sep; + } + foundAcronym = true; + i += 2; + continue; + } + + // Standard: erster Buchstabe + if (!foundAcronym) { + abbreviation += word[0].toUpperCase() + sep; + } + + i++; + } + + // Kein Akronym → Leerzeichen & Endpunkt entfernen + if (!foundAcronym) { + abbreviation = abbreviation.replace(/\s+/g, ''); + abbreviation = abbreviation.replace(/\.+$/, ''); + } + + return abbreviation.trim(); +} function integrate_locations(app, tournament_key, btp_state, callback) { const admin = require('./admin'); // avoid dependency cycle @@ -791,6 +880,7 @@ function integrate_locations(app, tournament_key, btp_state, callback) { const country = (l.Country ? l.Country[0] : ""); const preperation_addition = ""; const meetingpoint_announcement = ""; + const short_name = generateHallAbbreviation(name); const query = { tournament_key, @@ -801,11 +891,13 @@ function integrate_locations(app, tournament_key, btp_state, callback) { city, state, country, + short_name }; app.db.locations.findOne(query, (err, cur_location) => { if (err) return cb(err); if (cur_location) { + res.set(btp_id, cur_location._id); return cb(); } @@ -820,7 +912,7 @@ function integrate_locations(app, tournament_key, btp_state, callback) { if (cur_location) { //ADD BTP ID - app.db.locations.update(alt_query, { $set: { btp_id, name, address, postal_code, city, state, country, preperation_addition, meetingpoint_announcement} }, {}, (err) => cb(err)); + app.db.locations.update(alt_query, { $set: { btp_id, name, address, postal_code, city, state, country, preperation_addition, meetingpoint_announcement, short_name} }, {}, (err) => cb(err)); return; } @@ -848,9 +940,12 @@ function integrate_locations(app, tournament_key, btp_state, callback) { country, preperation_addition, meetingpoint_announcement, + short_name, highlight, }; + res.set(btp_id, location._id); + changed = true; app.db.locations.insert(location, (err) => cb(err)); }); @@ -864,10 +959,10 @@ function integrate_locations(app, tournament_key, btp_state, callback) { if (changed) { stournament.get_locations(app.db, tournament_key, function (err, all_locations) { admin.notify_change(app, tournament_key, 'location_changed', { all_locations }); - callback(err); + callback(err, res); }); } else { - callback(err); + callback(err, res); } }); } @@ -876,7 +971,7 @@ function integrate_locations(app, tournament_key, btp_state, callback) { // Returns a map btp_court_id => court._id -function integrate_courts(app, tournament_key, btp_state, callback) { +function integrate_courts(app, tournament_key, btp_state, location_map, callback) { const admin = require('./admin'); // avoid dependency cycle const stournament = require('./stournament'); // avoid dependency cycle @@ -886,7 +981,9 @@ function integrate_courts(app, tournament_key, btp_state, callback) { async.each(courts, (c, cb) => { const btp_id = c.ID[0]; const name = c.Name[0]; - const location_id = tournament_key + "_" + c.LocationID[0]; + const btp_location_id = c.LocationID[0]; + const location_id = location_map.get(btp_location_id); + assert(location_id); let num = parseInt(name, 10) || btp_id; const m = /^Court\s*([0-9]+)$/.exec(name); if (m) { @@ -943,10 +1040,10 @@ function integrate_courts(app, tournament_key, btp_state, callback) { if (changed) { stournament.get_courts(app.db, tournament_key, function (err, all_courts) { admin.notify_change(app, tournament_key, 'courts_changed', { all_courts }); - callback(err, res); + callback(err, location_map, res); }); } else { - callback(err, res); + callback(err, location_map, res); } }); } @@ -1268,8 +1365,8 @@ async function sync_btp_data(app, tkey, response) { cb => integrate_player_state(app, tkey, btp_state, cb), cb => integrate_umpires(app, tkey, btp_state, cb), cb => integrate_locations(app, tkey, btp_state, cb), - cb => integrate_courts(app, tkey, btp_state, cb), - (court_map, cb) => integrate_matches(app, tkey, btp_state, court_map, cb), + (location_map, cb) => integrate_courts(app, tkey, btp_state, location_map, cb), + (location_map, court_map, cb) => integrate_matches(app, tkey, btp_state, location_map, court_map, cb), cb => integrate_now_on_court(app, tkey, cb), cb => cleanup_entities(app, tkey, btp_state, cb), ], (err) => { diff --git a/bts/bupws.js b/bts/bupws.js index 20115e1..cd90ca7 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -152,6 +152,10 @@ async function handle_score_update(app, ws, msg) { send_error(ws, tournament_key, "Match not found or not on court actualy."); return; } + + console.log(match); + console.log(match.setup.teams); + const update = { network_score: score_data.network_score, network_team1_left:score_data.network_team1_left, diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 1c52dd0..f525d5d 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -4,6 +4,17 @@ margin: 10px 0px 10px 0px; } +.announce_button { + font-size: 100%; + padding: 9px 20px; + margin: 10px 0px 10px 0px; +} + +.announcements_btn_container { + display: flex; + justify-content: space-around; +} + .match_num { text-align: right; padding: 0 0.3em 0 0.3em; @@ -603,6 +614,14 @@ h3.section { background-color: #222222; } +.do_not_show { + display: none; +} + +.location { + color: #00000066; +} + .match_number_upcoming, .match_scheduled_upcoming, .match_event_upcoming { diff --git a/static/js/announcements.js b/static/js/announcements.js index 36a6490..651830c 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -1,7 +1,16 @@ - +function getLocationID(matchSetup) { + let location = null; + if(matchSetup.court_id) { + const court = utils.find(curt.courts, c => c._id === matchSetup.court_id); + location = utils.find(curt.locations, l => l._id === court.location_id); + } else if (matchSetup.preparation_location_id) { + location = utils.find(curt.locations, l => l._id === matchSetup.preparation_location_id); + } + return location._id; +} function announceNewMatch(matchSetup) { - if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + if(!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { return; } const field = createFieldAnnouncement(matchSetup); @@ -16,7 +25,7 @@ function announceNewMatch(matchSetup) { } function announcePreparationMatch(matchSetup) { - if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + if(!(window.localStorage.getItem('enable_announcement_preperations_' + getLocationID(matchSetup)) === 'true')) { return; } const field = createFieldPreparationAnnouncement(matchSetup); @@ -35,21 +44,21 @@ function announcePreparationMatch(matchSetup) { announce([preparation, field, matchNumber, eventName, round, teams, umpire, serviceJudge, tabletOperator, lastPart]); } function announceSecondCallTeamOne(matchSetup) { - if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + if(!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { return; } announceSecondCall(matchSetup, matchSetup.teams[0]); } function announceSecondCallTeamTwo(matchSetup) { - if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + if(!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { return; } announceSecondCall(matchSetup, matchSetup.teams[1]); } function announceSecondCallTabletoperator(matchSetup) { - if (!(window.localStorage.getItem('enable_announcements') === 'true')) { + if (!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { return; } const tabletOperatorCall = createTabletOperator(matchSetup);; @@ -59,7 +68,7 @@ function announceSecondCallTabletoperator(matchSetup) { } } function announceSecondCallUmpire(matchSetup) { - if (!(window.localStorage.getItem('enable_announcements') === 'true')) { + if (!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { return; } const umpireCall = createUmpire(matchSetup);; @@ -69,7 +78,7 @@ function announceSecondCallUmpire(matchSetup) { } } function announceSecondCallServiceJudge(matchSetup) { - if (!(window.localStorage.getItem('enable_announcements') === 'true')) { + if (!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { return; } const servicejudgeCall = createServiceJudge(matchSetup);; @@ -81,7 +90,7 @@ function announceSecondCallServiceJudge(matchSetup) { function announceSecondCall(matchSetup, team) { - if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + if(!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { return; } var secondCall = createSecondCallAnnouncement() + createSingleTeam(team.players); @@ -89,7 +98,7 @@ function announceSecondCall(matchSetup, team) { announce([secondCall, field]); } function announceBeginnToPlay(matchSetup, team) { - if(!(window.localStorage.getItem('enable_announcements') === 'true')) { + if(!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { return; } announce([createFieldAnnouncement(matchSetup) + ci18n('announcements:begin_to_play')]); @@ -282,8 +291,8 @@ function createMeetingPointAnnouncement(matchSetup) { return result; } -function announce(callArray) { - if(!(window.localStorage.getItem('enable_announcements') === 'true')) { +function announce(callArray, local) { + if(!(window.localStorage.getItem('enable_free_announcements') === 'true') && !local) { return; } diff --git a/static/js/cmatch.js b/static/js/cmatch.js index f505d5d..8315deb 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -205,6 +205,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ return; } + if (!court && match.setup.court_id) { court = curt.courts_by_id[match.setup.court_id]; } @@ -253,10 +254,25 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ const court_number_td = uiu.el(tr, 'td','court_number'); if(court) { uiu.el(court_number_td, 'span', 'court_history', court.num); + } else if (match.setup.location_id){ + const location = utils.find(curt.locations, l => l._id === match.setup.location_id); + uiu.el(court_number_td, 'span', 'location', "[" + location.short_name + "]"); + } + + if(match.setup.location_id) { + tr.setAttribute('data-location_id', match.setup.location_id); + } else { + tr.removeAttribute('data-location_id'); + } + + if (match.setup.location_id && !(window.localStorage.getItem('show_location_courts_' + match.setup.location_id) === 'true')) { + tr.classList.add('do_not_show'); + } else { + tr.classList.remove('do_not_show'); } - //uiu.el(tr, 'td', court ? 'court_history' : 'empty_court', court ? court.num : ''); } + if (style === 'plain') { const court_number_td = uiu.el(tr, "td", 'court_number'); if(!court) @@ -499,7 +515,9 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ if (style === 'unasigned' && completeMatch) { const locations = curt.locations; locations.forEach((l)=> { - create_match_prepparation_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id, l); + if(window.localStorage.getItem('show_location_courts_' + l._id) === 'true') { + create_match_prepparation_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id, l); + } }); } else if ((style === 'default' || style === 'plain') && court) { create_match_button(call_td, 'vlink match_manual_call_button', 'match:manualcall', on_announce_match_manually_button_click, match._id); @@ -697,6 +715,9 @@ function render_players_el(parentNode, setup, team_id, match, show_player_status } if (team.players.length > 0) { + if(team.entry_status !== ""){ + uiu.el(parentNode, 'span', {}, team.entry_status); + } render_player_el(parentNode, team.players[0], match._id, setup.now_on_court, show_player_status, style, team.players.length > 1 ? true : false); } else { @@ -1045,7 +1066,7 @@ function create_timer(timer_state, parent, default_color, exigent_color) { } else { tobj.timeout = null; el.style.display = "none"; - if(!curt.btp_settings.check_in_per_match) { + if(!curt.btp_settings.check_in_per_match && parent.querySelector('div.tablet_inline') === null) { parent.classList.remove("not_checked_in"); parent.classList.add("checked_in"); } @@ -1707,7 +1728,7 @@ function render_courts(container, style) { const expected_section = 'court_' + c._id; const court_matches = curt.matches.filter(m => calc_section(m) === expected_section); - const tr = uiu.el(tbody, 'tr', {class:"court_row", "data-court_id":c._id} ); + const tr = uiu.el(tbody, 'tr', {class:"court_row", "data-court_id":c._id, "data-location_id":c.location_id} ); const rowspan = Math.max(1, court_matches.length); //uiu.el(tr, 'th', { // 'class': 'court_num', @@ -1729,6 +1750,10 @@ function render_courts(container, style) { i++; } } + + if(!(window.localStorage.getItem('show_location_courts_' + c.location_id) === 'true')) { + tr.classList.add('do_not_show'); + } } if(style === 'public') { @@ -1736,6 +1761,19 @@ function render_courts(container, style) { } } +function update_tables(location_id, enabled) { + // Alle Elemente mit dem passenden data-location_id Attribut finden + const elements = document.querySelectorAll(`[data-location_id="${location_id}"]`); + + elements.forEach(el => { + if (enabled === true) { + el.classList.remove('do_not_show'); + } else { + el.classList.add('do_not_show'); + } + }); +} + function render_empty_court_row(tr, court, style, is_droppable) { tr.setAttribute("data-style", style); @@ -2340,7 +2378,8 @@ return { update_match, remove_match_from_gui, update_players, - create_timer + create_timer, + update_tables }; })(); diff --git a/static/js/ctournament.js b/static/js/ctournament.js index ea6e241..2e68d58 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -357,11 +357,43 @@ var ctournament = (function() { }); } + // function render_announcement_formular(target) { + // const announcements = uiu.el(target, 'div', 'announcements_container'); + // const heading = uiu.el(announcements, 'h3', {}, 'Freie Ansage'); + // const form = uiu.el(announcements, 'form'); + // uiu.el(form, 'textarea', { + // type: 'textarea', + // id: 'custom_announcement', + // name: 'custom_announcement', + // cols: '50', + // rows: '4', + // maxlength: '175' + // }); + // const btp_fetch_btn = uiu.el(form, 'button', { + // 'class': 'match_save_button', + // role: 'submit', + // }, 'Ansage abspielen'); + // form_utils.onsubmit(form, function (d) { + // //announce([d.custom_announcement]); + // send({ + // type: 'free_announce', + // tournament_key: curt.key, + // text: d.custom_announcement, + // }, function (err) { + // if (err) { + // return cerror.net(err); + // } + // }); + // }); + // } + function render_announcement_formular(target) { const announcements = uiu.el(target, 'div', 'announcements_container'); - const heading = uiu.el(announcements, 'h3', {}, 'Freie Ansage'); + uiu.el(announcements, 'h3', {}, 'Freie Ansage'); + const form = uiu.el(announcements, 'form'); - uiu.el(form, 'textarea', { + + const textarea = uiu.el(form, 'textarea', { type: 'textarea', id: 'custom_announcement', name: 'custom_announcement', @@ -369,16 +401,41 @@ var ctournament = (function() { rows: '4', maxlength: '175' }); - const btp_fetch_btn = uiu.el(form, 'button', { - 'class': 'match_save_button', - role: 'submit', - }, 'Ansage abspielen'); + + const btn_container = uiu.el(form, 'div', 'announcements_btn_container'); + + // Button: Lokal Abspielen + const local_btn = uiu.el(btn_container, 'button', { + type: 'button', + class: 'announce_button', + id: 'local_announce_btn' + }, 'Lokal Abspielen'); + + // Button: Remote Abspielen + const remote_btn = uiu.el(btn_container, 'button', { + type: 'submit', + class: 'announce_button', + id: 'remote_announce_btn' + }, 'Remote Abspielen'); + + // Lokales Abspielen (z. B. mit deiner announce-Funktion) + local_btn.addEventListener('click', function () { + const text = textarea.value.trim(); + if (!text) return; + + // Lokale Ansage abspielen + announce([text], true); // ← Diese Funktion muss bei dir lokal definiert sein + }); + + // Remote Abspielen form_utils.onsubmit(form, function (d) { - //announce([d.custom_announcement]); + const text = d.custom_announcement?.trim(); + if (!text) return; + send({ type: 'free_announce', tournament_key: curt.key, - text: d.custom_announcement, + text: text, }, function (err) { if (err) { return cerror.net(err); @@ -387,26 +444,129 @@ var ctournament = (function() { }); } - function render_enable_announcement(target) { - const announcements = uiu.el(target, 'div', 'enable_announcements_container'); - const heading = uiu.el(announcements, 'h3', {}, 'Ansagen auf diesem Gerät'); - const form = uiu.el(announcements, 'form'); - const enable_announcements = uiu.el(form, 'input', { - type: 'checkbox', - id: 'enable_announcements', - name: 'enable_announcements' + // function render_enable_announcement(target) { + // const announcements = uiu.el(target, 'div', 'enable_announcements_container'); + // const heading = uiu.el(announcements, 'h3', {}, 'Ansagen auf diesem Gerät'); + // const form = uiu.el(announcements, 'form'); + // const enable_announcements = uiu.el(form, 'input', { + // type: 'checkbox', + // id: 'enable_announcements', + // name: 'enable_announcements' + // }); + + // enable_announcements.checked = (window.localStorage.getItem('enable_announcements') === 'true'); + // uiu.el(form, 'label', { for: 'enable_announcements' }, 'aktiv'); + // enable_announcements.addEventListener('change', change_announcements); + // } + + // function change_announcements(e) { + // let enable_announcements = document.getElementById('enable_announcements'); + // window.localStorage.setItem('enable_announcements', enable_announcements.checked); + // } + + function render_enable_announcements(target, locations) { + const container = uiu.el(target, 'div', 'enable_announcements_container'); + uiu.el(container, 'h3', {}, 'Ansagen auf diesem Gerät'); + + locations.forEach(loc => { + { + const form = uiu.el(container, 'form'); + + const checkboxId = `enable_announcement_calls_${loc._id}`; + const checkbox = uiu.el(form, 'input', { + type: 'checkbox', + id: checkboxId, + name: checkboxId + }); + + // Initialer Zustand aus localStorage + checkbox.checked = (window.localStorage.getItem(checkboxId) === 'true'); + + // Label anzeigen mit dem Location-Namen + uiu.el(form, 'label', { for: checkboxId }, (loc.name || 'Unbenannte Location') + " (Spielaufruf)"); + + // Event Listener zum Speichern in localStorage + checkbox.addEventListener('change', function () { + window.localStorage.setItem(checkboxId, checkbox.checked); + }); + } + { + const form = uiu.el(container, 'form'); + + const checkboxId = `enable_announcement_preperations_${loc._id}`; + const checkbox = uiu.el(form, 'input', { + type: 'checkbox', + id: checkboxId, + name: checkboxId + }); + + // Initialer Zustand aus localStorage + checkbox.checked = (window.localStorage.getItem(checkboxId) === 'true'); + + // Label anzeigen mit dem Location-Namen + uiu.el(form, 'label', { for: checkboxId }, (loc.name || 'Unbenannte Location') + " (in Vorbereitung)"); + + // Event Listener zum Speichern in localStorage + checkbox.addEventListener('change', function () { + window.localStorage.setItem(checkboxId, checkbox.checked); + }); + } }); - enable_announcements.checked = (window.localStorage.getItem('enable_announcements') === 'true'); - uiu.el(form, 'label', { for: 'enable_announcements' }, 'aktiv'); - enable_announcements.addEventListener('change', change_announcements); + { + const form = uiu.el(container, 'form'); + + const checkboxId = 'enable_free_announcements'; + const checkbox = uiu.el(form, 'input', { + type: 'checkbox', + id: checkboxId, + name: checkboxId + }); + + // Initialer Zustand aus localStorage + checkbox.checked = (window.localStorage.getItem(checkboxId) === 'true'); + + // Label anzeigen mit dem Location-Namen + uiu.el(form, 'label', { for: checkboxId }, 'Freie Remote Ansagen'); + + // Event Listener zum Speichern in localStorage + checkbox.addEventListener('change', function () { + window.localStorage.setItem(checkboxId, checkbox.checked); + }); + } } - function change_announcements(e) { - let enable_announcements = document.getElementById('enable_announcements'); - window.localStorage.setItem('enable_announcements', enable_announcements.checked); + function render_enable_location_courts(target, locations) { + const container = uiu.el(target, 'div', 'enable_announcements_container'); + uiu.el(container, 'h3', {}, 'Zeige Felder'); + + locations.forEach(loc => { + const form = uiu.el(container, 'form'); + + const checkboxId = `show_location_courts_${loc._id}`; + const checkbox = uiu.el(form, 'input', { + type: 'checkbox', + id: checkboxId, + name: checkboxId + }); + + // Initialer Zustand aus localStorage oder Default auf true + const storedValue = window.localStorage.getItem(checkboxId); + checkbox.checked = (storedValue === null) ? true : (storedValue === 'true'); + + // Label anzeigen mit dem Location-Namen + uiu.el(form, 'label', { for: checkboxId }, loc.name + " ["+ loc.short_name +"]" || 'Unbenannte Location'); + + // Event Listener zum Speichern in localStorage und Aufruf mit Parametern + checkbox.addEventListener('change', function () { + window.localStorage.setItem(checkboxId, checkbox.checked); + cmatch.update_tables(loc._id, checkbox.checked); + }); + + // Gleich initial einmal aufrufen, damit der Sichtbarkeitszustand korrekt gesetzt ist + cmatch.update_tables(loc._id, checkbox.checked); + }); } - function ui_show() { crouting.set('t/:key/', { key: curt.key }); const bup_lang = ((curt.language && curt.language !== 'auto') ? '&lang=' + encodeURIComponent(curt.language) : ''); @@ -444,10 +604,14 @@ var ctournament = (function() { uiu.el(meta_div, 'div', 'umpire_container'); render_announcement_formular(meta_div); - const meta_right_div = uiu.el(meta_div, 'div', 'metadata_right_container'); - render_enable_announcement(meta_right_div); + render_enable_announcements(meta_div, curt.locations); + + const meta_right_div = uiu.el(meta_div, 'div', 'metadata_right_container'); + + render_enable_location_courts(meta_right_div, curt.locations); + render_settings(meta_right_div); cmatch.prepare_render(curt); From 5dd074ed4bd9b8491def2ff29a6c660692cdd5b8 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 5 Jun 2025 09:20:09 +0200 Subject: [PATCH 199/224] Ensure that old databases are deletet while reset the tournament --- blank_tournament/courts | 9 ---- .../display_court_displaysettings | 47 ++++++++----------- blank_tournament/displaysettings | 5 +- blank_tournament/event | 0 4 files changed, 21 insertions(+), 40 deletions(-) delete mode 100644 blank_tournament/courts delete mode 100644 blank_tournament/event diff --git a/blank_tournament/courts b/blank_tournament/courts deleted file mode 100644 index 84f822b..0000000 --- a/blank_tournament/courts +++ /dev/null @@ -1,9 +0,0 @@ -{"_id":"default_1","tournament_key":"default","btp_id":9,"num":1,"name":"1","match_id":"btp_default_U17 MD_545"} -{"_id":"default_2","tournament_key":"default","btp_id":10,"num":2,"name":"2","match_id":"btp_default_U19 JE_654"} -{"_id":"default_3","tournament_key":"default","btp_id":11,"num":3,"name":"3","match_id":"btp_default_U19 JE_649"} -{"_id":"default_4","tournament_key":"default","btp_id":12,"num":4,"name":"4","match_id":"btp_default_U19 ME_715"} -{"_id":"default_5","tournament_key":"default","btp_id":13,"num":5,"name":"5","match_id":"btp_default_U17 JD_467"} -{"_id":"default_6","tournament_key":"default","btp_id":14,"num":6,"name":"6","match_id":"btp_default_U17 JD_469"} -{"_id":"default_7","tournament_key":"default","btp_id":15,"num":7,"name":"7","match_id":"btp_default_U17 JE_13"} -{"_id":"default_8","tournament_key":"default","btp_id":16,"num":8,"name":"8","match_id":"btp_default_U19 JE_653"} -{"$$indexCreated":{"fieldName":"tournament_key","unique":false,"sparse":false}} diff --git a/blank_tournament/display_court_displaysettings b/blank_tournament/display_court_displaysettings index 54e339d..a2853cf 100644 --- a/blank_tournament/display_court_displaysettings +++ b/blank_tournament/display_court_displaysettings @@ -1,28 +1,19 @@ -{"client_id":"253","court_id":"default_2","displaysetting_id":"default","_id":"M3VrcixFrRE51rMY"} -{"client_id":"130","court_id":"default_5","displaysetting_id":"default","_id":"M3VrcixFrRE52rMY"} -{"client_id":"101","court_id":"default_3","displaysetting_id":"nur_spieler","_id":"bSSxTeYudRTkf1Ni"} -{"client_id":"102","court_id":"default_2","displaysetting_id":"default","_id":"bSSxTeYudRTkf2Ni"} -{"client_id":"103","court_id":"default_3","displaysetting_id":"nur_spieler","_id":"bSSxTeYudRTkf3Ni"} -{"client_id":"104","court_id":"default_3","displaysetting_id":"default","_id":"bSSxTeYudRTkf4Ni"} -{"client_id":"105","court_id":"default_4","displaysetting_id":"nur_spieler","_id":"bSSxTeYudRTkf5Ni"} -{"client_id":"106","court_id":"default_5","displaysetting_id":"default","_id":"bSSxTeYudRTkf6Ni"} -{"client_id":"107","court_id":"default_5","displaysetting_id":"default","_id":"bSSxTeYudRTkf7Ni"} -{"client_id":"108","court_id":"default_6","displaysetting_id":"default","_id":"bSSxTeYudRTkf8Ni"} -{"client_id":"109","court_id":"default_7","displaysetting_id":"default","_id":"bSSxTeYudRTkf9Ni"} -{"client_id":"110","court_id":"default_8","displaysetting_id":"default","_id":"bSSxTeYudRTkg1Ni"} -{"client_id":"111","court_id":"default_3","displaysetting_id":"default","_id":"bSSxTeYudRTkg2Ni"} -{"client_id":"112","court_id":"default_4","displaysetting_id":"default","_id":"bSSxTeYudRTkg3Ni"} -{"client_id":"113","court_id":"default_5","displaysetting_id":"default","_id":"bSSxTeYudRTkg4Ni"} -{"client_id":"114","court_id":"default_6","displaysetting_id":"default","_id":"bSSxTeYudRTkg5Ni"} -{"client_id":"120","court_id":"default_7","displaysetting_id":"default","_id":"bSSxTeYudRTkg6Ni"} -{"client_id":"121","court_id":"default_8","displaysetting_id":"default","_id":"bSSxTeYudRTkg7Ni"} -{"client_id":"130","court_id":"default_8","displaysetting_id":"default","_id":"bSSxTeYudRTkg8Ni"} -{"client_id":"131","court_id":"default_1","displaysetting_id":"default","_id":"bSSxTeYudRTkg9Ni"} -{"client_id":"132","court_id":"default_2","displaysetting_id":"default","_id":"bSSxTeYudRTkh1Ni"} -{"client_id":"133","court_id":"default_3","displaysetting_id":"default","_id":"bSSxTeYudRTkh2Ni"} -{"client_id":"134","court_id":"default_4","displaysetting_id":"default","_id":"bSSxTeYudRTkh3Ni"} -{"client_id":"135","court_id":"default_5","displaysetting_id":"default","_id":"bSSxTeYudRTkh4Ni"} -{"client_id":"136","court_id":"default_6","displaysetting_id":"default","_id":"bSSxTeYudRTkh5Ni"} -{"client_id":"137","court_id":"default_7","displaysetting_id":"default","_id":"bSSxTeYudRTkh6Ni"} -{"client_id":"138","court_id":"default_8","displaysetting_id":"default","_id":"bSSxTeYudRTkh7Ni"} -{"client_id":"139","court_id":"default_9","displaysetting_id":"default","_id":"bSSxTeYudRTkh8Ni"} \ No newline at end of file +{"client_id":"201","hostname":"Android-5","court_id":"default_1","displaysetting_id":"default_1 _1746279411442","_id":"4fOwaXIid6Aby102"} +{"client_id":"163","hostname":"display03","court_id":"default_3","displaysetting_id":"default_1 _1746279411442","_id":"58ko1WCwNVgeXZez"} +{"client_id":"209","hostname":"Tablet09","court_id":null,"displaysetting_id":"default_1 _1746279411442","_id":"AjeZXlFDktXuGuDV"} +{"client_id":"162","hostname":"display02","court_id":"default_2","displaysetting_id":"default_1 _1746279411442","_id":"Ar4cbZOWOnexWqbT"} +{"client_id":"168","hostname":"display08","court_id":"default_8","displaysetting_id":"default_1 _1746279411442","_id":"FqQdFy0ZZhrjz5jy"} +{"client_id":"166","hostname":"display06","court_id":"default_6","displaysetting_id":"default_1 _1746279411442","_id":"G0kDkTUNLlfHIBxU"} +{"client_id":"211","hostname":"Tablet11","court_id":null,"displaysetting_id":"default_1 _1746279411442","_id":"J0JXHgZh9tJbQHML"} +{"client_id":"161","hostname":"display01","court_id":"default_1","displaysetting_id":"default_1 _1746279411442","_id":"KAm9UyBesxxEQwCH"} +{"client_id":"204","hostname":"Android-2","court_id":"default_4","displaysetting_id":"default_1 _1746279411442","_id":"LruIunhfT22Pif01"} +{"client_id":"203","hostname":"Android","court_id":"default_3","displaysetting_id":"default_1 _1746279411442","_id":"O7GfGFfRtWP0bZ9E"} +{"client_id":"207","hostname":"Tablet07","court_id":"default_7","displaysetting_id":"default_1 _1746279411442","_id":"T0uPyYbLu7ZEbwTE"} +{"client_id":"212","hostname":"Tablet12","court_id":null,"displaysetting_id":"default_1 _1746279411442","_id":"X6DKVYegQLVFQulX"} +{"client_id":"202","hostname":"Android","court_id":"default_2","displaysetting_id":"default_1 _1746279411442","_id":"XR8v0Vgyrd0uI7Gb"} +{"client_id":"210","hostname":"Tablet10","court_id":null,"displaysetting_id":"default_1 _1746279411442","_id":"iKUjQjaauJzh45me"} +{"client_id":"164","hostname":"display04","court_id":"default_4","displaysetting_id":"default_1 _1746279411442","_id":"jBEO6pSWvkOFvsKV"} +{"client_id":"206","hostname":"Android-3","court_id":"default_6","displaysetting_id":"default_1 _1746279411442","_id":"o7pbXdlGLU8HYxtQ"} +{"client_id":"205","hostname":"Android-4","court_id":"default_5","displaysetting_id":"default_1 _1746279411442","_id":"qDVxBuPVBF0tTUXp"} +{"client_id":"165","hostname":"display05","court_id":"default_5","displaysetting_id":"default_1 _1746279411442","_id":"qFktmzgZP4RLWyBC"} +{"client_id":"208","hostname":"Tablet08","court_id":"default_8","displaysetting_id":"default_1 _1746279411442","_id":"uw6f1RQOLqBPCVOI"} diff --git a/blank_tournament/displaysettings b/blank_tournament/displaysettings index 516d955..462080e 100644 --- a/blank_tournament/displaysettings +++ b/blank_tournament/displaysettings @@ -1,3 +1,2 @@ -{"id":"nur_spieler","displaymode_style":"onlyplayers","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_2","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_2","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"style":"clean","_id":"9t2vgoCBMN0Rs5kA"} -{"id":"nur_spielstand","displaymode_style":"onlyscore","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_2","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_2","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"style":"clean","_id":"eddY0hYOZPvnQxMX"} -{"id":"default","displaymode_style":"tournamentcourt","fullscreen_ask":"never","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_7","court_description":"","network_timeout":10000,"network_update_interval":10000,"displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":100,"d_team_colors":true,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":1200,"negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_court_id":"default_7","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"style":"clean","_id":"fKYy03Qs6635VWRO"} +{"fullscreen_ask":"auto","show_announcements":"all","umpire_name":"","service_judge_name":"","court_id":"default_1","court_description":"","network_timeout":"10000","network_update_interval":"10000","displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":"100","d_team_colors":false,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":"1200","negative_timers":false,"shuttle_counter":true,"language":"de","editmode_doubleclick":false,"displaymode_style":"tournamentcourt","displaymode_court_id":"default_1","wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"settings_style":"complete","id":"default_default_1 _1746279900198","_id":"hsBi45n92LaUndK1","advertisements":[],"description":"TV","devicemode":"display","d_cfg4":"#000000","style":"complete"} +{"fullscreen_ask":"never","show_announcements":"none","umpire_name":"","service_judge_name":"","court_id":1,"court_description":"","network_timeout":"10000","network_update_interval":"10000","displaymode_update_interval":500,"d_c0":"#50e87d","d_cb0":"#000000","d_c1":"#f76a23","d_cb1":"#000000","d_cbg":"#000000","d_cfg":"#ffffff","d_cfgdark":"#000000","d_cbg2":"#d9d9d9","d_cbg3":"#252525","d_cbg4":"#404040","d_cfg2":"#aaaaaa","d_cfg3":"#cccccc","d_cexp":"#ff0000","d_cborder":"#444444","d_ct":"#80ff00","d_ctim_blue":"#0070c0","d_ctim_active":"#ffc000","d_cserv":"#fff200","d_cserv2":"#dba766","d_crecv":"#707676","d_scale":"100","d_team_colors":false,"d_show_pause":true,"d_show_court_number":true,"d_show_competition":true,"d_show_round":true,"d_show_middle_name":false,"d_show_doubles_receiving":false,"settings_autohide":30000,"dads_interval":20000,"dads_wait":60000,"dads_dtime":10000,"dads_atime":10000,"dads_utime":"14:00","dads_mode":"none","double_click_timeout":1000,"button_block_timeout":"1000","negative_timers":true,"shuttle_counter":false,"language":"de","editmode_doubleclick":true,"displaymode_style":"tournamentcourt","displaymode_court_id":1,"wakelock":"display","click_mode":"auto","refmode_client_enabled":false,"refmode_client_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_referee_ws_url":"wss://live.aufschlagwechsel.de/refmode_hub/","refmode_client_node_name":"","referee_service_judges":false,"settings_style":"default","id":"default_1 _1746279411442","_id":"okgn30wBUbGHQ5YX","description":"Tablet","devicemode":"umpire","d_cfg4":"#000000","style":"hidden"} diff --git a/blank_tournament/event b/blank_tournament/event deleted file mode 100644 index e69de29..0000000 From 6d00da54b81e3dd313c40b9769e9f646c9242432 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sun, 6 Jul 2025 02:28:18 +0200 Subject: [PATCH 200/224] Fix: The player is now also synchronized by btp if no club or district is set. --- bts/btp_sync.js | 120 +++++++++++++++++++++++++----------------------- 1 file changed, 62 insertions(+), 58 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index bc95861..bfc2b7e 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -311,65 +311,69 @@ function _craft_team(par) { pres.checked_in = p.CheckedIn[0]; } - const club = this.clubs.get(p.ClubID[0]); - const district = this.districts.get(club.DistrictID[0]); - const state_by_district = district.Name[0].split("-")[0]; - - var state = (state_by_district ? state_by_district : (p.State && p.Satate.length > 0 ? p.State[0] : undefined)); - if (state) { - switch (state) { - case 'BAW' : { - pres.state = "Baden-Württemberg"; - break; - } case 'BAY' : { - pres.state = "Bayern"; - break; - } case 'BBB': { - pres.state = "Berlin-Brandenburg"; - break; - } case 'BRE': { - pres.state = "Bremen"; - break; - } case 'HAM': { - pres.state = "Hamburg"; - break; - } case 'HES': { - pres.state = "Hessen"; - break; - } case 'MVP': { - pres.state = "Mecklenburg-Vorpommern"; - break; - } case 'NIS': { - pres.state = "Niedersachsen"; - break; - } case 'NRW': { - pres.state = "Nordrhein-Westfalen"; - break; - } case 'RHP': { - pres.state = "Rheinhessen-Pfalz"; - break; - } case 'RHL': { - pres.state = "Rheinland"; - break; - } case 'SAA': { - pres.state = "Saarland"; - break; - } case 'SAC': { - pres.state = "Sachsen"; - break; - } case 'SAH': { - pres.state = "Sachsen-Anhalt"; - break; - } case 'SLH': { - pres.state = "Schleswig-Holstein"; - break; - } case 'THÜ': { - pres.state = "Thüringen"; - break; - } - default: - pres.state = state + try{ + const club = this.clubs.get(p.ClubID[0]); + const district = this.districts.get(club.DistrictID[0]); + const state_by_district = district.Name[0].split("-")[0]; + + var state = (state_by_district ? state_by_district : (p.State && p.Satate.length > 0 ? p.State[0] : undefined)); + if (state) { + switch (state) { + case 'BAW' : { + pres.state = "Baden-Württemberg"; + break; + } case 'BAY' : { + pres.state = "Bayern"; + break; + } case 'BBB': { + pres.state = "Berlin-Brandenburg"; + break; + } case 'BRE': { + pres.state = "Bremen"; + break; + } case 'HAM': { + pres.state = "Hamburg"; + break; + } case 'HES': { + pres.state = "Hessen"; + break; + } case 'MVP': { + pres.state = "Mecklenburg-Vorpommern"; + break; + } case 'NIS': { + pres.state = "Niedersachsen"; + break; + } case 'NRW': { + pres.state = "Nordrhein-Westfalen"; + break; + } case 'RHP': { + pres.state = "Rheinhessen-Pfalz"; + break; + } case 'RHL': { + pres.state = "Rheinland"; + break; + } case 'SAA': { + pres.state = "Saarland"; + break; + } case 'SAC': { + pres.state = "Sachsen"; + break; + } case 'SAH': { + pres.state = "Sachsen-Anhalt"; + break; + } case 'SLH': { + pres.state = "Schleswig-Holstein"; + break; + } case 'THÜ': { + pres.state = "Thüringen"; + break; + } + default: + pres.state = state + } } + } catch (error) + { } fix_player(pres); return pres; From d6100911ea98dd7d31f1e0fcb737b433d936f8c0 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 29 Aug 2025 13:52:58 +0200 Subject: [PATCH 201/224] ensure that the courts are visible while first loading --- static/js/cmatch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 8315deb..670625b 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -265,7 +265,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ tr.removeAttribute('data-location_id'); } - if (match.setup.location_id && !(window.localStorage.getItem('show_location_courts_' + match.setup.location_id) === 'true')) { + if (match.setup.location_id && !(window.localStorage.getItem('show_location_courts_' + match.setup.location_id) !== 'false')) { tr.classList.add('do_not_show'); } else { tr.classList.remove('do_not_show'); @@ -515,7 +515,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ if (style === 'unasigned' && completeMatch) { const locations = curt.locations; locations.forEach((l)=> { - if(window.localStorage.getItem('show_location_courts_' + l._id) === 'true') { + if(window.localStorage.getItem('show_location_courts_' + l._id) !== 'false') { create_match_prepparation_button(call_td, 'vlink match_preparation_call_button', 'match:preparationcall', on_announce_preparation_matchbutton_click, match._id, l); } }); @@ -1751,7 +1751,7 @@ function render_courts(container, style) { } } - if(!(window.localStorage.getItem('show_location_courts_' + c.location_id) === 'true')) { + if(!(window.localStorage.getItem('show_location_courts_' + c.location_id) !== 'false')) { tr.classList.add('do_not_show'); } } From b3d8580516c5577cc513147ca0ba051dd101e4fb Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 30 Aug 2025 01:32:10 +0200 Subject: [PATCH 202/224] better support for multi locations --- bts/admin.js | 31 ++++++++++++++++++++++ bts/match_utils.js | 2 +- static/css/cmatch.css | 17 +++++++++--- static/js/announcements.js | 53 +++++++++++++++++++++++++++++++++----- static/js/change.js | 9 +++++++ static/js/cmatch.js | 50 +++++++++++++++++++++++++++++++++-- 6 files changed, 150 insertions(+), 12 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index bd89af3..abfb6a7 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -394,6 +394,7 @@ function _extract_setup(msg_setup) { 'scheduled_date', 'called_timestamp', 'preparation_call_timestamp', + 'location_id', 'teams', 'team_competition', 'tabletoperators', @@ -921,6 +922,20 @@ function handle_second_call_team_one(app, ws, msg) { ws.respond(msg); } + +function handle_second_preperation_call_team_one(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + notify_change(app, tournament_key, 'second_preperation_call_team_one', {setup}); + + ws.respond(msg); +} + function handle_display_delete(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'display_client_id'])) { return; @@ -1032,6 +1047,20 @@ function handle_second_call_team_two(app, ws, msg) { } +function handle_second_preperation_call_team_two(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + notify_change(app, tournament_key, 'second_preperation_call_team_two', {setup}); + + ws.respond(msg); +} + + async function async_handle_match_delete(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'id'])) { return; @@ -1258,6 +1287,8 @@ module.exports = { handle_second_call_tabletoperator, handle_second_call_team_one, handle_second_call_team_two, + handle_second_preperation_call_team_one, + handle_second_preperation_call_team_two, handle_tournament_get, handle_tournament_list, handle_tournament_edit_props, diff --git a/bts/match_utils.js b/bts/match_utils.js index 11f511b..a6ea405 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -123,7 +123,7 @@ function add_preparation_call_timestamp(db, tournament_key, setup, location_id) for (const location of all_locations) { if (location._id == location_id) { setup.highlight = location.highlight; - setup.preparation_location_id = location_id; + setup.location_id = location_id; setup.preparation_call_timestamp = Date.now(); setup.state = 'preparation'; resolve(setup); diff --git a/static/css/cmatch.css b/static/css/cmatch.css index f525d5d..09d72bb 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -26,6 +26,7 @@ .match_begin_to_play_button, .match_manual_call_button, .match_second_call_button, +.match_second_preperation_call_button, .match_rawinfo { display: inline-block; min-width: 1em; @@ -63,7 +64,8 @@ background-image: url(../icons/start.svg); } -.match_second_call_button { +.match_second_call_button, +.match_second_preperation_call_button { height: 1.2em; min-height: 1em; background-size: cover; @@ -98,6 +100,7 @@ .match_edit_button:hover, .match_second_call_button:hover, +.match_second_preperation_call_button:hover, .match_preparation_call_button:hover, .match_begin_to_play_button:hover, .match_manual_call_button:hover, @@ -314,6 +317,7 @@ match_preparation_call_button, match_begin_to_play_button, match_second_call_button, +match_second_preperation_call_button, match_manual_call_button, .match_scoresheet_buttons { position: fixed; @@ -336,11 +340,13 @@ match_manual_call_button, min-width: 5em; } -.match_second_call_button > * { +.match_second_call_button > * , +.match_second_preperation_call_button > * { display: block; } -.match_second_call_button > button { +.match_second_call_button > button, +.match_second_preperation_call_button > button { margin: 0 0 0 2vmin; font-size: 5vmin; padding: 0.1em 0.1em; @@ -520,6 +526,11 @@ match_manual_call_button, background-color: #c56bff; } +.preperation_container { + display: flex; + flex-direction: column; +} + .preperation { font-weight: bold; text-wrap: nowrap; diff --git a/static/js/announcements.js b/static/js/announcements.js index 651830c..0446bd6 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -3,8 +3,9 @@ function getLocationID(matchSetup) { if(matchSetup.court_id) { const court = utils.find(curt.courts, c => c._id === matchSetup.court_id); location = utils.find(curt.locations, l => l._id === court.location_id); - } else if (matchSetup.preparation_location_id) { - location = utils.find(curt.locations, l => l._id === matchSetup.preparation_location_id); + } else if (matchSetup.location_id) { + location = utils.find(curt.locations, l => l._id === matchSetup.location_id); + } return location._id; } @@ -57,6 +58,20 @@ function announceSecondCallTeamTwo(matchSetup) { announceSecondCall(matchSetup, matchSetup.teams[1]); } +function announceSecondPreperationCallTeamOne(matchSetup) { + if(!(window.localStorage.getItem('enable_announcement_preperations_' + getLocationID(matchSetup)) === 'true')) { + return; + } + announceSecondPreperationCall(matchSetup, matchSetup.teams[0]); +} + +function announceSecondPreperationCallTeamTwo(matchSetup) { + if(!(window.localStorage.getItem('enable_announcement_preperations_' + getLocationID(matchSetup)) === 'true')) { + return; + } + announceSecondPreperationCall(matchSetup, matchSetup.teams[1]); +} + function announceSecondCallTabletoperator(matchSetup) { if (!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { return; @@ -97,6 +112,32 @@ function announceSecondCall(matchSetup, team) { var field = createFieldAnnouncement(matchSetup); announce([secondCall, field]); } + +function announceSecondPreperationCall(matchSetup, team) { + if(!(window.localStorage.getItem('enable_announcement_preperations_' + getLocationID(matchSetup)) === 'true')) { + return; + } + var secondCall = createSecondCallAnnouncement() + createSingleTeam(team.players) + " in Vorbereitung"; + if(matchSetup.location_id) { + const l = utils.find(curt.locations, l => l._id === matchSetup.location_id); + if(l) { + secondCall += ' ' + l.preperation_addition; + } + } + secondCall += "!"; + var callUs = createSingleTeam(team.players); + if (curt.preparation_meetingpoint_enabled) { + var meetingPoint = createMeetingPointAnnouncement(matchSetup); + if(team.players.length == 1) + { + meetingPoint = meetingPoint.replace("meldet euch", "melde dich"); + } + + callUs += ' ' + meetingPoint; + } + announce([secondCall, callUs]); +} + function announceBeginnToPlay(matchSetup, team) { if(!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { return; @@ -270,8 +311,8 @@ function createFieldPreparationAnnouncement(matchSetup) { function createPreparationAnnouncement(matchSetup) { let addition = ""; - if(matchSetup.preparation_location_id) { - const l = utils.find(curt.locations, l => l._id === matchSetup.preparation_location_id); + if(matchSetup.location_id) { + const l = utils.find(curt.locations, l => l._id === matchSetup.location_id); if(l) { addition = ' ' + l.preperation_addition; } @@ -281,8 +322,8 @@ function createPreparationAnnouncement(matchSetup) { function createMeetingPointAnnouncement(matchSetup) { let result = ci18n('announcements:meetingpoint'); - if(matchSetup.preparation_location_id) { - const l = utils.find(curt.locations, l => l._id === matchSetup.preparation_location_id); + if(matchSetup.location_id) { + const l = utils.find(curt.locations, l => l._id === matchSetup.location_id); if(l) { result = l.meetingpoint_announcement; } diff --git a/static/js/change.js b/static/js/change.js index 99011fb..352fe9e 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -187,6 +187,9 @@ function default_handler(rerender, special_funcs) { break; case 'match_preparation_call': announcePreparationMatch(c.val.match.setup); + curt.matches.forEach((match) => { + match.setup.location_id = c.val.location_id; + }); ctournament.update_match(c); ctournament.update_upcoming_match(c); break; @@ -211,6 +214,12 @@ function default_handler(rerender, special_funcs) { case 'second_call_team_two': announceSecondCallTeamTwo(c.val.setup); break; + case 'second_preperation_call_team_one': + announceSecondPreperationCallTeamOne(c.val.setup); + break; + case 'second_preperation_call_team_two': + announceSecondPreperationCallTeamTwo(c.val.setup); + break; case 'btp_status': ctournament.btp_status_changed(c); break; diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 670625b..e4116be 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -327,6 +327,10 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } else { create_match_button(players0, 'vlink match_second_call_button', 'match:secondcallteamone', on_second_call_team_one_button_click, match._id); } + + if(style === 'unasigned' && match.setup.highlight >= 1){ + create_match_button(players0, 'vlink match_second_preperation_call_button', 'match:secondcallteamone', on_second_preperation_call_team_one_button_click, match._id); + } } render_players_el(players0, setup, 0, match, show_player_status, style); @@ -339,6 +343,10 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ render_players_el(players1, setup, 1, match, show_player_status, style); if (style === 'default' || style === 'plain' || style === 'unasigned') { + if(style === 'unasigned' && match.setup.highlight >= 1){ + create_match_button(players1, 'vlink match_second_preperation_call_button', 'match:secondcallteamtwo', on_second_preperation_call_team_two_button_click, match._id); + } + if (show_add_tabletoperator) { if (setup.teams[1].players.length > 0) { create_match_button(players1, 'vlink tabletoperator_add_button', 'tabletoperator:add', on_add_to_tabletoperators_team_two_button_click, match._id); @@ -410,8 +418,17 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ uiu.el(no_umpire_span, 'span', 'match_no_umpire', ci18n('No umpire')); } - } else if(style === 'upcoming' && setup.highlight == 6) { - uiu.el(to_td, 'span', 'preperation', 'Spiel in Vorbereitung!'); + } else if(style === 'upcoming' && setup.highlight >= 1) { + var preperation_container = uiu.el(to_td, 'div', 'preperation_container'); + uiu.el(preperation_container, 'span', 'preperation', 'in Vorbereitung' + (setup.location_id ? "" : "!")); + + if(setup.location_id) { + const l = utils.find(curt.locations, l => l._id === setup.location_id); + if(l) { + uiu.el(preperation_container, 'span', 'preperation', l.preperation_addition); //TODO: Hie die Halle mit ausgeben! + } + } + } } @@ -1179,6 +1196,7 @@ function on_scoresheet_button_click(e) { function on_announce_preparation_matchbutton_click(e) { const match = fetchMatchFromEvent(e); const location = fetchLocationFromEvent(e); + if (match != null && location != null) { send({ type: 'match_preparation_call', @@ -1230,6 +1248,34 @@ function on_second_call_team_two_button_click(e) { } }); } +} +function on_second_preperation_call_team_one_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_preperation_call_team_one', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +} +function on_second_preperation_call_team_two_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_preperation_call_team_two', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } } function on_second_call_tabletoperator_button_click(e) { const match = fetchMatchFromEvent(e); From ec7235e4e340a4dec929f047e5f7a22ac87bfc88 Mon Sep 17 00:00:00 2001 From: tengmel Date: Sun, 28 Sep 2025 19:41:35 +0200 Subject: [PATCH 203/224] Integration of language support for Dutch --- static/cbts.html | 1 + static/js/ci18n.js | 2 + static/js/ci18n_nl.js | 268 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 271 insertions(+) create mode 100644 static/js/ci18n_nl.js diff --git a/static/cbts.html b/static/cbts.html index 18bceb4..f55b036 100644 --- a/static/cbts.html +++ b/static/cbts.html @@ -56,6 +56,7 @@ + diff --git a/static/js/ci18n.js b/static/js/ci18n.js index f59833a..40b827a 100644 --- a/static/js/ci18n.js +++ b/static/js/ci18n.js @@ -33,6 +33,7 @@ function detect_lang() { function register_all() { register_lang(ci18n_en); register_lang(ci18n_de); + register_lang(ci18n_nl); } function init() { @@ -108,6 +109,7 @@ if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var utils = require('../bup/dev/js/utils'); var ci18n_de = require('./ci18n_de'); var ci18n_en = require('./ci18n_en'); + var ci18n_en = require('./ci18n_nl'); ci18n.register_all(); module.exports = ci18n; diff --git a/static/js/ci18n_nl.js b/static/js/ci18n_nl.js new file mode 100644 index 0000000..0c65fdc --- /dev/null +++ b/static/js/ci18n_nl.js @@ -0,0 +1,268 @@ +var ci18n_nl = { + + '_code': 'nl', + '_name': 'Nederlands', + + 'Unassigned Matches': 'Niet Toegewezen Wedstrijden', + 'Next Matches': 'Volgende Wedstrijden', + 'Current Matches': 'Huidige Wedstrijden', + 'Matchoverview': 'Overzicht Wedstrijden', + 'Scoreboard': 'Scorebord', + 'Umpire Panel': 'Scheidsrechterpaneel', + 'edit tournament': 'wijzig toernooi', + 'Court': 'Baan', + 'Match': 'Wedstrijd', + 'Players': 'Spelers', + 'Umpire': 'Scheidsrechter', + 'State': 'Status', + 'Umpire:': 'Scheidsrechter:', + 'Service judge:': 'Service rechter:', + 'Finished Matches': 'Beëindigde Wedstrijden', + 'Time:': 'Tijd:', + '(Singles)': '(Enkelspel)', + 'e.g. MX O55': 'bijv. MX O55', + 'e.g. semi-finals': 'bijv. halve finales', + 'Number:': 'Nummer:', + 'Cancel': 'Annuleer', + 'Change': 'Muteer', + 'Confirm_Finish': 'Bevestig Einde Wedstrijd', + 'No umpire': 'Geen scheidsrechter', + 'No service judge': 'Geen servicerechter', + 'Edit match': 'Aanpassen wedstrijd', + 'Edit display setting': 'Aanpassen weergave instellingen', + 'Back': 'Terug', + 'PDF': 'PDF', + 'Print': 'Druk Af', + 'Add Match': 'Voeg Wedstrijd Toe', + ' Ready to start ': ' Klaar om te beginnen ', + 'Ready': 'Klaar', + 'Not assigned': 'Niet toegewezen', + 'team competition': 'competitie', + 'nation competition': 'landen toerooi', + 'update from BTP': 'Actualiseer BTP', + 'update ticker': 'Actualiseer Ticker', + 'Tournaments': 'Tournooien', + 'referee view': 'Referee weergave', + 'Connecting ...': 'Verbinden...', + 'Connected': 'Verbonden', + 'Connection lost': 'Verbinding verloren', + 'Create tournament': 'Creëer toernooi', + 'create:id:label': 'tournooi ID (allemaal kleine letterd, geen spaties):', + 'experimental': '(experimenteel)', + 'Winner': 'Winnaar', + 'Loser': 'Verliezer', + 'activate_court': 'Baan gebruiken', + 'inactivate_court': 'Baan blokkeren', + + 'announcements:begin_to_play': 'Speel!', + 'announcements:second_call': '"Tweede oproep voor:"', + 'announcements:vs': ' tegen ', + 'announcements:counting_board_service': 'Scorebordbediening:', + 'announcements:table_service': 'Tabletbediening:', + 'announcements:umpire': 'Scheidsrechter:', + 'announcements:service_judge': 'Servicerechter:', + 'announcements:and': ' en ', + 'announcements:preparation': 'In voorbereiding', + 'announcements:meetingpoint': 'Kom naar het meetingpoint!', + 'announcements:on_court': 'Op Baan ', + 'announcements:for_court': 'Voor Baan ', + 'announcements:match_number': 'Wedstrijdnummer ', + 'announcements:boys_singles': 'Jongens enkel', + 'announcements:boys_doubles': 'Jongens dubbel', + 'announcements:girls_singles': 'Meisjes enkel', + 'announcements:girls_doubles': 'Meisjes dubbel', + 'announcements:mixed_doubles': 'Gemengd dubbel', + 'announcements:men_singles': 'Mannen enkel', + 'announcements:men_doubles': 'Mannen dubbel', + 'announcements:women_singles': 'Dames enkel', + 'announcements:women_doubles': 'Dames dubbel', + 'announcements:round_16': 'Ronde van 16', + 'announcements:quaterfinal': 'Kwartfinale', + 'announcements:semifinal': 'Halve Finale', + 'announcements:final': 'Finale', + 'announcements:intermediate_round': 'Tussenronde', + 'announcements:round_for_places': 'Plaatsingsronde', + 'announcements:to': 'tot', + 'announcements:game_for_place': 'Wedstrijd om plaats ', + 'announcements:voice': 'Google Nederlands', + 'announcements:lang': 'nl-NL', + 'tournament:edit:add': 'Voeg Toe', + 'tournament:edit:delete': 'Verwijderen', + + + 'display_setting:id': 'ID:', + 'display_setting:wakelock': 'Bedrijfsmodus: ', + 'display_setting:style': 'Stijl: ', + 'display_setting:show_pause': 'Toon interval timer', + 'display_setting:show_court_number': 'Toon baannummer', + 'display_setting:show_competition': 'Toon de competitie', + 'display_setting:show_round': 'Toon de ronde', + 'display_setting:show_middle_name': 'Toon tussenvoegsel naam van spelers', + 'display_setting:show_doubles_receiving': 'Onderstreep de ontvangende speler in dubbels', + 'display_setting:colors': 'Kleuren : ', + 'display_setting:use_team_colors': 'Gebruik Teamkleuren', + 'display_setting:scale': 'Schaal: : ', + 'display_setting:language': 'Taal: ', + 'display_setting:language_automatic': 'Automatisch', + 'display_setting:language_en': 'Engels', + 'display_setting:language_de-DE': 'Duits (Duitsland)', + 'display_setting:language_de-AT': 'Duits (Oostenrijks)', + 'display_setting:language_de-CH': 'Duits (Zwitsers)', + 'display_setting:language_fr-CH': 'Frans (Zwitsers)', + 'display_setting:language_nl-BE': 'Vlaams', + 'display_setting:language_nl-NL': 'Nederlands', + 'display_setting:fullscreen_ask': 'Vraag volledig scherm bij opstarten: ', + 'display_setting:show_announcements': 'Toon aankondigingen: ', + 'display_setting:autohide': 'Verberg Instelling na (ms): ', + 'display_setting:double_click_timeout': 'Dubbelklik blokkade (ms): ', + 'display_setting:button_block_timeout': 'Dubbelklik bescherming (ms): ', + 'display_setting:negative_timers': 'Timers lopen door in negatief: ', + 'display_setting:shuttle_counter': 'Toon shuttleteller: ', +'display_setting:editmode_doubleclick': 'Touch-herkenning: ', + 'display_setting:settings_style': 'Gebruikersinterface: ', + 'display_setting:network_timeout': 'Netwerktimeout (s): ', + 'display_setting:network_update_interval': 'Netwerk-herhalingsinterval (ms): ', + 'display_setting:displaymode_update_interval': 'Schermmodus-update-interval (ms): ', + + 'tournament:edit:tournament': 'Toernooi', + 'tournament:edit:tournament_flow': 'Toernooiverloop', + 'tournament:edit:ticker_connection': 'Ticker-verbinding', + 'tournament:edit:btp_connection': 'BTP-verbinding', + 'tournament:edit:devices': 'Verbonden apparaten', + 'tournament:edit:calls': 'Oproepen', + 'tournament:edit:location': 'Locatie', + 'tournament:edit': 'Beheer instellingen', + 'tournament:edit:save': 'Opslaan', + 'tournament:edit:save_and_back': 'Opslaan en terug naar toernooioverzicht', + + 'tournament:edit:tournament:type': 'Type toernooi:', + 'tournament:edit:id': 'Toernooi-id:', + 'tournament:edit:language': 'Taal:', + 'tournament:edit:language:auto': 'Niet ingesteld (standaard browser)', + 'tournament:edit:name': 'Naam:', + 'tournament:edit:tguid': 'Toernooi-GUID:', + 'tournament:edit:courts': 'Banen:', + 'tournament:edit:dm_style': 'Standaard schermstijl:', + 'tournament:edit:displaysettings_general': 'Standaard scherminstelling:', + 'tournament:edit:warmup_timer_behavior': 'Gedrag voorbereidingstimer:', + 'tournament:edit:warmup_timer_behavior:bwf-2016': 'BWF 2016+ (na keuze speelhelft)', + 'tournament:edit:warmup_timer_behavior:legacy': 'Oud (na keuze speelhelft)', + 'tournament:edit:warmup_timer_behavior:choise': 'Na keuze speelhelft (individuele tijd)', + 'tournament:edit:warmup_timer_behavior:call-down': 'Vanaf oproep (aftellen)', + 'tournament:edit:warmup_timer_behavior:call-up': 'Vanaf oproep (timer)', + 'tournament:edit:warmup_timer_behavior:none': 'Geen', + 'tournament:edit:warmup_ready': 'Klaar in seconden:', + 'tournament:edit:warmup_start': 'Wedstrijd start na seconden:', + 'tournament:edit:btp:enabled': 'Activeer BTP-synchronisatie', + 'tournament:edit:btp:autofetch_enabled': 'Automatische synchronisatie', + 'tournament:edit:btp_autofetch_timeout_intervall': 'Synchronisatie-interval (ms)', + 'tournament:edit:btp:readonly': 'Alleen lezen', + 'tournament:edit:btp:ip': 'IP-adres:', + 'tournament:edit:btp:password': 'BTP-wachtwoord:', + 'tournament:edit:btp:timezone': 'BTP-tijdzone:', + 'tournament:edit:btp:system timezone': 'Standaard systeem ({tz})', + + 'tournament:edit:ticker_enabled': 'Activeer online ticker', + 'tournament:edit:ticker_url': 'Ticker-URL:', + 'tournament:edit:ticker_password': 'Ticker-wachtwoord:', + 'tournament:edit:tabletoperator_enabled': 'Gebruik Tabletgebruiker', + 'tournament:edit:tabletoperator_winner_of_quaterfinals_enabled': 'Winnaars kwartfinales moeten tabletbediening doen', + 'tournament:edit:tabletoperator_set_break_after_tabletservice': 'Pauze instellen na tabletbediening', + 'tournament:edit:tabletoperator_break_seconds': 'Pauzetijd na tabletgebruik (sec)', + 'tournament:edit:tabletoperator_split_doubles': 'Splits dubbels voor tabletbediening', + 'tournament:edit:tabletoperator_with_state_enabled': 'Roep bond/club op i.p.v. speler', + 'tournament:edit:tabletoperator_with_state_from_match_enabled': 'Roep bond/club op van eerste speler in wedstrijd', + 'tournament:edit:tabletoperator_with_umpire_enabled': 'Kondig scheidsrechter en tabletgebruiker aan', + 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Gebruik scoreborden i.p.v. tablets', + 'tournament:edit:annoncement_include_event': 'Kondig onderdeel aan', + 'tournament:edit:annoncement_include_round': 'Kondig ronde van toernooi aan', + 'tournament:edit:annoncement_include_matchnumber': 'Kondig wedstrijdnummer aan', + 'tournament:edit:announcement_speed': 'Aankondigingssnelheid (0.8-1.3): ', + 'tournament:edit:announcement_pause_time_ms': 'Pauze tussen aankondigingen (sec): ', + 'tournament:edit:preparation_meetingpoint_enabled': 'Gebruik meetingpoint voor voorbereiding', + 'tournament:edit:preparation_tabletoperator_setup_enabled': 'Roep tabletgebruiker op in voorbereiding', + 'tournament:edit:normalizations': 'Uitspraak-optimalisatie', + 'tournament:edit:normalizations:origin': 'Origineel', + 'tournament:edit:normalizations:replace': 'Vervanging', + 'tournament:edit:normalizations:language': 'Taal', + 'tournament:edit:advertisements': 'Advertenties', + 'tournament:edit:advertisements:id': 'Id', + 'tournament:edit:advertisements:url': 'URL', + 'tournament:edit:advertisements:type': 'Type', + 'tournament:edit:advertisements:disabled': 'Uitgeschakeld', + 'tournament:edit:general_displaysettings': 'Beheer weergave-instellingen:', + 'tournament:edit:displays': 'Beheer schermen:', + 'tournament:edit:displays:hostname': 'Hostnaam', + 'tournament:edit:displays:batterylevel': 'Batterij', + 'tournament:edit:displays:battery_charging_time': 'Opladen: {battery_charging_time} minuten resterend.', + 'tournament:edit:displays:battery_duscharging_time': '{battery_discharging_time} minuten over.', + 'tournament:edit:displays:num': 'Schermnummer', + 'tournament:edit:displays:court': 'Baan', + 'tournament:edit:displays:setting': 'Instelling', + 'tournament:edit:displays:description': 'Omschrijving', + 'tournament:edit:displays:onlinestatus': 'Status', + 'tournament:edit:tablets': 'Tablet-instellingen:', + 'tournament:edit:ticker': 'Ticker-instellingen:', + 'tournament:edit:btp': 'Badminton Tournament Planner-instellingen:', + 'tournament:edit:bts': 'Badminton Tournament Server-instellingen:', + 'tournament:edit:upcoming_matches_settings': 'Instellingen wedstrijdoverzicht', + 'tournament:edit:upcoming_matches_animation_speed': 'Animatiesnelheid scrollen in wedstrijdoverzicht', + 'tournament:edit:upcoming_matches_animation_pause': 'Pauze begin/einde pagina (sec)', + 'tournament:edit:upcoming_matches_max_count': 'Max. aantal wedstrijden in overzicht', + 'tournament:edit:call_preparation_matches_automatically_enabled': 'Roep voorbereidende wedstrijden automatisch op vrije banen', + 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Roep volgende mogelijke wedstrijd automatisch in voorbereiding op', + 'to_stats:header': 'Statistieken officials', + 'to_stats:name': 'Naam', + 'to_stats:umpire': 'U', + 'to_stats:service_judge': 'SJ', + 'to_stats:total': 'Totaal', + + 'match:rawinfo': 'Technische informatie', + 'match:manualcall': 'Aankondiging: handmatige oproep van wedstrijd', + 'match:preparationcall': 'Aankondiging: wedstrijd in voorbereiding', + 'match:begintoplay': 'Aankondiging: start wedstrijd', + 'match:secondcallteamone': 'Aankondiging: tweede oproep team 1', + 'match:secondcallteamtwo': 'Aankondiging: tweede oproep team 2', + 'match:secondcallumpire': 'Aankondiging: tweede oproep scheidsrechter', + 'match:secondcallservicejudge': 'Aankondiging: tweede oproep service judge', + 'match:secondcaltabletoperator': 'Aankondiging: tweede oproep tabletoperator', + 'match:scoresheet': 'Scoreformulier', + 'match:edit': 'Bewerken', + 'match:override_colors': 'Kleuren overschrijven', + 'match:incomplete': '[onvolledig!] ', + 'match:edit:scheduled_date': 'Datum:', + 'match:edit:delete': 'Wedstrijd verwijderen', + 'match:edit:now_on_court': 'Nu op baan', + 'match:delete:really': 'Wedstrijd {match_id} echt verwijderen?', + 'tournament:edit:logo': 'Logo', + 'tournament:edit:logo:nologo': 'Geen logo', + 'tournament:edit:logo:upload': 'Upload logo', + 'tournament:edit:logo:background': 'Achtergrond logo:', + 'tournament:edit:logo:foreground': 'Voorgrond logo:', + + 'nationstats': 'Landstatistieken', + 'nationstats:summary': '{player_count} spelers uit {nation_count} landen', + 'nationstats:summary:umpires': '{umpire_count} scheidsrechters uit {nation_count} landen', + + 'umpires:status:heading': 'Scheidsrechters', + 'umpires:status:ready': 'Beschikbaar', + 'umpires:status:paused': 'Reserve', + 'umpires:paused_since': 'sinds {time}', + 'umpires:oncourt': 'Op baan', + 'umpires:btp_id': 'BTP ID {btp_id}', + 'umpires:last_on_court': 'vorige wedstrijd eindigde om {time}', + + 'tabletoperator:unassigned': 'Volgende tabletgebruikers', + 'tabletoperator:name': 'Naam', + 'tabletoperator:add': 'Inplannen als tabletgebruiker', + 'tabletoperator:move_up': 'Omhoog in lijst', + 'tabletoperator:move_down': 'Omlaag in lijst', + 'tabletoperator:remove': 'Verwijderen uit lijst', + 'csvexport:winners': 'CSV-export (winnaarscertificaten)', +}; + +/*@DEV*/ +if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { + module.exports = ci18n_en; +} +/*/@DEV*/ \ No newline at end of file From 96aca6c51d175deb972d089a9582eb46d699a948 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Fri, 7 Nov 2025 01:55:47 +0100 Subject: [PATCH 204/224] Rufe Tabletbediener der Landesverbaende in Vorbereitung auf und aktualisiere dit upcoming matches korrekt. --- bts/admin.js | 70 ++++-- bts/btp_sync.js | 6 +- bts/match_utils.js | 29 ++- static/css/admin.css | 2 +- static/css/cmatch.css | 18 +- .../{preperation.svg => preparation.svg} | 2 +- static/js/announcements.js | 98 ++++++-- static/js/change.js | 22 +- static/js/ci18n_de.js | 5 +- static/js/ci18n_en.js | 5 +- static/js/cmatch.js | 218 +++++++++++------- static/js/ctournament.js | 24 +- 12 files changed, 352 insertions(+), 147 deletions(-) rename static/icons/{preperation.svg => preparation.svg} (99%) diff --git a/bts/admin.js b/bts/admin.js index abfb6a7..35b9c48 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -207,11 +207,11 @@ function handle_court_edit(app, ws, msg) { } function handle_location_changed(app, ws, msg) { - if (!_require_msg(ws, msg, ['tournament_key', 'location_id', 'highlight', 'preperation_addition', 'meetingpoint_announcement'])) { + if (!_require_msg(ws, msg, ['tournament_key', 'location_id', 'highlight', 'preparation_addition', 'meetingpoint_announcement'])) { return; } const location_id = msg.location_id; - const preperation_addition = msg.preperation_addition; + const preparation_addition = msg.preparation_addition; const meetingpoint_announcement = msg.meetingpoint_announcement; const highlight = msg.highlight; @@ -226,13 +226,13 @@ function handle_location_changed(app, ws, msg) { return; } - app.db.locations.update(query, { $set: {highlight, preperation_addition, meetingpoint_announcement} }, {}, (err) => { + app.db.locations.update(query, { $set: {highlight, preparation_addition, meetingpoint_announcement} }, {}, (err) => { if(err) { ws.respond(msg, err); return; } - notify_change(app, msg.tournament_key, 'location_changed', {location_id, highlight, preperation_addition, meetingpoint_announcement}); + notify_change(app, msg.tournament_key, 'location_changed', {location_id, highlight, preparation_addition, meetingpoint_announcement}); notify_change(app, msg.tournament_key, 'location_highlight_changed', {old_location_highlight: old_location.highlight, new_location_highlight: highlight}); @@ -777,10 +777,10 @@ function handle_match_preparation_call(app, ws, msg) { const match_utils = require('./match_utils'); - if (!_require_msg(ws, msg, ['tournament_key', 'match_id', 'location_id', 'setup'])) { + if (!_require_msg(ws, msg, ['tournament_key', 'match', 'location_id'])) { return; } - if (match_utils.match_completly_initialized(msg.setup) == false) { + if (match_utils.match_completly_initialized(msg.match.setup) == false) { return ws.respond("Match cannot be called one or more Teams are not set."); } @@ -790,8 +790,7 @@ function handle_match_preparation_call(app, ws, msg) { return ws.respond(err); } - const setup = _extract_setup(msg.setup); - match_utils.call_match_in_preparation(app, tournament, msg.match_id, msg.location_id, setup, (err) => { + match_utils.call_match_in_preparation(app, tournament, msg.match, msg.location_id, (err) => { ws.respond(msg, err); return; }); @@ -883,6 +882,19 @@ function handle_second_call_tabletoperator(app, ws, msg) { ws.respond(msg); } +function handle_second_preparation_call_tabletoperator(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + notify_change(app, tournament_key, 'second_preparation_call_tabletoperator', {setup}); + + ws.respond(msg); +} + function handle_second_call_umpire(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { return; @@ -895,6 +907,20 @@ function handle_second_call_umpire(app, ws, msg) { ws.respond(msg); } + +function handle_second_preparation_call_umpire(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + notify_change(app, tournament_key, 'second_preparation_call_umpire', { setup }); + + ws.respond(msg); +} + function handle_second_call_servicejudge(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { return; @@ -908,6 +934,19 @@ function handle_second_call_servicejudge(app, ws, msg) { ws.respond(msg); } +function handle_second_preparation_call_servicejudge(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { + return; + } + + const tournament_key = msg.tournament_key; + const setup = _extract_setup(msg.setup); + + notify_change(app, tournament_key, 'second_preparation_call_servicejudge', { setup }); + + ws.respond(msg); +} + function handle_second_call_team_one(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { @@ -923,7 +962,7 @@ function handle_second_call_team_one(app, ws, msg) { } -function handle_second_preperation_call_team_one(app, ws, msg) { +function handle_second_preparation_call_team_one(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { return; } @@ -931,7 +970,7 @@ function handle_second_preperation_call_team_one(app, ws, msg) { const tournament_key = msg.tournament_key; const setup = _extract_setup(msg.setup); - notify_change(app, tournament_key, 'second_preperation_call_team_one', {setup}); + notify_change(app, tournament_key, 'second_preparation_call_team_one', {setup}); ws.respond(msg); } @@ -1047,7 +1086,7 @@ function handle_second_call_team_two(app, ws, msg) { } -function handle_second_preperation_call_team_two(app, ws, msg) { +function handle_second_preparation_call_team_two(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { return; } @@ -1055,7 +1094,7 @@ function handle_second_preperation_call_team_two(app, ws, msg) { const tournament_key = msg.tournament_key; const setup = _extract_setup(msg.setup); - notify_change(app, tournament_key, 'second_preperation_call_team_two', {setup}); + notify_change(app, tournament_key, 'second_preparation_call_team_two', {setup}); ws.respond(msg); } @@ -1283,12 +1322,15 @@ module.exports = { handle_ticker_reset, handle_free_announce, handle_second_call_umpire, + handle_second_preparation_call_umpire, handle_second_call_servicejudge, + handle_second_preparation_call_servicejudge, handle_second_call_tabletoperator, + handle_second_preparation_call_tabletoperator, handle_second_call_team_one, handle_second_call_team_two, - handle_second_preperation_call_team_one, - handle_second_preperation_call_team_two, + handle_second_preparation_call_team_one, + handle_second_preparation_call_team_two, handle_tournament_get, handle_tournament_list, handle_tournament_edit_props, diff --git a/bts/btp_sync.js b/bts/btp_sync.js index bfc2b7e..cac36ea 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -882,7 +882,7 @@ function integrate_locations(app, tournament_key, btp_state, callback) { const city = (l.City ? l.City[0] : ""); const state = (l.State ? l.State[0] : ""); const country = (l.Country ? l.Country[0] : ""); - const preperation_addition = ""; + const preparation_addition = ""; const meetingpoint_announcement = ""; const short_name = generateHallAbbreviation(name); @@ -916,7 +916,7 @@ function integrate_locations(app, tournament_key, btp_state, callback) { if (cur_location) { //ADD BTP ID - app.db.locations.update(alt_query, { $set: { btp_id, name, address, postal_code, city, state, country, preperation_addition, meetingpoint_announcement, short_name} }, {}, (err) => cb(err)); + app.db.locations.update(alt_query, { $set: { btp_id, name, address, postal_code, city, state, country, preparation_addition, meetingpoint_announcement, short_name} }, {}, (err) => cb(err)); return; } @@ -942,7 +942,7 @@ function integrate_locations(app, tournament_key, btp_state, callback) { city, state, country, - preperation_addition, + preparation_addition, meetingpoint_announcement, short_name, highlight, diff --git a/bts/match_utils.js b/bts/match_utils.js index a6ea405..925931f 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -48,7 +48,7 @@ async function call_match(app, tournament, match, old_court, callback) { async.waterfall([ (wcb) => add_called_timestamp(match, wcb), (wcb) => add_tabletoperators(app, tournament, match, wcb), (wcb) => set_umpires_on_court(app, tournament, match, wcb), - (wcb) => remove_highlight_preperation(match, wcb), + (wcb) => remove_highlight_preparation(match, wcb), (wcb) => update_match_btp(app, match, wcb), (wcb) => update_match_db(app, match, wcb), (wcb) => update_court_db(app, match, wcb), @@ -75,7 +75,7 @@ async function switch_court(app, tournament, match, old_court, callback) { async.waterfall([ (wcb) => add_tabletoperators(app, tournament, match, wcb), (wcb) => set_umpires_on_court(app, tournament, match, wcb), - (wcb) => remove_highlight_preperation(match, wcb), + (wcb) => remove_highlight_preparation(match, wcb), (wcb) => update_match_btp(app, match, wcb), (wcb) => update_match_db(app, match, wcb), (wcb) => update_court_db(app, match, wcb), @@ -93,7 +93,7 @@ async function switch_court(app, tournament, match, old_court, callback) { } function match_completly_initialized(setup) { - if (setup.teams[0].players.length == 0 || setup.teams[1].players.length == 0) { + if (!setup || setup.teams[0].players.length == 0 || setup.teams[1].players.length == 0) { return false; } return true; @@ -214,7 +214,7 @@ async function set_umpires_on_court(app, tournament, match, callback) { return callback(null); } -function remove_highlight_preperation(match, callback){ +function remove_highlight_preparation(match, callback){ const setup = match.setup; if(setup.highlight && setup.highlight == 6){ @@ -995,16 +995,33 @@ async function call_next_possible_match_for_preparation(app, tournament_key, cal } -async function call_match_in_preparation(app, tournament, match_id, location_id, setup, callback) { +async function call_match_in_preparation(app, tournament, match, location_id, callback) { const tournament_key = tournament.key; const admin = require('./admin'); + const setup = match.setup; + const match_id = match._id; await add_preparation_call_timestamp(app.db, tournament_key, setup, location_id); if (tournament.preparation_tabletoperator_setup_enabled) { if (!setup.umpire || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { if (!setup.tabletoperators || setup.tabletoperators == null) { - setup.tabletoperators = await fetch_tabletoperator(admin, app, tournament_key, "prep_call"); + const fetch_result = await fetch_tabletoperator(admin, app, tournament.key, "prep_call"); + let value = []; + if (tournament.tabletoperator_with_state_from_match_enabled && typeof(fetch_result) == "undefined") { + value.push({ + asian_name: false, + name: setup.teams[0].players[0].state, + firstname: "", + lastname: "", + btp_id: -1}); + } else { + value = fetch_result; + } + + if (!setup.umpire || !setup.umpire.name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { + setup.tabletoperators = value; + } } } } diff --git a/static/css/admin.css b/static/css/admin.css index 02efdf5..87cd009 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -369,7 +369,7 @@ h2.edit { } -#preperation_addition{ +#preparation_addition{ width: -webkit-fill-available;; resize: vertical; min-height: 5.0em; diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 09d72bb..5cbefaa 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -26,7 +26,7 @@ .match_begin_to_play_button, .match_manual_call_button, .match_second_call_button, -.match_second_preperation_call_button, +.match_second_preparation_call_button, .match_rawinfo { display: inline-block; min-width: 1em; @@ -47,7 +47,7 @@ height: 100%; width: auto; margin: 0 0 -0.1em 0.3em; - /*background-image: url(../icons/preperation.svg);*/ + /*background-image: url(../icons/preparation.svg);*/ } .match_manual_call_button { height: 1.5em; @@ -65,7 +65,7 @@ } .match_second_call_button, -.match_second_preperation_call_button { +.match_second_preparation_call_button { height: 1.2em; min-height: 1em; background-size: cover; @@ -100,7 +100,7 @@ .match_edit_button:hover, .match_second_call_button:hover, -.match_second_preperation_call_button:hover, +.match_second_preparation_call_button:hover, .match_preparation_call_button:hover, .match_begin_to_play_button:hover, .match_manual_call_button:hover, @@ -317,7 +317,7 @@ match_preparation_call_button, match_begin_to_play_button, match_second_call_button, -match_second_preperation_call_button, +match_second_preparation_call_button, match_manual_call_button, .match_scoresheet_buttons { position: fixed; @@ -341,12 +341,12 @@ match_manual_call_button, } .match_second_call_button > * , -.match_second_preperation_call_button > * { +.match_second_preparation_call_button > * { display: block; } .match_second_call_button > button, -.match_second_preperation_call_button > button { +.match_second_preparation_call_button > button { margin: 0 0 0 2vmin; font-size: 5vmin; padding: 0.1em 0.1em; @@ -526,12 +526,12 @@ match_manual_call_button, background-color: #c56bff; } -.preperation_container { +.preparation_container { display: flex; flex-direction: column; } -.preperation { +.preparation { font-weight: bold; text-wrap: nowrap; } diff --git a/static/icons/preperation.svg b/static/icons/preparation.svg similarity index 99% rename from static/icons/preperation.svg rename to static/icons/preparation.svg index abae4ad..16a8428 100644 --- a/static/icons/preperation.svg +++ b/static/icons/preparation.svg @@ -17,7 +17,7 @@ version="1.1" id="svg8" inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)" - sodipodi:docname="preperation.svg"> + sodipodi:docname="preparation.svg"> l._id === matchSetup.location_id); if(l) { - secondCall += ' ' + l.preperation_addition; + secondCall += ' ' + l.preparation_addition; } } secondCall += "!"; @@ -146,7 +212,11 @@ function announceBeginnToPlay(matchSetup, team) { } function createSecondCallAnnouncement() { - return ci18n('announcements:second_call'); + return ci18n('announcements:second_call') + ' ' + ci18n('announcements:second_call_for') + ':'; +} + +function createSecondPreparationCallAnnouncement() { + return ci18n('announcements:second_call') + ' ' + ci18n('announcements:preparation')+ ' ' + ci18n('announcements:second_call_for') + ':'; } function createTeamAnnouncement(matchSetup) { @@ -314,7 +384,7 @@ function createPreparationAnnouncement(matchSetup) { if(matchSetup.location_id) { const l = utils.find(curt.locations, l => l._id === matchSetup.location_id); if(l) { - addition = ' ' + l.preperation_addition; + addition = ' ' + l.preparation_addition; } } return ci18n('announcements:preparation') + addition; diff --git a/static/js/change.js b/static/js/change.js index 352fe9e..dac95d2 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -126,6 +126,7 @@ function default_handler(rerender, special_funcs) { if (!m) { ctournament.add_match(c); curt.matches.push(c.val.match); + curt.matches = curt.matches.sort( (a, b) => cmatch.cmp_match_order(a, b)); } else { ctournament.add_match(c); } @@ -159,10 +160,10 @@ function default_handler(rerender, special_funcs) { const l = utils.find(curt.locations, l => l._id === c.val.location_id); if(l) { l.highlight = c.val.highlight; - l.preperation_addition = c.val.preperation_addition; + l.preparation_addition = c.val.preparation_addition; l.meetingpoint_announcement = c.val.meetingpoint_announcement; } - ctournament.update_location(c.val.location_id, c.val.highlight, c.val.preperation_addition, c.val.meetingpoint_announcement); + ctournament.update_location(c.val.location_id, c.val.highlight, c.val.preparation_addition, c.val.meetingpoint_announcement); break; case 'location_highlight_changed': const old_location_highlight = c.val.old_location_highlight; @@ -213,12 +214,21 @@ function default_handler(rerender, special_funcs) { break; case 'second_call_team_two': announceSecondCallTeamTwo(c.val.setup); + break; + case 'second_preparation_call_tabletoperator': + announceSecondPreparationCallTabletoperator(c.val.setup); break; - case 'second_preperation_call_team_one': - announceSecondPreperationCallTeamOne(c.val.setup); + case 'second_preparation_call_umpire': + announceSecondPreparationCallUmpire(c.val.setup); break; - case 'second_preperation_call_team_two': - announceSecondPreperationCallTeamTwo(c.val.setup); + case 'second_preparation_call_servicejudge': + announceSecondPreparationCallServiceJudge(c.val.setup); + break; + case 'second_preparation_call_team_one': + announceSecondPreparationCallTeamOne(c.val.setup); + break; + case 'second_preparation_call_team_two': + announceSecondPreparationCallTeamTwo(c.val.setup); break; case 'btp_status': ctournament.btp_status_changed(c); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index ae78c96..6198ccd 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -55,10 +55,13 @@ var ci18n_de = { 'inactivate_court': 'Spielfeld sperren', 'announcements:begin_to_play': 'Bitte mit dem Spielen beginnen!', - 'announcements:second_call': '"Zweiter Aufruf fuer:"', + 'announcements:second_call': 'Zweiter Aufruf', + 'announcements:second_call_for': 'für', + 'announcements:vs': ' gegen ', 'announcements:counting_board_service': 'Klapptafelbedienung:', 'announcements:table_service': 'Tabletbedienung:', + 'announcements:please_as_tablet_service': 'bitte als Tabletbedienung', 'announcements:umpire': 'Schiedsrichter:', 'announcements:service_judge': 'Aufschlagrichter:', 'announcements:and': ' und ', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 18acf78..18880f5 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -55,10 +55,13 @@ var ci18n_en = { 'inactivate_court': 'Spielfeld sperren', 'announcements:begin_to_play': 'Start to play!', - 'announcements:second_call': '"Second call for:"', + 'announcements:second_call': '"Second call', + 'announcements:second_call_for': 'for', 'announcements:vs': ' vs ', 'announcements:counting_board_service': 'Countingboard service:', 'announcements:table_service': 'Tablet service:', + 'announcements:the_table_service_from': 'The Tablet service from', + 'announcements:please_as_tablet_service': 'please', 'announcements:umpire': 'Umpire:', 'announcements:service_judge': 'Servicejudge:', 'announcements:and': ' and ', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index e4116be..bc880cf 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -329,7 +329,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } if(style === 'unasigned' && match.setup.highlight >= 1){ - create_match_button(players0, 'vlink match_second_preperation_call_button', 'match:secondcallteamone', on_second_preperation_call_team_one_button_click, match._id); + create_match_button(players0, 'vlink match_second_preparation_call_button', 'match:secondcallteamone', on_second_preparation_call_team_one_button_click, match._id); } } @@ -344,7 +344,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ render_players_el(players1, setup, 1, match, show_player_status, style); if (style === 'default' || style === 'plain' || style === 'unasigned') { if(style === 'unasigned' && match.setup.highlight >= 1){ - create_match_button(players1, 'vlink match_second_preperation_call_button', 'match:secondcallteamtwo', on_second_preperation_call_team_two_button_click, match._id); + create_match_button(players1, 'vlink match_second_preparation_call_button', 'match:secondcallteamtwo', on_second_preparation_call_team_two_button_click, match._id); } if (show_add_tabletoperator) { @@ -380,6 +380,10 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ const umpire_span = uiu.el(to_td, 'span', 'person'); uiu.el(umpire_span, 'div', 'umpire', ''); uiu.el(umpire_span, 'span', 'name', setup.umpire.name); + + if(style === 'unasigned' && match.setup.highlight >= 1){ + create_match_button(umpire_span, 'vlink match_second_preparation_call_button', 'match:secondcallumpire', on_second_preparation_call_umpire_button_click, match._id); + } if (style === 'default' || style === 'plain' || style === 'unasigned') { create_match_button(umpire_span, 'vlink match_second_call_button', 'match:secondcallumpire', on_second_call_umpire_button_click, match._id); } @@ -388,6 +392,9 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ const service_judge_span = uiu.el(to_td, 'span', 'person'); uiu.el(service_judge_span, 'div', 'service_judge', ''); uiu.el(service_judge_span, 'span', 'name', setup.service_judge.name); + if(style === 'unasigned' && match.setup.highlight >= 1){ + create_match_button(service_judge_span, 'vlink match_second_preparation_call_button', 'match:secondcallservicejudge', on_second_preparation_call_servicejudge_button_click, match._id); + } if (style === 'default' || style === 'plain' || style === 'unasigned') { create_match_button(service_judge_span, 'vlink match_second_call_button', 'match:secondcallservicejudge', on_second_call_servicejudge_button_click, match._id); } @@ -407,6 +414,10 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ uiu.el(person2_div, 'span', 'match_no_umpire', setup.tabletoperators[1].name ); } + if(style === 'unasigned' && match.setup.highlight >= 1){ + create_match_button(tablet_div, 'vlink match_second_preparation_call_button', 'match:secondcaltabletoperator', on_second_preparation_call_tabletoperator_button_click, match._id); + } + if (style === 'default' || style === 'plain' || style === 'unasigned') { create_match_button(tablet_div, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); } @@ -419,13 +430,13 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } } else if(style === 'upcoming' && setup.highlight >= 1) { - var preperation_container = uiu.el(to_td, 'div', 'preperation_container'); - uiu.el(preperation_container, 'span', 'preperation', 'in Vorbereitung' + (setup.location_id ? "" : "!")); + var preparation_container = uiu.el(to_td, 'div', 'preparation_container'); + uiu.el(preparation_container, 'span', 'preparation', 'in Vorbereitung' + (setup.location_id ? "" : "!")); if(setup.location_id) { const l = utils.find(curt.locations, l => l._id === setup.location_id); if(l) { - uiu.el(preperation_container, 'span', 'preperation', l.preperation_addition); //TODO: Hie die Halle mit ausgeben! + uiu.el(preparation_container, 'span', 'preparation', l.preparation_addition); //TODO: Hie die Halle mit ausgeben! } } @@ -589,14 +600,14 @@ function create_match_button(targetEl, cssClass, title, listener, matchId,) { function create_match_prepparation_button(targetEl, cssClass, title, listener, matchId, location){ const btn = uiu.el(targetEl, 'div', { 'class': cssClass, - 'title': ci18n(title) + (location.preperation_addition ? ' ' + location.preperation_addition : ''), + 'title': ci18n(title) + (location.preparation_addition ? ' ' + location.preparation_addition : ''), 'data-match_id': matchId, 'data-location_id': location._id, }); uiu.el(btn, 'img', { style: 'height: 1.2em; margin-top: 0.2em;', - src: location.logo_id ? '/h/' + encodeURIComponent(curt.key) + '/logo/' + location.logo_id : '/static/icons/preperation.svg', + src: location.logo_id ? '/h/' + encodeURIComponent(curt.key) + '/logo/' + location.logo_id : '/static/icons/preparation.svg', name: 'location_logo_img', 'data-match_id': matchId, 'data-location_id': location._id @@ -1200,10 +1211,9 @@ function on_announce_preparation_matchbutton_click(e) { if (match != null && location != null) { send({ type: 'match_preparation_call', - match_id: match._id, + match: match, location_id : location._id, tournament_key: match.tournament_key, - setup: match.setup, }, function (err) { if (err) { return cerror.net(err); @@ -1249,11 +1259,11 @@ function on_second_call_team_two_button_click(e) { }); } } -function on_second_preperation_call_team_one_button_click(e) { +function on_second_preparation_call_team_one_button_click(e) { const match = fetchMatchFromEvent(e); if (match != null) { send({ - type: 'second_preperation_call_team_one', + type: 'second_preparation_call_team_one', tournament_key: curt.key, setup: match.setup, }, err => { @@ -1263,11 +1273,11 @@ function on_second_preperation_call_team_one_button_click(e) { }); } } -function on_second_preperation_call_team_two_button_click(e) { +function on_second_preparation_call_team_two_button_click(e) { const match = fetchMatchFromEvent(e); if (match != null) { send({ - type: 'second_preperation_call_team_two', + type: 'second_preparation_call_team_two', tournament_key: curt.key, setup: match.setup, }, err => { @@ -1277,80 +1287,127 @@ function on_second_preperation_call_team_two_button_click(e) { }); } } - function on_second_call_tabletoperator_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - send({ - type: 'second_call_tabletoperator', - tournament_key: curt.key, - setup: match.setup, - }, err => { - if (err) { - return cerror.net(err); - } - }); - } +function on_second_call_tabletoperator_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_call_tabletoperator', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); } - function on_second_call_umpire_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - send({ - type: 'second_call_umpire', - tournament_key: curt.key, - setup: match.setup, - }, err => { - if (err) { - return cerror.net(err); - } - }); - } +} +function on_second_preparation_call_tabletoperator_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_preparation_call_tabletoperator', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); } +} +function on_second_call_umpire_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_call_umpire', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +} - function on_second_call_servicejudge_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - send({ - type: 'second_call_servicejudge', - tournament_key: curt.key, - setup: match.setup, - }, err => { - if (err) { - return cerror.net(err); - } - }); - } + +function on_second_preparation_call_umpire_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_preparation_call_umpire', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); } - +} - function on_begin_to_play_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - send({ - type: 'begin_to_play_call', - tournament_key: curt.key, - setup: match.setup, - }, err => { - if (err) { - return cerror.net(err); - } - }); - } +function on_second_call_servicejudge_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_call_servicejudge', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); } +} - function on_announce_match_manually_button_click(e) { - const match = fetchMatchFromEvent(e); - if (match != null) { - send({ - type: 'announce_match_manually', - tournament_key: curt.key, - match: match, - }, err => { - if (err) { - return cerror.net(err); - } - }); - } + +function on_second_preparation_call_servicejudge_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'second_preparation_call_servicejudge', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); } +} + + + +function on_begin_to_play_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'begin_to_play_call', + tournament_key: curt.key, + setup: match.setup, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +} + +function on_announce_match_manually_button_click(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'announce_match_manually', + tournament_key: curt.key, + match: match, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +} function fetchMatchFromEvent(e) { const btn = e.target; const match_id = btn.getAttribute('data-match_id'); @@ -1723,6 +1780,7 @@ function render_unassigned(container) { } function render_upcoming_matches(container) { + console.warn("Rendere upcoming"); const UPCOMING_MATCH_COUNT = parseInt(curt.upcoming_matches_max_count ? curt.upcoming_matches_max_count : 15); uiu.empty(container); @@ -1733,6 +1791,8 @@ function render_upcoming_matches(container) { const upcoming_table = uiu.el(container, 'table', 'upcoming_table'); const upcoming_tbody = uiu.el(upcoming_table, 'tbody', 'upcoming_tbody'); const unassigned_matches = curt.matches.filter(m => calc_section(m) === 'unassigned'); + + console.log(unassigned_matches); var resizable_rows = []; diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 2e68d58..822332e 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -493,7 +493,7 @@ var ctournament = (function() { { const form = uiu.el(container, 'form'); - const checkboxId = `enable_announcement_preperations_${loc._id}`; + const checkboxId = `enable_announcement_preparations_${loc._id}`; const checkbox = uiu.el(form, 'input', { type: 'checkbox', id: checkboxId, @@ -2127,12 +2127,12 @@ var ctournament = (function() { send_location_to_admin(e.target.parentNode.parentNode, e.target.getAttribute('data-location_id')); }); - const preperation_td = uiu.el(tr, 'td', {}); - const preperation_input = create_textarea_input("textarea", preperation_td, 'preperation_addition'); - preperation_input.value = l.preperation_addition; - preperation_input.setAttribute('data-location-id', l._id); - preperation_input.setAttribute('maxlength', 175); - preperation_input.addEventListener('focusout', (e) => { + const preparation_td = uiu.el(tr, 'td', {}); + const preparation_input = create_textarea_input("textarea", preparation_td, 'preparation_addition'); + preparation_input.value = l.preparation_addition; + preparation_input.setAttribute('data-location-id', l._id); + preparation_input.setAttribute('maxlength', 175); + preparation_input.addEventListener('focusout', (e) => { send_location_to_admin(e.target.parentNode.parentNode, e.target.getAttribute('data-location-id')); }); const meetinpoint_td = uiu.el(tr, 'td', {}); @@ -2146,7 +2146,7 @@ var ctournament = (function() { const icon_td = uiu.el(tr, 'td', 'icon_td'); uiu.el(icon_td, 'img', { style: 'height: 40px;', - src: l.logo_id ? '/h/' + encodeURIComponent(curt.key) + '/logo/' + l.logo_id : '/static/icons/preperation.svg', + src: l.logo_id ? '/h/' + encodeURIComponent(curt.key) + '/logo/' + l.logo_id : '/static/icons/preparation.svg', name: 'location_logo_img', 'data-location_id': l._id }); @@ -2157,7 +2157,7 @@ var ctournament = (function() { const filename_display = uiu.el(logo_form, 'div', { class: 'upload_filename_location', 'data-location_id': l._id, - }, l.logo_name ? l.logo_name : 'preperation.svg'); + }, l.logo_name ? l.logo_name : 'preparation.svg'); const custom_label = uiu.el(logo_form, 'label', { for: logo_button_id, @@ -2234,7 +2234,7 @@ var ctournament = (function() { function send_location_to_admin(parent, location_id) { const highlight = parseInt(parent.querySelector("#select_highlight").value, 10); - const preperation_addition = parent.querySelector("#preperation_addition").value; + const preparation_addition = parent.querySelector("#preparation_addition").value; const meetingpoint_announcement = parent.querySelector("#meetingpoint_announcement").value; send({ @@ -2242,7 +2242,7 @@ var ctournament = (function() { tournament_key: curt.key, location_id, highlight: highlight, - preperation_addition, + preparation_addition, meetingpoint_announcement, }, function (err, response) { if (err) { @@ -2251,7 +2251,7 @@ var ctournament = (function() { }); } - function update_location(location_id, highlight, preperation_addition, meetingpoint_announcement) { + function update_location(location_id, highlight, preparation_addition, meetingpoint_announcement) { switch (get_admin_subpage()){ case 'edit': const locations_table = document.querySelector('.locations_table'); From f5e952a9d493a63ea246fc5d215958b9ecb24a90 Mon Sep 17 00:00:00 2001 From: Sascha Heinl Date: Fri, 28 Nov 2025 01:17:55 +0100 Subject: [PATCH 205/224] Add script to install bts as a systemd service --- install-service.sh | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 install-service.sh diff --git a/install-service.sh b/install-service.sh new file mode 100644 index 0000000..9b33a20 --- /dev/null +++ b/install-service.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Check if node is installed +if ! command -v node >/dev/null 2>&1; then + +# If not, install it + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash + \. "$HOME/.nvm/nvm.sh" + nvm install 24 +fi + +# Install BTS to home +cd ~ +if [ ! -d "bts" ]; then + git clone https://github.com/tlehr/bts.git +fi + +cd bts +git fetch +git switch feat/automaticCall +make deps + +# Install BUP to home +cd ~ + +if [ ! -d "bup" ]; then + git clone https://github.com/tlehr/bup.git +fi + +cd bup +git fetch +git switch feat/addTimerAfterCall +make deps + +# Use Development BUP in BTS +cd ~/bts || exit 1 + +cat > config.json <<'EOF' +{ + "port": 4000, + "bup_location": "static/bup/dev", + "bup_index": "bup.html", + "report_errors": true, + "enable_https": false +} +EOF + +# Create symlink +ln -s ~/bup/ ~/bts/static/bup/dev + +# use node version that works +nvm install 22.18.0 +node_path="$(which node)" + +# copy used node version into service template +cat > "$HOME/bts/div/bts.service.template" < Date: Mon, 1 Dec 2025 23:11:47 +0100 Subject: [PATCH 206/224] Add script to install bts as a systemd service --- install-service.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install-service.sh b/install-service.sh index 9b33a20..0b9f8f6 100644 --- a/install-service.sh +++ b/install-service.sh @@ -7,7 +7,7 @@ if ! command -v node >/dev/null 2>&1; then # If not, install it curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash \. "$HOME/.nvm/nvm.sh" - nvm install 24 + nvm install 18 fi # Install BTS to home From 9bcc3400beeb6d3a31ee6a9bf3b8e65607afd152 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 27 Jan 2026 02:50:37 +0100 Subject: [PATCH 207/224] Version for the DM 2026 in Cloppenburg --- bts/admin.js | 394 ++++++++++++++++++++++- bts/btp_sync.js | 33 +- bts/bupws.js | 3 - bts/match_utils.js | 125 ++++++-- bts/utils.js | 3 +- static/audio/evakuierung.mp3 | Bin 0 -> 156802 bytes static/css/admin.css | 121 ++++++- static/css/cmatch.css | 19 +- static/css/toprow.css | 7 +- static/js/announcements.js | 39 +++ static/js/cerror.js | 12 +- static/js/change.js | 57 +++- static/js/ci18n_de.js | 5 + static/js/ci18n_en.js | 4 + static/js/cmatch.js | 156 +++++++-- static/js/ctournament.js | 592 ++++++++++++++++++++++++++++++++++- 16 files changed, 1498 insertions(+), 72 deletions(-) create mode 100644 static/audio/evakuierung.mp3 diff --git a/bts/admin.js b/bts/admin.js index 35b9c48..8181083 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -198,7 +198,6 @@ function handle_court_edit(app, ws, msg) { ws.respond(msg, err); return; } - notify_change(app, msg.tournament_key, 'court_changed', {court_id, is_active}); ws.respond(msg); return; @@ -869,6 +868,19 @@ function handle_free_announce(app, ws, msg) { ws.respond(msg); } +function handle_emergency_announce(app, ws, msg) { + + if (!_require_msg(ws, msg, ['tournament_key', 'enable'])) { + return; + } + const tournament_key = msg.tournament_key; + const enable = msg.enable; + + notify_change(app, tournament_key, 'emergency_announce', enable); + + ws.respond(msg); +} + function handle_second_call_tabletoperator(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { return; @@ -975,6 +987,382 @@ function handle_second_preparation_call_team_one(app, ws, msg) { ws.respond(msg); } +function handle_official_list_move(app, ws, msg) { + if (!_require_msg(ws, msg, [ + 'tournament_key', + 'official_id', + 'from_list', + 'to_list', + 'prev_btp_id', + 'next_btp_id' + ])) { + return; + } + + const { + tournament_key, + official_id, + prev_btp_id, + next_btp_id, + from_list, + to_list + } = msg; + + // btp_id sicher normalisieren + const prevId = (prev_btp_id == null) ? null : Number(prev_btp_id); + const nextId = (next_btp_id == null) ? null : Number(next_btp_id); + + const neighborBtpIds = []; + if (Number.isFinite(prevId)) neighborBtpIds.push(prevId); + if (Number.isFinite(nextId)) neighborBtpIds.push(nextId); + + // Query: current über _id, prev/next über btp_id + const query = { + tournament_key, + $or: [{ _id: official_id }] + }; + if (neighborBtpIds.length > 0) { + query.$or.push({ btp_id: { $in: neighborBtpIds } }); + } + + app.db.umpires.find(query, function (err, docs) { + if (err) return cerror.ws(ws, err); + + let currentUmpire = null; + let prevUmpire = null; + let nextUmpire = null; + + for (const u of docs) { + if (u._id === official_id) { + currentUmpire = u; + continue; + } + if (Number.isFinite(prevId) && Number(u.btp_id) === prevId) { + prevUmpire = u; + continue; + } + if (Number.isFinite(nextId) && Number(u.btp_id) === nextId) { + nextUmpire = u; + } + } + + if (!currentUmpire) { + return cerror.ws(ws, new Error('current umpire not found')); + } + + // --- Timestamp für to_list berechnen gemäß deiner Regeln (robust gegen null) --- + const now = Date.now(); + + const prevTS = (prevUmpire && prevUmpire[to_list] != null) ? Number(prevUmpire[to_list]) : null; + const nextTS = (nextUmpire && nextUmpire[to_list] != null) ? Number(nextUmpire[to_list]) : null; + + const prevOk = (prevTS != null) && Number.isFinite(prevTS); + const nextOk = (nextTS != null) && Number.isFinite(nextTS); + + let newTS; + + // Ende der Liste: wenn es keinen Nachfolger gibt -> aktueller Timestamp + if (!nextUmpire || !nextOk) { + newTS = now; + + // Anfang der Liste: kein Vorgänger, aber Nachfolger -> zwischen 0 und next + } else if (!prevUmpire || !prevOk) { + newTS = nextTS / 2; + + // Zwischen zwei Elementen -> Mittelwert + } else { + newTS = (prevTS + nextTS) / 2; + } + + // --- Update vorbereiten --- + // Spezifikation: + // - currentUmpire[from_list] = null + // - currentUmpire[to_list] = newTS + const setObj = {}; + + setObj[inactive_list] = null; + setObj[service_judge_pause] = null; + setObj[umpire_pause] = null; + setObj[service_judge_wait] = null; + setObj[umpire_wait] = null; + setObj[service_judge_on_court] = null; + setObj[umpire_on_court] = null; + setObj[is_planed_as_service_judge] = false; + setObj[is_planed_as_umpire] = false; + setObj[to_list] = newTS; + + app.db.umpires.update( + { _id: currentUmpire._id, tournament_key }, + { $set: setObj }, + {}, + function (err2) { + if (err2) return cerror.ws(ws, err2); + + // Optional: aktualisiertes Objekt laden (für Broadcast/Clients) + app.db.umpires.findOne( + { _id: currentUmpire._id, tournament_key }, + function (err3, updated) { + if (err3) return cerror.ws(ws, err3); + + notify_change(app, tournament_key, 'official_list_move', { + official_id: currentUmpire._id, + from_list, + to_list, + new_ts: newTS, + }); + + ws.respond(msg); + } + ); + } + ); + }); +} + +function handle_official_edit(app, ws, msg) { + // Pflichtfelder prüfen + if (!_require_msg(ws, msg, ['tournament_key', 'official_id', 'field', 'value'])) { + return; + } + + const { tournament_key, official_id, field, value } = msg; + + // Nur diese Felder dürfen vom Client geändert werden + if (field !== 'is_umpire' && field !== 'is_service_judge') { + return ws.respond( + msg, + new Error('Field not allowed for official_edit: ' + field) + ); + } + + // Checkbox-Wert normalisieren + const newVal = !!value; + + // Offiziellen suchen + app.db.umpires.findOne( + { _id: official_id, tournament_key }, + function (err, umpire) { + if (err) { + return ws.respond(msg, err); + } + + if (!umpire) { + return ws.respond( + msg, + new Error( + 'Cannot find official ' + + official_id + + ' of tournament ' + + tournament_key + + ' in database' + ) + ); + } + + // Update vorbereiten + const setObj = {}; + setObj[field] = newVal; + setObj.updated_at = Date.now(); // optional + + // DB-Update + app.db.umpires.update( + { _id: official_id, tournament_key }, + { $set: setObj }, + {}, + function (err2) { + if (err2) { + return ws.respond(msg, err2); + } + + // Aktualisiertes Dokument laden (für Broadcast) + app.db.umpires.findOne( + { _id: official_id, tournament_key }, + function (err3, updated) { + if (err3) { + return ws.respond(msg, err3); + } + + // Broadcast an alle Clients + notify_change(app, tournament_key, 'official_edit', { + official_id, + field, + value: newVal + }); + + ws.respond(msg); + } + ); + } + ); + } + ); +} + +function handle_add_officials_to_match(app, ws, msg) { + // 1) Pflichtfelder prüfen + if (!_require_msg(ws, msg, ['tournament_key', 'match_id'])) { + return; + } + + const { tournament_key, match_id } = msg; + + function pack_official(u) { + return { + _id: u._id, + btp_id: u.btp_id, + name: u.name, + firstname: u.firstname, + surname: u.surname, + country: u.country + }; + } + + // 2) Match laden und prüfen, ob schon Officials gesetzt sind + app.db.matches.findOne({ _id: match_id, tournament_key }, function (err, match) { + if (err) return ws.respond(msg, err); + if (!match) { + return ws.respond( + msg, + new Error('Cannot find match ' + match_id + ' of tournament ' + tournament_key + ' in database') + ); + } + + if (match.setup?.umpire || match.setup?.service_judge) { + return ws.respond( + msg, + new Error('Match already has assigned officials') + ); + } + + const setup = match.setup; + + // 3) Ältesten Umpire suchen + app.db.umpires + .find({ tournament_key, umpire_wait: { $ne: null } }) + .sort({ umpire_wait: 1 }) + .limit(1) + .exec(function (err2, umps) { + if (err2) return ws.respond(msg, err2); + if (!umps || umps.length === 0) { + return ws.respond(msg, new Error('No umpire available')); + } + + const umpire = umps[0]; + + // 4) Atomar reservieren (Race-Condition-sicher) + app.db.umpires.update( + { _id: umpire._id, tournament_key, umpire_wait: { $ne: null } }, + { $set: { umpire_wait: null, + is_planed_as_umpire: true } }, + {}, + function (err3, affected1) { + if (err3) return ws.respond(msg, err3); + if (affected1 === 0) { + return ws.respond(msg, new Error('Umpire was already taken by another assignment')); + } + + // 5) Ältesten Service Judge suchen + app.db.umpires + .find({ tournament_key, service_judge_wait: { $ne: null } }) + .sort({ service_judge_wait: 1 }) + .limit(1) + .exec(function (err4, sjs) { + if (err4) return ws.respond(msg, err4); + if (!sjs || sjs.length === 0) { + // Rollback Umpire + app.db.umpires.update( + { _id: umpire._id, tournament_key }, + { $set: { umpire_wait: Date.now(), + is_planed_as_umpire: false } } + ); + return ws.respond(msg, new Error('No service judge available')); + } + + const service_judge = sjs[0]; + + // 6) Atomar reservieren + app.db.umpires.update( + { _id: service_judge._id, tournament_key, service_judge_wait: { $ne: null } }, + { $set: { service_judge_wait: null, + is_planed_as_service_judge: true + } }, + {}, + function (err5, affected2) { + if (err5) return ws.respond(msg, err5); + if (affected2 === 0) { + // Rollback Umpire + app.db.umpires.update( + { _id: umpire._id, tournament_key }, + { $set: { umpire_wait: Date.now(), + is_planed_as_service_judge: true } } + ); + return ws.respond(msg, new Error('Service judge was already taken')); + } + + // 7) Match.setup updaten + setup.umpire = pack_official(umpire); + setup.service_judge = pack_official(service_judge); + + app.db.matches.update( + { _id: match_id, tournament_key }, + { $set: {setup} }, + {}, + function (err6) { + if (err6) { + // Rollback beider Officials + app.db.umpires.update( + { _id: umpire._id, tournament_key }, + { $set: { umpire_wait: Date.now()/10, + is_planed_as_umpire: false + } } + ); + app.db.umpires.update( + { _id: service_judge._id, tournament_key }, + { $set: { service_judge_wait: Date.now()/10, + is_planed_as_service_judge: false + } } + ); + return ws.respond(msg, err6); + } + + // 8) Broadcast + app.db.matches.findOne( + { _id: match_id, tournament_key }, + function (err7, updatedMatch) { + if (err7) return ws.respond(msg, err7); + + notify_change(app, tournament_key, 'match_edit', {match__id: msg.match_id, match: updatedMatch}); + btp_manager.update_score(app, updatedMatch); + + // Officials ebenfalls broadcasten + app.db.umpires.find( + { tournament_key, _id: { $in: [umpire._id, service_judge._id] } }, + function (err8, updatedOfficials) { + if (!err8 && updatedOfficials) { + for (const u of updatedOfficials) { + notify_change(app, tournament_key, 'umpire_updated', u); + } + } + + // 9) Erfolg + return ws.respond(msg); + } + ); + } + ); + } + ); + } + ); + }); + } + ); + }); + }); +} + + + function handle_display_delete(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'display_client_id'])) { return; @@ -1321,6 +1709,10 @@ module.exports = { handle_ticker_pushall, handle_ticker_reset, handle_free_announce, + handle_emergency_announce, + handle_official_list_move, + handle_official_edit, + handle_add_officials_to_match, handle_second_call_umpire, handle_second_preparation_call_umpire, handle_second_call_servicejudge, diff --git a/bts/btp_sync.js b/bts/btp_sync.js index cac36ea..d140046 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -1234,14 +1234,30 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { app.db.umpires.findOne({ tournament_key, btp_id }, (err, cur) => { if (err) return cb(err); + + if (cur) { + + const allListsNull = !cur.is_planed_as_umpire && + !cur.is_planed_as_service_judge && + cur.umpire_on_court == null && + cur.service_judge_on_court == null && + cur.umpire_wait == null && + cur.service_judge_wait == null && + cur.umpire_pause == null && + cur.service_judge_pause == null && + cur.inactive_list == null; + + if (cur.btp_id === btp_id && cur.firstname == firstname && cur.surname == surname && - cur.country === country) { + cur.country === country && + !allListsNull) { return cb(); } else { - app.db.umpires.update({ tournament_key, btp_id }, { $set: { btp_id, firstname, surname, name, country } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { + const inactive_list = allListsNull ? Date.now() : null; + app.db.umpires.update({ tournament_key, btp_id }, { $set: { btp_id, firstname, surname, name, country, inactive_list} }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { if (err) { console.error(err); return cb(err); @@ -1261,7 +1277,18 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { name, status: 'ready', tournament_key, - country + country, + is_umpire: true, + is_service_judge: true, + is_planed_as_umpire: false, + is_planed_as_service_judge: false, + umpire_on_court: null, + service_judge_on_court: null, + umpire_wait: null, + service_judge_wait: null, + umpire_pause: null, + service_judge_pause: null, + inactive_list: Date.now() }; changed = true; app.db.umpires.insert(u, function (err, inserted_umpire) { diff --git a/bts/bupws.js b/bts/bupws.js index cd90ca7..a311044 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -153,9 +153,6 @@ async function handle_score_update(app, ws, msg) { return; } - console.log(match); - console.log(match.setup.teams); - const update = { network_score: score_data.network_score, network_team1_left:score_data.network_team1_left, diff --git a/bts/match_utils.js b/bts/match_utils.js index 925931f..6ebcf37 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -205,11 +205,39 @@ async function set_umpires_on_court(app, tournament, match, callback) { } if (setup.umpire) { - update_umpire(app, tournament.key, setup.umpire, 'oncourt', setup.called_timestamp, court_id); + const umpire = setup.umpire; + umpire.umpire_on_court = court_id; + umpire.is_planed_as_umpire = false; + umpire.is_planed_as_service_judge = false; + umpire.service_judge_on_court = null; + umpire.umpire_wait = null; + umpire.service_judge_wait = null; + umpire.umpire_pause = null; + umpire.service_judge_pause = null; + umpire.inactive_list = null; + umpire.last_time_on_court_ts = setup.called_timestamp; + umpire.status = 'oncourt'; + umpire.court_id = court_id; + + update_umpire(app, tournament.key, umpire); } if (setup.service_judge) { - update_umpire(app, tournament.key, setup.service_judge, 'oncourt', setup.called_timestamp, court_id); + const service_judge = setup.service_judge; + service_judge.service_judge_on_court = court_id; + service_judge.is_planed_as_umpire = false; + service_judge.is_planed_as_service_judge = false; + service_judge.umpire_on_court = null; + service_judge.umpire_wait = null; + service_judge.service_judge_wait = null; + service_judge.umpire_pause = null; + service_judge.service_judge_pause = null; + service_judge.inactive_list = null; + service_judge.last_time_on_court_ts = setup.called_timestamp; + service_judge.status = 'oncourt'; + service_judge.court_id = court_id; + + update_umpire(app, tournament.key, service_judge); } return callback(null); } @@ -851,49 +879,100 @@ function reset_tabletoperator_settings_at_player(app, tkey, tournament, player, } } -function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, callback) { +async function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, callback) { app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { if (err) { - return callback(err); + return reject(err); } if (cur_match.setup.umpire) { - - update_umpire(app, tournament_key, cur_match.setup.umpire, 'ready', end_ts, null); + const umpire = cur_match.setup.umpire; + umpire.umpire_on_court = null; + umpire.is_planed_as_umpire = false; + umpire.last_time_on_court_ts = end_ts; + umpire.status = 'ready'; + umpire.court_id = null; + + if(umpire.is_service_judge === true) { + umpire.service_judge_wait = end_ts; + } else if(umpire.is_umpire === true) { + umpire.umpire_wait = end_ts + 100; + } else { + umpire.inactive_list = end_ts; + } + + update_umpire(app, tournament_key, umpire, 'ready', end_ts, null); } if (cur_match.setup.service_judge) { - - update_umpire(app, tournament_key, cur_match.setup.service_judge, 'ready', end_ts, null); - } - return callback(null); + const service_judge = cur_match.setup.service_judge; + service_judge.umpire_on_court = null; + service_judge.is_planed_as_umpire = false; + service_judge.last_time_on_court_ts = end_ts; + service_judge.status = 'ready'; + service_judge.court_id = null; + + if(service_judge.is_umpire === true) { + service_judge.umpire_wait = end_ts; + } else if(service_judge.is_service_judge === true) { + service_judge.service_judge_wait = end_ts + 100; + } else { + service_judge.inactive_list = end_ts + 50; + } + update_umpire(app, tournament_key, service_judge); + } + return callback(null); }); } function set_umpire_to_standby(app, tournament_key, setup) { if (setup.umpire) { - update_umpire(app, tournament_key, setup.umpire, 'standby', null, null); + const umpire = setup.umpire; + umpire.umpire_on_court = null; + umpire.is_planed_as_umpire = false; + umpire.last_time_on_court_ts = null; + umpire.status = 'standby'; + umpire.court_id = null; + update_umpire(app, tournament_key, umpire); } if (setup.service_judge) { - update_umpire(app, tournament_key, setup.service_judge, 'standby', null, null); + const service_judge = setup.service_judge; + service_judge.umpire_on_court = null; + service_judge.is_planed_as_umpire = false; + service_judge.last_time_on_court_ts = null; + service_judge.status = 'standby'; + service_judge.court_id = null; + update_umpire(app, tournament_key, service_judge); } } -function update_umpire(app, tkey, umpire, status, last_time_on_court_ts, court_id) { - app.db.umpires.update({ tournament_key: tkey, name: umpire.name }, { $set: { last_time_on_court_ts: last_time_on_court_ts, status: status, court_id: court_id } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { - if (err) { - console.error(err); - return; - } - const admin = require('./admin'); - admin.notify_change(app, tkey, 'umpire_updated', changed_umpire); - return; - }); -} +function update_umpire(app, tkey, umpire) { + if (!umpire || !umpire._id) { + console.error('update_umpire: invalid umpire object'); + return; + } + + // Sicherheitsnetz: tournament_key immer korrekt setzen + umpire.tournament_key = tkey; + + app.db.umpires.update( + { _id: umpire._id, tournament_key: tkey }, + { $set: umpire }, + { returnUpdatedDocs: true }, + function (err, numAffected, changed_umpire) { + if (err) { + console.error(err); + return; + } + const admin = require('./admin'); + admin.notify_change(app, tkey, 'umpire_updated', changed_umpire); + } + ); +} function call_preparation_match_on_court(app, tournament_key, court_id) { return new Promise((resolve, reject) => { diff --git a/bts/utils.js b/bts/utils.js index 2631f21..cc2e9a3 100644 --- a/bts/utils.js +++ b/bts/utils.js @@ -228,6 +228,7 @@ function get_system_timezone() { return _cached_timezone; } + module.exports = { cmp, cmp_key, @@ -249,5 +250,5 @@ module.exports = { remove, root_dir, size, - values, + values }; diff --git a/static/audio/evakuierung.mp3 b/static/audio/evakuierung.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..5c59e4bd2c60c0b942d94f6c54fd5cb5a0d1d785 GIT binary patch literal 156802 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

    dc3r<%CF0$1mYv-jx_{ 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

    lzp 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≀TE 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 literal 0 HcmV?d00001 diff --git a/static/css/admin.css b/static/css/admin.css index 87cd009..6eda8ae 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -75,12 +75,37 @@ button { color: #666; } +.metadata_container{ + width: 100%; +} + +.metadata_right_container{ + min-width: 550px; + width: 20%; +} + +.metadata_right_top_container{ + display:flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; +} + +.metadata_right_container_2 > table { + margin-top: -5px; +} + +.errors_scroll_left { + direction: rtl; /* Scrollbar links */ + overflow-y: auto; + margin-top: 5px; +} .errors { + direction: ltr; /* Inhalt wieder normal */ color: #f00; white-space: pre-wrap; - max-height: 50px; - overflow-y: scroll; + max-height: 92px; } .vlink, @@ -241,7 +266,7 @@ table.striped-table { } .announcements_container{ - width: 400px; + width: 450px; } .announcements_container > form { display: flex; @@ -391,4 +416,92 @@ h2.edit { max-width: 150px; inline-size: 150px; overflow-wrap: break-word; -} \ No newline at end of file +} + +.paralel { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; /* <<< DAS ist entscheidend */ +} + +.paralel > table { + flex: 1; +} + +/* Tabellenlayout stabil halten */ +.officials_table { + table-layout: fixed; + width: 100%; +} + +/* rechte zwei Spalten fix 50px */ +.officials_table th:nth-child(2), +.officials_table td:nth-child(2), +.officials_table th:nth-child(3), +.officials_table td:nth-child(3) { + width: 50px; + min-width: 50px; + max-width: 50px; +} + +/* Checkbox-Zellen zentrieren */ +.officials_table td:nth-child(2), +.officials_table td:nth-child(3) { + text-align: center; + vertical-align: middle; +} + +/* Falls create_simple_checkbox ein erzeugt */ +.officials_table td:nth-child(2) input, +.officials_table td:nth-child(3) input { + margin: 0 auto; + display: block; +} + +/* Header-Icons zentrieren */ +.officials_table th:nth-child(2), +.officials_table th:nth-child(3) { + text-align: center; + vertical-align: middle; +} + +.officials_table th:nth-child(2) > div, +.officials_table th:nth-child(3) > div { + margin: 0 auto; +} + +.space { + width: 10px; +} + + * /* Normalzustand: gleiche Grundfarbe wie Tabelle/Rows (oder transparent) */ +.drop-zone td { + background: inherit; +} + + +/* Sobald eine Row gezogen wird, werden alle Drop-Zonen gelblich */ +tbody.drop-zones-active .drop-zone td { + background: rgba(255, 215, 0, 0.35); +} + +/* Aktuell anvisierte Zone stärker */ +tbody.drop-zones-active .drop-zone.drop-zone-hover td { + background: rgba(255, 215, 0, 0.65); +} + +tr[data-official-id] { + cursor: grab; +} + +tr.dragging { + cursor: grabbing; + opacity: 0.5; +} + +tr.table-spacer td { + padding: 0; + border: none; + background: #eee; +} diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 5cbefaa..aea208d 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -10,6 +10,24 @@ margin: 10px 0px 10px 0px; } +.announce_emergency_button { + font-size: 100%; + padding: 9px 20px; + margin: 10px 0px 10px 0px; + background-color: red; + color: whitesmoke; + font-weight: bold; +} + +.stop_emergency_button { + font-size: 100%; + padding: 9px 20px; + margin: 10px 0px 10px 0px; + background-color: green; + color: whitesmoke; + font-weight: bold; +} + .announcements_btn_container { display: flex; justify-content: space-around; @@ -280,7 +298,6 @@ .match_shuttle_count_display_active { width: 40px; - background-color: yellow; } .match_shuttle_count span::after { diff --git a/static/css/toprow.css b/static/css/toprow.css index b39d9d0..bf6095c 100644 --- a/static/css/toprow.css +++ b/static/css/toprow.css @@ -3,6 +3,7 @@ padding: 0.3em 0 0.3em 1em; background-color: #00000020; border-bottom: 1px solid black; + display: flex; } .toprow > .toprow_link, .toprow_right > .toprow_link { @@ -15,8 +16,12 @@ } .toprow_right { - float: right; margin-right: 0.5em; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-end; + flex: 1; } .toprow_right > * + * { margin-left: 1.2em; diff --git a/static/js/announcements.js b/static/js/announcements.js index 9c1f582..ee3a019 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -402,6 +402,45 @@ function createMeetingPointAnnouncement(matchSetup) { return result; } +let emergencyInterval = null; +let emergencyAudio = null; + +function emergency_announce(enable) { + + + if (enable) { + // Verhindert mehrfaches Starten + if (emergencyInterval !== null) { + return; + } + + emergencyAudio = new Audio('/static/audio/evakuierung.mp3'); + + // Sofort abspielen + emergencyAudio.play(); + + // Wiederholung alle 30 Sekunden + emergencyInterval = setInterval(() => { + emergencyAudio.currentTime = 0; + emergencyAudio.play(); + }, 20_000); + + } else { + // Timer stoppen + if (emergencyInterval !== null) { + clearInterval(emergencyInterval); + emergencyInterval = null; + } + + // Audio stoppen + if (emergencyAudio) { + emergencyAudio.pause(); + emergencyAudio.currentTime = 0; + emergencyAudio = null; + } + } +} + function announce(callArray, local) { if(!(window.localStorage.getItem('enable_free_announcements') === 'true') && !local) { return; diff --git a/static/js/cerror.js b/static/js/cerror.js index f117cd8..8c812bc 100644 --- a/static/js/cerror.js +++ b/static/js/cerror.js @@ -19,7 +19,7 @@ var cerror = (function() { function on_error(msg, script_url, line, col, err) { - show(msg); + show(getCurrentTimeString() + ' - ' + msg); } function silent(msg) { @@ -55,6 +55,16 @@ var cerror = (function() { silent, }; + function getCurrentTimeString() { + const now = new Date(); + + const hh = String(now.getHours()).padStart(2, '0'); + const mm = String(now.getMinutes()).padStart(2, '0'); + const ss = String(now.getSeconds()).padStart(2, '0'); + + return `${hh}:${mm}:${ss}`; + } + })(); /*@DEV*/ diff --git a/static/js/change.js b/static/js/change.js index dac95d2..198d878 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -31,6 +31,18 @@ function default_handler(rerender, special_funcs) { case 'free_announce': announce([c.val.text]); break; + case 'emergency_announce': + curt.enable_emergency = c.val; + emergency_announce(c.val); + if (current_view === 'show'){ + ctournament.update_emergency_btn() + } + if(curt.enable_emergency) { + cerror.silent('Evakuierungsdurchsage wird gestartet!'); + } else { + cerror.silent('Evakuierungsdurchsage wurde gestoppt'); + } + break; case 'props': { curt.name = c.val.name; curt.is_team = c.val.is_team; @@ -281,19 +293,58 @@ function default_handler(rerender, special_funcs) { u.country = umpire.country; u.status = umpire.status; u.court_id = umpire.court_id; + u.is_umpire = umpire.is_umpire; + u.is_service_judge = umpire.is_service_judge; + u.is_planed_as_umpire = umpire.is_planed_as_umpire; + u.is_planed_as_service_judge = umpire.is_planed_as_service_judge; + u.umpire_on_court = umpire.umpire_on_court; + u.service_judge_on_court = umpire.service_judge_on_court; + u.umpire_wait = umpire.umpire_wait; + u.service_judge_wait = umpire.service_judge_wait; + u.umpire_pause = umpire.umpire_pause; + u.service_judge_pause = umpire.service_judge_pause; + u.inactive_list = umpire.inactive_list; } - cumpires.ui_status(uiu.qs('.umpire_container')); + if(current_view === 'show') { + cumpires.ui_status(uiu.qs('.umpire_container')); + } + ctournament.update_officials(); break; case 'umpire_add': const added_umpire = c.val.umpire; curt.umpires.push(added_umpire); - cumpires.ui_status(uiu.qs('.umpire_container')); + if(current_view === 'show') { + cumpires.ui_status(uiu.qs('.umpire_container')); + } + ctournament.update_officials(); break; case 'umpire_removed': const removed_umpire = c.val.umpire; const ru = utils.find(curt.umpires, m => m._id === removed_umpire._id); curt.umpires.splice(curt.umpires.indexOf(ru), 1); - cumpires.ui_status(uiu.qs('.umpire_container')); + if(current_view === 'show') { + cumpires.ui_status(uiu.qs('.umpire_container')); + } + ctournament.update_officials(); + break; + case 'official_list_move': + const official = utils.find(curt.umpires, u => u._id === c.val.official_id); + official[c.val.inactive_list] = null; + official[c.val.service_judge_pause] = null; + official[c.val.umpire_pause] = null; + official[c.val.service_judge_wait] = null; + official[c.val.umpire_wait] = null; + official[c.val.service_judge_on_court] = null; + official[c.val.umpire_on_court] = null; + official[c.val.is_planed_as_service_judge] = false; + official[c.val.is_planed_as_umpirefrom_list] = false; + official[c.val.to_list] = c.val.new_ts; + ctournament.update_officials(); + break; + case 'official_edit': + const official_edit = utils.find(curt.umpires, u => u._id === c.val.official_id); + official_edit[c.val.field] = c.val.value; + ctournament.update_officials(); break; case 'score': change_score(c.val); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 6198ccd..27d1c60 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -53,6 +53,10 @@ var ci18n_de = { 'Loser': 'Verlierer', 'activate_court': 'Spielfeld nutzen', 'inactivate_court': 'Spielfeld sperren', + 'Waiting for the next game:': 'Wartet auf das nächste Spiel:', + 'Currently on break:': 'Aktuell in der Pause:', + 'Not available:': 'Nicht verfügbar:', + 'announcements:begin_to_play': 'Bitte mit dem Spielen beginnen!', 'announcements:second_call': 'Zweiter Aufruf', @@ -240,6 +244,7 @@ var ci18n_de = { 'match:edit:delete': 'Löschen', 'match:edit:now_on_court': 'Jetzt auf dem Feld', 'match:delete:really': 'Wirklich Spiel {match_id} löschen?', + 'match:add_officials': 'Schiedsrichter und Aufschlagrichter hinzufügen', 'tournament:edit:logo': 'Logo', 'tournament:edit:logo:nologo': 'Kein Logo', 'tournament:edit:logo:upload': 'Logo hochladen', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 18880f5..6e73bee 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -53,6 +53,9 @@ var ci18n_en = { 'Loser': 'Loser', 'activate_court': 'Spielfeld nutzen', 'inactivate_court': 'Spielfeld sperren', + 'Waiting for the next game:': 'Waiting for the next game:', + 'Currently on break:': 'Currently on break:', + 'Not available:': 'Not available:', 'announcements:begin_to_play': 'Start to play!', 'announcements:second_call': '"Second call', @@ -236,6 +239,7 @@ var ci18n_en = { 'match:edit:delete': 'Delete match', 'match:edit:now_on_court': 'Now on court', 'match:delete:really': 'Really delete match {match_id}?', + 'match:add_officials': 'Add umpire and service judge', 'tournament:edit:logo': 'Logo', 'tournament:edit:logo:nologo': 'No logo', 'tournament:edit:logo:upload': 'Upload logo', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index bc880cf..a976e32 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -425,7 +425,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ if (!setup.umpire && (!setup.tabletoperators || setup.tabletoperators.length == 0)) { const no_umpire_span = uiu.el(to_td, 'span', 'person'); - uiu.el(no_umpire_span, 'div', 'no_umpire', ''); + create_match_button(no_umpire_span, 'vlink no_umpire', 'match:add_officials', on_add_officials_button, match._id); uiu.el(no_umpire_span, 'span', 'match_no_umpire', ci18n('No umpire')); } @@ -495,8 +495,8 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } } - if ((style === 'default' || style === 'plain') && match.setup.now_on_court != undefined) { - const shuttle_td = uiu.el(tr, 'td', 'match_shuttle_count'); + if ((style === 'default' || style === 'plain')) { + const shuttle_td = uiu.el(tr, 'td', {'class': 'match_shuttle_count', 'data-match_id': match._id}); if(match.shuttle_count) { shuttle_td.classList.add('match_shuttle_count_display_active'); } @@ -508,12 +508,16 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ 'data-match_id': match._id, }, match.shuttle_count || ''); - const shutle_image = uiu.el(shuttle_td, 'div', 'match_shuttle_image'); + const shuttle_image = uiu.el(shuttle_td, 'div', { + 'class' : ( + 'match_shuttle_image' + ), + 'data-match_id': match._id}); if(!match.shuttle_count) { - shutle_image.style.display = 'none'; + shuttle_image.style.display = 'none'; } else { - shutle_image.style.display = 'inline-block' + shuttle_image.style.display = 'inline-block'; } } @@ -580,6 +584,21 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ return resizable_elements; } +function on_add_officials_button(e) { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'add_officials_to_match', + tournament_key: curt.key, + match_id: match._id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +} + function short_name (first_names, last_name, name) { if(first_names && last_name){ const split_name = first_names.split(" "); @@ -666,12 +685,24 @@ function update_match_score(m) { button_el.style.visibility = 'visible'; }); } - - uiu.qsEach('.match_shuttle_count_display[data-match_id=' + JSON.stringify(m._id) + ']', function(el) { - uiu.text(el, m.shuttle_count || ''); + uiu.qsEach('.match_shuttle_count[data-match_id=' + JSON.stringify(m._id) + ']', function(el) { uiu.setClass(el, 'match_shuttle_count_display_active', !!m.shuttle_count); }); + + uiu.qsEach('.match_shuttle_count_number[data-match_id=' + JSON.stringify(m._id) + ']', function(el) { + uiu.text(el, m.shuttle_count || ''); + }); + + uiu.qsEach('.match_shuttle_image[data-match_id=' + JSON.stringify(m._id) + ']', function(shuttle_image) { + if(!m.shuttle_count) { + shuttle_image.style.display = 'none'; + } + else { + shuttle_image.style.display = 'inline-block'; + } + + }); } function on_match_confirm_button_click(e) { @@ -980,12 +1011,12 @@ function insert_new_match_row(m, section) { uiu.qsEach('.finished_container', (finished_container) => { const tbody = finished_container.querySelector('.match_table > tbody'); const match_row_el = uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); - render_match_row(match_row_el, m, null, 'default', false, true); + render_match_row(match_row_el, m, null, 'default', false, curt.tabletoperator_enabled); for (const child of tbody.children) { const child_btp_id = child.dataset.match_id; const child_match = utils.find(curt.matches, m => 'btp_'+m.btp_id === child_btp_id); if(child_match) { - if(cmp_match_order(m, child_match) < 0) { + if(cmp_end_ts_match_order(m, child_match) < 0) { tbody.insertBefore(match_row_el, child); break; } @@ -997,12 +1028,12 @@ function insert_new_match_row(m, section) { uiu.qsEach('.unassigned_container', (unassigned_container) => { const tbody = unassigned_container.querySelector('.match_table > tbody'); const match_row_el = uiu.el(tbody, 'tr', {'class' : 'match highlight_' + m.setup.highlight , 'data-match_id': m._id}); - render_match_row(match_row_el, m, null, 'unasigned', true, true); + render_match_row(match_row_el, m, null, 'unasigned', true, curt.tabletoperator_enabled); for (const child of tbody.children) { const child_btp_id = child.dataset.match_id; const child_match = utils.find(curt.matches, m => 'btp_'+m.btp_id === child_btp_id); if(child_match) { - if(cmp_match_order(m, child_match) < 0) { + if(cmp_scheduled_match_order(m, child_match) < 0) { tbody.insertBefore(match_row_el, child); break; } @@ -1016,7 +1047,7 @@ function insert_new_match_row(m, section) { match_row_el.innerHTML = ""; const closest = match_row_el.closest('.main_upcoming'); if(Boolean(closest)) { - render_match_row(match_row_el, m, court, 'public'); + render_match_row(match_row_el, m, court, 'public', ); } else { render_match_row(match_row_el, m, court, 'plain', false, false); } @@ -1031,11 +1062,11 @@ function update_match_row(m, new_section) { switch (new_section) { case 'finished': - render_match_row(match_row_el, m, null, 'default', false, true); + render_match_row(match_row_el, m, null, 'default', false, curt.tabletoperator_enabled); break; case 'unassigned': match_row_el.setAttribute('class', 'match highlight_' + (m.setup.highlight ? m.setup.highlight : 0)); - render_match_row(match_row_el, m, null, 'unasigned', true, true); + render_match_row(match_row_el, m, null, 'unasigned', true, curt.tabletoperator_enabled); break; default: const court = utils.find(curt.courts, c => c._id === m.setup.court_id); @@ -1152,7 +1183,7 @@ function _extract_match_timer_state(match) { return rs; } -function cmp_match_order(m1, m2) { +function cmp_scheduled_match_order(m1, m2) { const time_str1 = m1.setup.scheduled_time_str; const time_str2 = m2.setup.scheduled_time_str; @@ -1184,8 +1215,21 @@ function cmp_match_order(m1, m2) { return cbts_utils.cmp(m1.setup.match_num, m2.setup.match_num); } +function cmp_end_ts_match_order(m1, m2) { + var m1_ts = m1.end_ts; + var m2_ts = m2.end_ts; + + if(!m1_ts) { + m1_ts = zoned_time_to_utc_timestamp(m1.setup.scheduled_date, m1.setup.scheduled_time_str, 'Europe/Berlin') / 2; + } + if(!m2_ts) { + m2_ts = zoned_time_to_utc_timestamp(m2.setup.scheduled_date, m2.setup.scheduled_time_str, 'Europe/Berlin') / 2; + } + return m1_ts - m2_ts +} + function prepare_render(t) { - t.matches.sort((m1, m2) => {return cmp_match_order(m1, m2)}); + t.matches.sort((m1, m2) => {return cmp_scheduled_match_order(m1, m2)}); t.courts_by_id = {}; for (const c of t.courts) { @@ -1776,7 +1820,7 @@ function render_unassigned(container) { uiu.el(container, 'h3', 'section', ci18n('Unassigned Matches')); const unassigned_matches = curt.matches.filter(m => calc_section(m) === 'unassigned'); - render_match_table(container, unassigned_matches, 'unasigned', true, true); + render_match_table(container, unassigned_matches, 'unasigned', true, curt.tabletoperator_enabled); } function render_upcoming_matches(container) { @@ -1791,8 +1835,6 @@ function render_upcoming_matches(container) { const upcoming_table = uiu.el(container, 'table', 'upcoming_table'); const upcoming_tbody = uiu.el(upcoming_table, 'tbody', 'upcoming_tbody'); const unassigned_matches = curt.matches.filter(m => calc_section(m) === 'unassigned'); - - console.log(unassigned_matches); var resizable_rows = []; @@ -1813,12 +1855,73 @@ function render_upcoming_matches(container) { }); } + +function zoned_time_to_utc_timestamp(dateStr, timeStr, timeZone) { + if (!dateStr) { + return 0; + } + + if (!timeStr) { + return 0; + } + + const [year, month, day] = dateStr.split('-').map(Number); + const [hour, minute] = timeStr.split(':').map(Number); + + // "Guess": als ob die eingegebene Zeit UTC wäre + const utcGuessMs = Date.UTC(year, month - 1, day, hour, minute, 0); + + const formatter = new Intl.DateTimeFormat('en-US', { + timeZone, + hour12: false, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + + // Wie sieht dieser utcGuess in der Ziel-Zeitzone aus? + const parts = Object.fromEntries( + formatter.formatToParts(new Date(utcGuessMs)).map(p => [p.type, p.value]) + ); + + // Diese "Zonen-Zeit" interpretieren wir als UTC, um den Offset zu bekommen + const asIfUtcMs = Date.UTC( + Number(parts.year), + Number(parts.month) - 1, + Number(parts.day), + Number(parts.hour), + Number(parts.minute), + Number(parts.second) + ); + + // Offset = (Zonen-Darstellung als UTC) - (echte UTC) + const offsetMs = asIfUtcMs - utcGuessMs; + + // Gesucht: Zeitpunkt, der in der Zone die gewünschte lokale Zeit ergibt + return utcGuessMs - offsetMs; +} + + function render_finished(container) { uiu.empty(container); uiu.el(container, 'h3', 'section', ci18n('Finished Matches')); - const matches = curt.matches.filter(m => calc_section(m) === 'finished').sort((a, b) => {return b.end_ts - a.end_ts}); - render_match_table(container, matches, 'default', false, true); + const matches = curt.matches.filter(m => calc_section(m) === 'finished').sort((a, b) => { + var a_ts = a.end_ts; + var b_ts = b.end_ts; + + if(!a_ts) { + a_ts = zoned_time_to_utc_timestamp(a.setup.scheduled_date, a.setup.scheduled_time_str, 'Europe/Berlin'); + } + if(!b_ts) { + b_ts = zoned_time_to_utc_timestamp(b.setup.scheduled_date, b.setup.scheduled_time_str, 'Europe/Berlin'); + } + return b_ts - a_ts + }); + render_match_table(container, matches, 'default', false, curt.tabletoperator_enabled); } function render_courts(container, style) { @@ -1883,6 +1986,11 @@ function update_tables(location_id, enabled) { function render_empty_court_row(tr, court, style, is_droppable) { tr.setAttribute("data-style", style); + if (!court) { + console.warn('court is not set!'); + + } + const is_active = court.is_active; if(style != 'public') { @@ -2471,7 +2579,7 @@ function render_create(container) { return { add_match, calc_section, - cmp_match_order, + cmp_match_order: cmp_scheduled_match_order, prepare_render, render_create, render_finished, diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 822332e..05bb8a6 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -1,6 +1,7 @@ 'use strict'; var curt; // current tournament +let current_view = null; var ctournament = (function() { function _route_single(rex, func, handler) { @@ -141,7 +142,10 @@ var ctournament = (function() { m.btp_winner = cval.btp_winner; m.setup = cval.setup; - cmatch.update_players(m); + if(current_view == 'show'){ + cmatch.update_players(m); + } + } function remove_match(c) { @@ -326,7 +330,9 @@ var ctournament = (function() { cmatch.render_finished(uiu.qs('.finished_container')); } function _show_render_tabletoperators() { - ctabletoperator.render_unassigned(uiu.qs('.unassigned_tableoperators_container')); + if(curt.tabletoperator_enabled) { + ctabletoperator.render_unassigned(uiu.qs('.unassigned_tableoperators_container')); + } } function _show_render_umpires() { @@ -417,6 +423,12 @@ var ctournament = (function() { class: 'announce_button', id: 'remote_announce_btn' }, 'Remote Abspielen'); + + const emergency_btn = uiu.el(btn_container, 'button', { + type: 'submit', + class: !curt.enable_emergency ? 'announce_emergency_button' : 'stop_emergency_button', + id: 'announce_emergency_btn' + }, !curt.enable_emergency ? 'Evakuierung Abspielen' : 'Evakuierung Stoppen'); // Lokales Abspielen (z. B. mit deiner announce-Funktion) local_btn.addEventListener('click', function () { @@ -426,6 +438,22 @@ var ctournament = (function() { // Lokale Ansage abspielen announce([text], true); // ← Diese Funktion muss bei dir lokal definiert sein }); + + emergency_btn.addEventListener("click", () => { + const bestaetigt = confirm(!curt.enable_emergency ? "Soll wirklich evakuiert werden?" : "Soll die Evakuierung wirklich abgebrochen werden?"); + + if (bestaetigt) { + send({ + type: 'emergency_announce', + tournament_key: curt.key, + enable: !curt.enable_emergency + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + } + }); // Remote Abspielen form_utils.onsubmit(form, function (d) { @@ -444,6 +472,21 @@ var ctournament = (function() { }); } + function update_emergency_btn() { + const btn = document.getElementById('announce_emergency_btn'); + if (!btn) return; + + if (curt.enable_emergency) { + btn.classList.remove('announce_emergency_button'); + btn.classList.add('stop_emergency_button'); + btn.textContent = 'Evakuierung Stoppen'; + } else { + btn.classList.remove('stop_emergency_button'); + btn.classList.add('announce_emergency_button'); + btn.textContent = 'Evakuierung Abspielen'; + } + } + // function render_enable_announcement(target) { // const announcements = uiu.el(target, 'div', 'enable_announcements_container'); // const heading = uiu.el(announcements, 'h3', {}, 'Ansagen auf diesem Gerät'); @@ -568,6 +611,7 @@ var ctournament = (function() { }); } function ui_show() { + current_view = 'show' crouting.set('t/:key/', { key: curt.key }); const bup_lang = ((curt.language && curt.language !== 'auto') ? '&lang=' + encodeURIComponent(curt.language) : ''); const bup_dm_style = '&dm_style=' + encodeURIComponent(curt.dm_style || 'international'); @@ -600,7 +644,10 @@ var ctournament = (function() { const meta_div = uiu.el(main, 'div', 'metadata_container'); - uiu.el(meta_div, 'div', 'unassigned_tableoperators_container'); + + if(curt.tabletoperator_enabled) { + uiu.el(meta_div, 'div', 'unassigned_tableoperators_container'); + } uiu.el(meta_div, 'div', 'umpire_container'); render_announcement_formular(meta_div); @@ -610,9 +657,15 @@ var ctournament = (function() { const meta_right_div = uiu.el(meta_div, 'div', 'metadata_right_container'); - render_enable_location_courts(meta_right_div, curt.locations); + const meta_right_top_div = uiu.el(meta_right_div, 'div', 'metadata_right_top_container'); + + render_enable_location_courts(meta_right_top_div, curt.locations); - render_settings(meta_right_div); + render_settings(meta_right_top_div); + + const errors_scroll_left_div = uiu.el(meta_right_div, 'div', 'errors_scroll_left'); + + uiu.el(errors_scroll_left_div, 'div', 'errors'); cmatch.prepare_render(curt); @@ -682,8 +735,6 @@ var ctournament = (function() { const ticker_push_btn = uiu.el(td, 'button', 'tournament_ticker_push vlink', ci18n('update ticker')); ticker_push_btn.addEventListener('click', ui_ticker_push); } - - uiu.el(settings_div, 'div', 'errors'); } function btp_status_changed(c) { @@ -734,6 +785,7 @@ var ctournament = (function() { } function ui_edit() { + current_view = 'edit'; crouting.set('t/:key/edit', { key: curt.key }); toprow.set([{ label: ci18n('Tournaments'), @@ -1071,6 +1123,13 @@ var ctournament = (function() { input.upcoming_animation_pause = create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_animation_pause', 1, 20, 4, 1); input.upcoming_matches_max_count = create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_max_count', 10, 50, 15, 1); } + +// officials_host ###################################################################################################### + + // irgendwo in ui_edit (oder wo du initial renderst) + const officials_host = uiu.el(form, 'div', { id: 'officials_host' }); + update_official_tables(officials_host); // initial + später auch für Updates + // devices-div################################################################################## @@ -1193,6 +1252,7 @@ var ctournament = (function() { } _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit, change.default_handler(_update_all_ui_elements_edit, { update_general_displaysettings: update_general_displaysettings, + update_player_status: update_player_status, })); function send_props(input, callback) { @@ -2266,6 +2326,518 @@ var ctournament = (function() { return; }; +/* ============================================================ + * DROP-ZONES (schmale Reihen zum Droppen) + * ============================================================ */ + +function add_drop_zones_to_tbody(tbody, { + row_selector = 'tr', + zone_class = 'drop-zone', + zone_active_class = 'drop-zones-active', + is_header_row = (tr) => !!tr.querySelector('th'), + col_count = 3, + on_zone_dragover = (tbody, insertBeforeRow, e) => {}, +} = {}) { + // alte Zonen entfernen + for (const z of [...tbody.querySelectorAll(`tr.${zone_class}`)]) z.remove(); + tbody.classList.add(zone_active_class); + + const rows = [...tbody.querySelectorAll(row_selector)]; + const header = rows.find(is_header_row) || null; + + // Spacer erkennen (kommt von ensure_min_table_height) + const spacer = tbody.querySelector('tr.table-spacer') || null; + + // Datenzeilen: data-official-id (und NICHT spacer) + const dataRows = rows.filter(tr => + tr !== header && + tr !== spacer && + tr.getAttribute('data-official-id') + ); + + function makeZone(insertBeforeRow, heightPx = null) { + const ztr = document.createElement('tr'); + ztr.className = zone_class; + + const ztd = document.createElement('td'); + ztd.colSpan = col_count; + ztr.appendChild(ztd); + + if (heightPx != null) { + ztd.style.height = `${heightPx}px`; + ztd.style.padding = '0'; + ztd.style.border = 'none'; + } + + ztr.addEventListener('dragover', (e) => { + e.preventDefault(); + on_zone_dragover(tbody, insertBeforeRow, e); + }); + + ztr.addEventListener('drop', (e) => e.preventDefault()); + ztr.addEventListener('dragenter', () => ztr.classList.add('drop-zone-hover')); + ztr.addEventListener('dragleave', () => ztr.classList.remove('drop-zone-hover')); + + return ztr; + } + + // Wenn Spacer existiert: Spacer zu einer einzigen großen Dropzone machen + if (spacer) { + // Spacer-Höhe ermitteln (td height oder inline style) + const spacerTd = spacer.querySelector('td'); + const h = spacerTd ? spacerTd.getBoundingClientRect().height : 0; + + // Spacer entfernen und als Dropzone mit gleicher Höhe ersetzen. + // Drop soll "unter letzte Datenzeile" sein -> insertBeforeRow = null + spacer.remove(); + + const bigZone = makeZone(null, Math.max(0, Math.ceil(h))); + tbody.appendChild(bigZone); + return; + } + + // Normalfall ohne Spacer: Top/Between/Bottom Zonen + const topZone = makeZone(dataRows[0] || null); + + if (header) { + if (header.nextSibling) tbody.insertBefore(topZone, header.nextSibling); + else tbody.appendChild(topZone); + } else { + tbody.insertBefore(topZone, tbody.firstChild); + } + + for (let i = 0; i < dataRows.length; i++) { + const current = dataRows[i]; + const next = dataRows[i + 1] || null; + const zone = makeZone(next); + if (current.nextSibling) tbody.insertBefore(zone, current.nextSibling); + else tbody.appendChild(zone); + } +} + + +function remove_drop_zones_from_tbody(tbody, { + zone_class = 'drop-zone', + zone_active_class = 'drop-zones-active', +} = {}) { + tbody.classList.remove(zone_active_class); + for (const z of [...tbody.querySelectorAll(`tr.${zone_class}`)]) { + z.remove(); + } +} + +/* ============================================================ + * MULTI-TABLE DND (mit Drop-Zones, zwischen Tabellen) + * ============================================================ */ + +function enable_multitable_row_dragdrop(tbodies, { + row_selector = 'tr', + table_id_attr = 'data-table-id', + row_id_attr = 'data-official-id', + is_header_row = (tr) => !!tr.querySelector('th'), + can_drag_row = (tr) => !is_header_row(tr) && !tr.classList.contains('drop-zone'), + col_count = 3, + on_move = ({ row_id, from_table, to_table, from_order, to_order }) => {}, +} = {}) { + let dragged_tr = null; + let from_tbody = null; + + function set_dragging(tr, isDragging) { + if (!tr) return; + tr.classList.toggle('dragging', !!isDragging); + } + + function get_table_id(tbody) { + return tbody?.getAttribute(table_id_attr) || ''; + } + + function get_order_ids(tbody) { + if (!tbody) return []; + return [...tbody.querySelectorAll(row_selector)] + .filter(tr => !is_header_row(tr) && !tr.classList.contains('drop-zone')) + .map(tr => tr.getAttribute(row_id_attr)) + .filter(Boolean); + } + + // >>> NEU: ans Ende bedeutet "vor Spacer", falls vorhanden + function append_before_spacer(tbody, row) { + const spacer = tbody.querySelector('tr.table-spacer'); + if (spacer) tbody.insertBefore(row, spacer); + else tbody.appendChild(row); + } + + function insert_dragged_into_tbody(tbody, insertBeforeRow) { + if (!dragged_tr) return; + + if (insertBeforeRow == null) { + // ans Ende (unter letzte Datenzeile) => aber vor Spacer, falls vorhanden + append_before_spacer(tbody, dragged_tr); + } else { + tbody.insertBefore(dragged_tr, insertBeforeRow); + } + } + + // Drop-Zones beim Start aktivieren + function activate_drop_zones() { + for (const tbody of tbodies) { + add_drop_zones_to_tbody(tbody, { + row_selector, + is_header_row, + col_count, + on_zone_dragover: (target_tbody, insertBeforeRow, e) => { + if (!dragged_tr) return; + insert_dragged_into_tbody(target_tbody, insertBeforeRow); + } + }); + } + } + + function deactivate_drop_zones() { + for (const tbody of tbodies) { + remove_drop_zones_from_tbody(tbody); + } + } + + // Rows draggable machen + for (const tbody of tbodies) { + for (const tr of tbody.querySelectorAll(row_selector)) { + if (!can_drag_row(tr)) continue; + + tr.draggable = true; + + tr.addEventListener('dragstart', (e) => { + dragged_tr = tr; + from_tbody = tr.closest('tbody'); + set_dragging(tr, true); + + // Drop-Zones global aktivieren + activate_drop_zones(); + + // Firefox benötigt Daten im dataTransfer + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', tr.getAttribute(row_id_attr) || ''); + } + }); + + tr.addEventListener('dragend', () => { + if (!dragged_tr) return; + + set_dragging(dragged_tr, false); + + // Drop-Zones entfernen + deactivate_drop_zones(); + + const row_id = dragged_tr.getAttribute(row_id_attr) || ''; + const to_tbody = dragged_tr.closest('tbody'); + + const from_table = get_table_id(from_tbody); + const to_table = get_table_id(to_tbody); + + const from_order = get_order_ids(from_tbody); + const to_order = get_order_ids(to_tbody); + + dragged_tr = null; + from_tbody = null; + + on_move({ row_id, from_table, to_table, from_order, to_order }); + }); + } + } + +for (const tbody of tbodies) { + tbody.addEventListener('dragover', (e) => { + if (!dragged_tr) return; + e.preventDefault(); + + // 1) Wenn wir über dem Spacer sind: vor Spacer einfügen (Preview korrekt) + const spacer_tr = e.target.closest ? e.target.closest('tr.table-spacer') : null; + if (spacer_tr) { + tbody.insertBefore(dragged_tr, spacer_tr); + return; + } + + // 2) Wenn wir über dem Header sind: vor die erste Datenzeile einfügen + const header_tr = e.target.closest ? e.target.closest('tr') : null; + const isHeader = header_tr && header_tr.querySelector && header_tr.querySelector('th'); + if (isHeader) { + const first_data = tbody.querySelector(`tr[${row_id_attr}]:not(.drop-zone)`); + if (first_data) { + tbody.insertBefore(dragged_tr, first_data); + } else { + // keine Datenzeile vorhanden -> vor Spacer falls vorhanden, sonst ans Ende + const spacer = tbody.querySelector('tr.table-spacer'); + if (spacer) tbody.insertBefore(dragged_tr, spacer); + else tbody.appendChild(dragged_tr); + } + return; + } + + // 3) Optional: Wenn über einer Datenzeile, nahe oben -> davor einfügen + // (macht "oben" noch leichter zu treffen, ohne Drop-Zones zu vergrößern) + const data_tr = e.target.closest ? e.target.closest(`tr[${row_id_attr}]`) : null; + if (data_tr && data_tr !== dragged_tr) { + const box = data_tr.getBoundingClientRect(); + const before = e.clientY < (box.top + box.height / 2); + if (before) tbody.insertBefore(dragged_tr, data_tr); + else tbody.insertBefore(dragged_tr, data_tr.nextSibling); + return; + } + + // sonst nichts tun: Drop-Zones übernehmen die Präzision + }); + + tbody.addEventListener('drop', (e) => { + if (!dragged_tr) return; + e.preventDefault(); + }); +} +} + + +/* ============================================================ + * DEINE TABELLE (pro Feld) - gibt TBODY zurück + * ============================================================ */ + +function render_officials_by_timestamp(main, { + title = null, + officials, + timestamp_field, + min_height_px = 240 +}) { + if (title) { + uiu.el(main, 'h2', 'edit', title); + } + + const rows = officials + .filter(o => o[timestamp_field] !== null) + .sort((a, b) => a[timestamp_field] - b[timestamp_field]); + + const table = uiu.el(main, 'table', 'officials_table'); + const tbody = uiu.el(table, 'tbody', { 'data-table-id': timestamp_field }); + + /* ---------- Header ---------- */ + const trHead = uiu.el(tbody, 'tr'); + uiu.el(trHead, 'th', {}, 'Name'); + + const thUmpire = uiu.el(trHead, 'th', {}); + uiu.el(thUmpire, 'div', { class: 'umpire' }); + + const thService = uiu.el(trHead, 'th', {}); + uiu.el(thService, 'div', { class: 'service_judge' }); + + /* ---------- Data rows ---------- */ + for (const o of rows) { + const tr = uiu.el(tbody, 'tr', { 'data-official-id': o._id }); + + uiu.el(tr, 'td', {}, o.name || `${o.firstname} ${o.surname}`.trim()); + + const umpire_td = uiu.el(tr, 'td', {}); + const umpire_cb = create_simple_checkbox( + umpire_td, + { name: 'umpire_cb', 'data-official-id': o._id }, + !!o.is_umpire + ); + umpire_cb.addEventListener('change', (e) => { + send({ + type: 'official_edit', + tournament_key: o.tournament_key, + official_id: o._id, + field: 'is_umpire', + value: e.target.checked + }, err => { if (err) return cerror.net(err); }); + }); + + const service_td = uiu.el(tr, 'td', {}); + const service_cb = create_simple_checkbox( + service_td, + { name: 'service_judge_cb', 'data-official-id': o._id }, + !!o.is_service_judge + ); + service_cb.addEventListener('change', (e) => { + send({ + type: 'official_edit', + tournament_key: o.tournament_key, + official_id: o._id, + field: 'is_service_judge', + value: e.target.checked + }, err => { if (err) return cerror.net(err); }); + }); + } + + /* ---------- Mindesthöhe per Spacer ---------- */ + function ensure_min_table_height() { + const old = tbody.querySelector('tr.table-spacer'); + if (old) old.remove(); + + if (!document.body.contains(table)) return; + + requestAnimationFrame(() => { + if (!document.body.contains(table)) return; + + const current = table.getBoundingClientRect().height; + const missing = Math.max(0, min_height_px - current); + if (missing <= 0) return; + + const spacer_tr = document.createElement('tr'); + spacer_tr.className = 'table-spacer'; + + const spacer_td = document.createElement('td'); + spacer_td.colSpan = trHead.children.length; + spacer_td.style.height = `${Math.ceil(missing)}px`; + + spacer_tr.appendChild(spacer_td); + tbody.appendChild(spacer_tr); + }); + } + + ensure_min_table_height(); + + // für globalen Resize-Recalc + table._ensureMinHeight = ensure_min_table_height; + + return { table, tbody }; +} + +function enable_min_height_resize_recalc(tables) { + window._officialMinHeightTables = tables; + + if (window._officialMinHeightResizeHandlerInstalled) return; + window._officialMinHeightResizeHandlerInstalled = true; + + window.addEventListener('resize', () => { + const list = window._officialMinHeightTables || []; + for (const t of list) { + if (t && t._ensureMinHeight) t._ensureMinHeight(); + } + }); +} + +function update_official_tables(officials_host) { + // officials_host: DOM-Element, in das die gesamte Officials-UI gerendert wird + // curt.umpires ist hier verfügbar (wie von dir beschrieben) + + // alles neu bauen + officials_host.innerHTML = ''; + + const tbodies = []; + const tables = []; + + const officials_div = uiu.el(officials_host, 'div', 'settings'); + uiu.el(officials_div, 'h2', 'edit', ci18n('Umpire:')); + + uiu.el(officials_div, 'h3', 'edit', ci18n('Waiting for the next game:')); + const waiting_officials_div = uiu.el(officials_div, 'div', 'paralel'); + + { + const r = render_officials_by_timestamp(waiting_officials_div, { + officials: curt.umpires, + timestamp_field: 'umpire_wait' + }); + tbodies.push(r.tbody); + tables.push(r.table); + } + + uiu.el(waiting_officials_div, 'div', 'space'); + + { + const r = render_officials_by_timestamp(waiting_officials_div, { + officials: curt.umpires, + timestamp_field: 'service_judge_wait' + }); + tbodies.push(r.tbody); + tables.push(r.table); + } + + uiu.el(officials_div, 'h3', 'edit', ci18n('Currently on break:')); + const paused_officials_div = uiu.el(officials_div, 'div', 'paralel'); + + { + const r = render_officials_by_timestamp(paused_officials_div, { + officials: curt.umpires, + timestamp_field: 'umpire_pause' + }); + tbodies.push(r.tbody); + tables.push(r.table); + } + + uiu.el(paused_officials_div, 'div', 'space'); + + { + const r = render_officials_by_timestamp(paused_officials_div, { + officials: curt.umpires, + timestamp_field: 'service_judge_pause' + }); + tbodies.push(r.tbody); + tables.push(r.table); + } + + uiu.el(officials_div, 'h3', 'edit', ci18n('Not available:')); + { + const r = render_officials_by_timestamp(officials_div, { + officials: curt.umpires, + timestamp_field: 'inactive_list' + }); + tbodies.push(r.tbody); + tables.push(r.table); + } + + // Map neu aufbauen (wichtig, weil curt.umpires aktualisiert ist) + const officialById = new Map(); + for (const o of curt.umpires) { + officialById.set(o._id, o); + } + + // Drag & Drop (jedes Render neu aktivieren) + enable_multitable_row_dragdrop(tbodies, { + col_count: 3, + is_header_row: (tr) => !!tr.querySelector('th'), + on_move: ({ row_id, from_table, to_table, from_order, to_order }) => { + + // Mindesthöhe nach DOM-Move neu setzen + for (const t of tables) { + if (t && t._ensureMinHeight) t._ensureMinHeight(); + } + + // prev/next aus der Ziel-Reihenfolge + const idx = to_order.indexOf(row_id); + const prev_id = idx > 0 ? to_order[idx - 1] : null; + const next_id = (idx >= 0 && idx < to_order.length - 1) ? to_order[idx + 1] : null; + + const prev_btp_id = prev_id ? officialById.get(prev_id)?.btp_id : null; + const next_btp_id = next_id ? officialById.get(next_id)?.btp_id : null; + + send({ + type: 'official_list_move', + tournament_key: curt.key, + official_id: row_id, + from_list: from_table, + to_list: to_table, + prev_btp_id, + next_btp_id + }, (err) => { + if (err) return cerror.net(err); + }); + } + }); + + // Mindesthöhe bei Resize neu berechnen (globaler Handler) + enable_min_height_resize_recalc(tables); + + // Optional: direkt nach Render einmal neu setzen (falls Fonts/Layout verzögert) + for (const t of tables) { + if (t && t._ensureMinHeight) t._ensureMinHeight(); + } +} + +function update_officials() { + if(current_view === 'edit') { + update_official_tables(document.getElementById('officials_host')); + } + return; +} + + function render_courts(main) { uiu.el(main, 'h2', 'edit', ci18n('tournament:edit:courts')); @@ -2519,21 +3091,25 @@ var ctournament = (function() { } function ui_upcoming() { + current_view = 'upcoming'; const main = ui_match_screens('t/:key/upcoming'); render_upcoming(main); } function ui_current_matches() { + current_view = 'current_matches'; const main = ui_match_screens('t/:key/current_matches'); render_current_matches(main); } function ui_next_matches() { + current_view = 'next_matches'; const main = ui_match_screens('t/:key/next_matches'); render_next_matches(main); } function ui_match_screens(route) { + current_view = 'match_screen'; crouting.set(route, { key: curt.key }); toprow.hide(); const main = uiu.qs('.main'); @@ -2760,12 +3336,14 @@ var ctournament = (function() { ui_list, add_match, update_match, + update_officials, update_upcoming_match, update_logo, update_display, update_location, update_location_logo, update_court, + update_emergency_btn, btp_status_changed, ticker_status_changed, bts_status_changed, From e9ea66c804a5e148243ffdf9751c42c5fd826cbf Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 21 Feb 2026 22:51:28 +0100 Subject: [PATCH 208/224] Add querry ?location=Halle to filter current_matches, next_matces and upcoming views by url --- static/js/cmatch.js | 47 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index bc880cf..599a00d 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -205,7 +205,6 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ return; } - if (!court && match.setup.court_id) { court = curt.courts_by_id[match.setup.court_id]; } @@ -1780,7 +1779,6 @@ function render_unassigned(container) { } function render_upcoming_matches(container) { - console.warn("Rendere upcoming"); const UPCOMING_MATCH_COUNT = parseInt(curt.upcoming_matches_max_count ? curt.upcoming_matches_max_count : 15); uiu.empty(container); @@ -1790,10 +1788,26 @@ function render_upcoming_matches(container) { const upcoming_table = uiu.el(container, 'table', 'upcoming_table'); const upcoming_tbody = uiu.el(upcoming_table, 'tbody', 'upcoming_tbody'); - const unassigned_matches = curt.matches.filter(m => calc_section(m) === 'unassigned'); - console.log(unassigned_matches); - + const locationById = Object.fromEntries( + curt.locations.map(l => [l._id, l]) + ); + + const params = new URLSearchParams(window.location.search); + const param_location = params.get("location"); + + const unassigned_matches = curt.matches.filter(m => { + if (calc_section(m) !== 'unassigned') return false; + if (!param_location) return true; + + const loc = locationById[m.setup.location_id]; + + // KEINE Location → trotzdem anzeigen + if (!loc) return true; + + // Location vorhanden → muss matchen + return loc.name === param_location; + }); var resizable_rows = []; for (const match of unassigned_matches.slice(0, UPCOMING_MATCH_COUNT)) { @@ -1830,7 +1844,28 @@ function render_courts(container, style) { const table = uiu.el(container, 'table', 'match_table'); const tbody = uiu.el(table, 'tbody'); var resizable_rows = []; - for (const c of curt.courts) { + + const locationById = Object.fromEntries( + curt.locations.map(l => [l._id, l]) + ); + + const params = new URLSearchParams(window.location.search); + const param_location = params.get("location"); + + + const courts = curt.courts.filter(c => { + if (!param_location) return true; + + const loc = locationById[c.location_id]; + + // KEINE Location → trotzdem anzeigen + if (!loc) return true; + + // Location vorhanden → muss matchen + return loc.name === param_location; + }); + + for (const c of courts) { const expected_section = 'court_' + c._id; const court_matches = curt.matches.filter(m => calc_section(m) === expected_section); From 2ba5ec7f5e5800ad694cac1720ee9329e4592f04 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 24 Jan 2026 14:00:11 +0100 Subject: [PATCH 209/224] Import the scoring format from BTP and use this scoring format for all internal calculations. --- bts/admin.js | 13 +- bts/btp_conn.js | 2 +- bts/btp_parse.js | 6 + bts/btp_sync.js | 571 +++++++++++++++++++++++++++++++++++-- bts/bupws.js | 37 +-- bts/match_utils.js | 33 ++- bts/ticker_conn.js | 2 +- static/cbts.html | 3 +- static/css/admin.css | 95 +++++- static/js/change.js | 35 ++- static/js/ci18n_de.js | 32 ++- static/js/ci18n_en.js | 30 +- static/js/cmatch.js | 56 +--- static/js/ctournament.js | 469 ++++++++++++++++++++++++++++++ static/js/match_scoring.js | 92 ++++++ test/test_btp_sync.js | 274 ++++++++++++++++++ test/test_cmatch.js | 36 +++ ticker/tdata.js | 15 +- 18 files changed, 1680 insertions(+), 121 deletions(-) create mode 100644 static/js/match_scoring.js create mode 100644 test/test_btp_sync.js create mode 100644 test/test_cmatch.js diff --git a/bts/admin.js b/bts/admin.js index 35b9c48..fd27589 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -80,7 +80,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'annoncement_include_event', 'annoncement_include_round','annoncement_include_matchnumber', 'preparation_meetingpoint_enabled', 'preparation_tabletoperator_setup_enabled', 'call_preparation_matches_automatically_enabled', 'call_next_possible_scheduled_match_in_preparation' , - 'logo_background_color', 'logo_foreground_color']); + 'logo_background_color', 'logo_foreground_color', 'scoring_formats']); if (msg.props.btp_timezone) { props.btp_timezone = msg.props.btp_timezone === 'system' ? undefined : msg.props.btp_timezone; @@ -353,6 +353,13 @@ function handle_tournament_get(app, ws, msg) { cb(err); }); }], function(err) { + if (tournament.scoring_formats && Array.isArray(tournament.scoring_formats.formats)) { + const btp_sync = require('./btp_sync'); + tournament.scoring_formats = { + ...tournament.scoring_formats, + formats: tournament.scoring_formats.formats.map(f => btp_sync._sanitize_scoring_format(f)), + }; + } tournament.btp_status = btp_manager.get_status(tournament.key); tournament.ticker_status = ticker_manager.get_status(tournament.key); _annotate_tournament(tournament); @@ -392,6 +399,7 @@ function _extract_setup(msg_setup) { 'links', 'scheduled_time_str', 'scheduled_date', + 'scoring_format', 'called_timestamp', 'preparation_call_timestamp', 'location_id', @@ -406,7 +414,6 @@ function _extract_setup(msg_setup) { if (!setup.match_name && setup.match_num) { setup.match_name = '# ' + setup.match_num; } - setup.counting = '3x21'; return setup; } @@ -1343,4 +1350,4 @@ module.exports = { generate_tournament_web_url, on_close, on_connect, -}; \ No newline at end of file +}; diff --git a/bts/btp_conn.js b/bts/btp_conn.js index e74f090..8201c0e 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -260,7 +260,7 @@ class BTPConn { }); }, (umpire_btp_id, service_judge_btp_id, cb) => { - if (!match.setup || !match.setup.court_id) { + if (!match.setup || !match.setup.court_id || match.setup.now_on_court !== true) { return cb(null, umpire_btp_id, service_judge_btp_id, null); } diff --git a/bts/btp_parse.js b/bts/btp_parse.js index 5dd9171..aba3c47 100644 --- a/bts/btp_parse.js +++ b/bts/btp_parse.js @@ -218,6 +218,7 @@ function get_btp_state(response) { const all_btp_entries = btp_t.Entries ? btp_t.Entries[0].Entry : []; const all_btp_stage_entries = btp_t.StageEntries ? btp_t.StageEntries[0].StageEntry : []; const all_btp_events = btp_t.Events ? btp_t.Events[0].Event : []; + const all_btp_stages = btp_t.Stages ? btp_t.Stages[0].Stage : []; const all_btp_players = btp_t.Players ? btp_t.Players[0].Player : []; const all_btp_draws = btp_t.Draws ? btp_t.Draws[0].Draw : []; const all_btp_officials = btp_t.Officials ? btp_t.Officials[0].Official : []; @@ -226,6 +227,7 @@ function get_btp_state(response) { const all_btp_settings = btp_t.Settings ? btp_t.Settings[0].Setting : []; const all_btp_clubs = btp_t.Clubs ? btp_t.Clubs[0].Club : []; const all_btp_districts = btp_t.Districts ? btp_t.Districts[0].District : []; + const all_btp_scoring_formats = btp_t.ScoringFormats ? btp_t.ScoringFormats[0].ScoringFormat : []; const on_court_match_ids = new Set(); for (const c of all_btp_courts) { @@ -239,6 +241,7 @@ function get_btp_state(response) { const entries = utils.make_index(all_btp_entries, e => e.ID[0]); const stage_entries = utils.make_index(all_btp_stage_entries, s => s.ID[0]); const events = utils.make_index(all_btp_events, e => e.ID[0]); + const stages = utils.make_index(all_btp_stages, e => e.ID[0]); const draws = utils.make_index(all_btp_draws, d => d.ID[0]); let team_matches = undefined; @@ -262,6 +265,7 @@ function get_btp_state(response) { const locations = utils.make_index(all_btp_locations, l => l.ID[0]); const clubs = utils.make_index(all_btp_clubs, c => c.ID[0]); const districts = utils.make_index(all_btp_districts,d => d.ID[0]); + const scoring_formats = utils.make_index(all_btp_scoring_formats, sf => sf.ID[0]); const btp_settings = utils.make_index(all_btp_settings, s =>s.ID[0]); for (const bm of matches) { @@ -272,6 +276,7 @@ function get_btp_state(response) { locations, draws, events, + stages, matches, links, officials, @@ -281,6 +286,7 @@ function get_btp_state(response) { teams, clubs, districts, + scoring_formats, // Testing only _matches_by_pid: matches_by_pid, btp_settings diff --git a/bts/btp_sync.js b/bts/btp_sync.js index cac36ea..8aff60c 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -18,7 +18,7 @@ function date_str(dt) { return utils.pad(dt.year, 2, '0') + '-' + utils.pad(dt.month, 2, '0') + '-' + utils.pad(dt.day, 2, '0'); } -async function craft_match(app, tkey, btp_id, location_map, court_map, event, draw, btp_links, officials, clubs, districts, bm, match_ids_on_court, match_types, is_league) { +async function craft_match(app, tkey, btp_id, location_map, court_map, event, stage, scoring_formats, draw, btp_links, officials, clubs, districts, bm, match_ids_on_court, match_types, is_league) { return new Promise((resolve, reject) => { const stournament = require('./stournament'); // avoid dependency cycle @@ -127,17 +127,30 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, dr } } + + let scoring_format = null; + + if (stage.ScoringFormat) { + scoring_format = scoring_formats.get(Number(stage.ScoringFormat)); + } else { + scoring_format = findDefaultScoringFormat(scoring_formats); + } + + // Fallback, falls gar nichts gefunden wird + if (!scoring_format) { + scoring_format = fallbackScoringFormat(); + } + const setup = { is_match: (bm.IsMatch && bm.IsMatch[0] ? true : false), incomplete: !bm.bts_complete, is_doubles: (gtid === 2), match_num: bm.MatchNr[0], - counting: '3x21', + scoring_format: scoring_format, team_competition: false, - //match_name, event_name, teams, - warmup: 'none', + warmup: "none", links: links, highlight: bm.Highlight[0], }; @@ -147,11 +160,6 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, dr if (err) { reject(err); } - - if (tournament.warmup) { - setup.warmup = tournament.warmup; - } - if (tournament.warmup) { setup.warmup = tournament.warmup; } @@ -256,6 +264,17 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, dr }); } +function findDefaultScoringFormat(scoringFormatMap) { + + for (const entry of scoringFormatMap.entries()) { + const id = entry[0]; + const sf = entry[1]; + + if (sf && sf.isDefault) return sf; + } + return null; +} + function _craft_team(par) { if (!par) { return { players: [] }; @@ -501,10 +520,10 @@ function get_umpire(app, tkey, umpires , btp_id) { return returnValue; } -async function integrate_matches(app, tkey, btp_state, location_map, court_map, callback) { +async function integrate_matches(app, tkey, btp_state, scoring_formats, location_map, court_map, callback) { const admin = require('./admin'); // avoid dependency cycle const match_utils = require('./match_utils'); - const { draws, events } = btp_state; + const { draws, events, stages } = btp_state; const match_ids_on_court = calculate_match_ids_on_court(btp_state); @@ -525,6 +544,9 @@ async function integrate_matches(app, tkey, btp_state, location_map, court_map, const event = events.get(draw.EventID[0]); assert(event); + const stage = stages.get(draw.StageID[0]); + assert(stage); + const btp_id = calculate_btp_match_id(tkey, bm, draws, events); if (bm.ReverseHomeAway) { @@ -547,7 +569,7 @@ async function integrate_matches(app, tkey, btp_state, location_map, court_map, return; } - craft_match(app, tkey, btp_id, location_map, court_map, event, draw, btp_state.links, officials, clubs, districts, bm, match_ids_on_court).then(match => { + craft_match(app, tkey, btp_id, location_map, court_map, event, stage, scoring_formats, draw, btp_state.links, officials, clubs, districts, bm, match_ids_on_court).then(match => { match.setup.state = 'unscheduled'; @@ -571,6 +593,18 @@ async function integrate_matches(app, tkey, btp_state, location_map, court_map, if (cur_match.btp_winner) { match.setup.state = 'finished'; } + if (typeof cur_match.team1_won === 'boolean' || cur_match.btp_winner || cur_match.btp_needsync) { + match.setup.now_on_court = false; + match.setup.state = 'finished'; + } else if (cur_match.setup.now_on_court === true) { + // Keep the local on-court state until the result is explicitly confirmed. + match.setup.now_on_court = true; + if (cur_match.setup.state === 'blocked') { + match.setup.state = 'blocked'; + } else if (cur_match.setup.called_timestamp) { + match.setup.state = 'oncourt'; + } + } if (!match.network_score && cur_match.network_score) { match.network_score = cur_match.network_score; @@ -866,7 +900,7 @@ function generateHallAbbreviation(name) { return abbreviation.trim(); } -function integrate_locations(app, tournament_key, btp_state, callback) { +function integrate_locations(app, tournament_key, btp_state, scoring_formats, callback) { const admin = require('./admin'); // avoid dependency cycle const stournament = require('./stournament'); // avoid dependency cycle @@ -963,10 +997,10 @@ function integrate_locations(app, tournament_key, btp_state, callback) { if (changed) { stournament.get_locations(app.db, tournament_key, function (err, all_locations) { admin.notify_change(app, tournament_key, 'location_changed', { all_locations }); - callback(err, res); + callback(err, scoring_formats, res); }); } else { - callback(err, res); + callback(err, scoring_formats, res); } }); } @@ -975,7 +1009,7 @@ function integrate_locations(app, tournament_key, btp_state, callback) { // Returns a map btp_court_id => court._id -function integrate_courts(app, tournament_key, btp_state, location_map, callback) { +function integrate_courts(app, tournament_key, btp_state, scoring_formats, location_map, callback) { const admin = require('./admin'); // avoid dependency cycle const stournament = require('./stournament'); // avoid dependency cycle @@ -1044,10 +1078,10 @@ function integrate_courts(app, tournament_key, btp_state, location_map, callback if (changed) { stournament.get_courts(app.db, tournament_key, function (err, all_courts) { admin.notify_change(app, tournament_key, 'courts_changed', { all_courts }); - callback(err, location_map, res); + callback(err, scoring_formats, location_map, res); }); } else { - callback(err, location_map, res); + callback(err, scoring_formats, location_map, res); } }); } @@ -1105,6 +1139,400 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { }); } +function buildScoringFormatMap(formats) { + const map = new Map(); + for (const f of formats) { + map.set(Number(f.id), f); + } + return map; +} + +function setTypeToEndMax(setType, score) { + const t = Number(setType); + switch (t) { + case 0: return { end_points: 21, max_points: 30, end_points_editable: false, max_points_editable: false }; + case 301: return { end_points: 11, max_points: 11, end_points_editable: false, max_points_editable: false }; + case 304: return { end_points: 11, max_points: 15, end_points_editable: false, max_points_editable: false }; + case 305: return { end_points: 11, max_points: 13, end_points_editable: false, max_points_editable: false }; + case 306: return { end_points: 15, max_points: 21, end_points_editable: false, max_points_editable: false }; + case 1000: + return { + end_points: null, + max_points: null, + end_points_editable: true, + max_points_editable: true, + }; + case 999: { + const s = Number(score); + const fallback = Number.isFinite(s) && s > 0 ? s : null; + return { + end_points: fallback, + max_points: fallback, + end_points_editable: false, + max_points_editable: true, + defaults_from_score: true, + }; + } + default: + return { + end_points: null, + max_points: null, + end_points_editable: false, + max_points_editable: false, + raw: t, + }; + } +} + +function inferSetTiming(name, numSets, setType, isLastSet) { + const normalizedName = String(name || ''); + const t = Number(setType); + const singleSet = Number(numSets) === 1; + + function elevenSetTiming() { + if (singleSet || isLastSet) { + return { + interval_at: 6, + interval_duration_ms: normalizedName.includes('^90') ? 90000 : 60000, + }; + } + return { + interval_at: null, + interval_duration_ms: null, + }; + } + + let timing; + switch (t) { + case 0: + timing = { + interval_at: 11, + interval_duration_ms: 60000, + }; + break; + case 301: + case 304: + case 305: + timing = elevenSetTiming(); + break; + case 306: + timing = { + interval_at: 8, + interval_duration_ms: 60000, + }; + break; + default: + timing = { + interval_at: null, + interval_duration_ms: null, + }; + break; + } + + let breakBeforeSetDurationMs = null; + if (!singleSet) { + if (normalizedName.includes('2x21+11') && isLastSet) { + breakBeforeSetDurationMs = 120000; + } else if (normalizedName.includes('^90')) { + breakBeforeSetDurationMs = 90000; + } else if ( + normalizedName.includes('~NLA') || + t === 0 || + t === 306 + ) { + breakBeforeSetDurationMs = 120000; + } else if (t === 301 || t === 304 || t === 305) { + breakBeforeSetDurationMs = 60000; + } + } + + return { + ...timing, + break_before_set_duration_ms: breakBeforeSetDurationMs, + }; +} + +function applyDefaultSetTiming(setPoints) { + const merged = { + ...setPoints, + }; + const endPoints = Number(merged.end_points); + if (merged.interval_at == null && Number.isFinite(endPoints) && endPoints > 0) { + merged.interval_at = Math.ceil(endPoints / 2); + } + if (merged.interval_duration_ms == null) { + merged.interval_duration_ms = 60000; + } + if (merged.break_before_set_duration_ms == null) { + merged.break_before_set_duration_ms = 120000; + } + return merged; +} + +function sanitizeSetPoints(setPoints) { + const merged = applyDefaultSetTiming(setPoints); + let endPoints = Number(merged.end_points); + if (!Number.isFinite(endPoints) || endPoints < 1) { + endPoints = 1; + } + let maxPoints = Number(merged.max_points); + if (!Number.isFinite(maxPoints) || maxPoints < endPoints) { + maxPoints = endPoints; + } + merged.end_points = endPoints; + merged.max_points = maxPoints; + if (merged.interval_at == null) { + merged.interval_at = Math.ceil(endPoints / 2); + } + return merged; +} + +function sanitizeScoringFormat(scoringFormat) { + if (!scoringFormat) { + return scoringFormat; + } + return { + ...scoringFormat, + set_points: sanitizeSetPoints(scoringFormat.set_points || {}), + last_set_points: sanitizeSetPoints(scoringFormat.last_set_points || {}), + }; +} + +function normalizeScoringFormat(sf, unwrap = (v) => (Array.isArray(v) && v.length === 1 ? v[0] : v)) { + const id = Number(unwrap(sf.ID)); + const name = String(unwrap(sf.Name)); + const numSets = Number(unwrap(sf.NumSets)); + const setType = Number(unwrap(sf.SetType)); + const lastSetType = Number(unwrap(sf.LastSetType)); + const score = Number(unwrap(sf.Score)); + const isDefault = Boolean(unwrap(sf.IsDefault)); + + return sanitizeScoringFormat({ + id, + name, + numSets, + score, + isDefault, + setType, + lastSetType, + set_points: applyDefaultSetTiming({ + ...setTypeToEndMax(setType, score), + ...inferSetTiming(name, numSets, setType, false), + }), + last_set_points: applyDefaultSetTiming({ + ...setTypeToEndMax(lastSetType, score), + ...inferSetTiming(name, numSets, lastSetType, true), + }), + }); +} + +function fallbackScoringFormat() { + const scoringFormat = normalizeScoringFormat({ + ID: [0], + Name: ['3x21'], + NumSets: ['3'], + SetType: ['0'], + LastSetType: ['0'], + Score: ['21'], + IsDefault: [false], + }); + scoringFormat.id = null; + return scoringFormat; +} + +function mergeLocalSetPoints(existingSetPoints, normalizedSetPoints) { + const merged = { + ...normalizedSetPoints, + }; + if (!existingSetPoints) { + return merged; + } + + if (normalizedSetPoints.end_points_editable) { + merged.end_points = existingSetPoints.end_points ?? merged.end_points; + } + if (normalizedSetPoints.max_points_editable) { + merged.max_points = existingSetPoints.max_points ?? merged.max_points; + } + + merged.interval_at = existingSetPoints.interval_at ?? merged.interval_at; + merged.interval_duration_ms = existingSetPoints.interval_duration_ms ?? merged.interval_duration_ms; + merged.break_before_set_duration_ms = existingSetPoints.break_before_set_duration_ms ?? merged.break_before_set_duration_ms; + if (existingSetPoints.interval_enabled !== undefined) { + merged.interval_enabled = existingSetPoints.interval_enabled; + } + + return merged; +} + +function mergeLocalScoringFormat(existingFormat, normalizedFormat) { + if (!existingFormat) { + return sanitizeScoringFormat(normalizedFormat); + } + return sanitizeScoringFormat({ + ...normalizedFormat, + set_points: mergeLocalSetPoints(existingFormat.set_points, normalizedFormat.set_points), + last_set_points: mergeLocalSetPoints(existingFormat.last_set_points, normalizedFormat.last_set_points), + }); +} + +function integrate_btp_scoring_formats(app, tkey, btp_state, callback) { + const admin = require("./admin"); // avoid dependency cycle + + const unwrap = (v) => (Array.isArray(v) && v.length === 1 ? v[0] : v); + + const deepEqualJson = (a, b) => JSON.stringify(a) === JSON.stringify(b); + + app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { + if (err) return callback(err); + if (!tournament) return callback(new Error(`Tournament not found for key: ${tkey}`)); + + if (!tournament.scoring_formats) tournament.scoring_formats = {}; + + if (!btp_state?.scoring_formats || !(btp_state.scoring_formats instanceof Map)) { + return callback(new Error("btp_state.scoring_formats is missing or not a Map")); + } + + const existingFormatsById = new Map( + (((tournament.scoring_formats || {}).formats) || []).map(f => [Number(f.id), f]) + ); + + const formats = Array.from(btp_state.scoring_formats.values()) + .map(sf => normalizeScoringFormat(sf, unwrap)) + .map(sf => mergeLocalScoringFormat(existingFormatsById.get(Number(sf.id)), sf)) + .sort((a, b) => a.id - b.id); + + const defaultFormat = formats.find(f => f.isDefault) || null; + + const scoringFormatsPayload = { + formats, + default_id: defaultFormat ? defaultFormat.id : null, + }; + + const scoringFormatMap = buildScoringFormatMap(formats); + + const existing = tournament.scoring_formats || null; + + // No change + if (deepEqualJson(existing, scoringFormatsPayload)) { + return callback(null, scoringFormatMap); + } + + tournament.scoring_formats = scoringFormatsPayload; + + app.db.tournaments.update( + { key: tkey }, + { $set: { scoring_formats: tournament.scoring_formats } }, + {}, + (err) => { + if (err) return callback(err); + + admin.notify_change(app, tkey, "update_btp_scoring_formats", { + scoring_formats: scoringFormatsPayload, + }); + + return callback(null, scoringFormatMap); + } + ); + }); +} + +function integrate_events(app, tkey, btp_state, callback) { + const admin = require("./admin"); // avoid dependency cycle + + const unwrap = (v) => (Array.isArray(v) && v.length === 1 ? v[0] : v); + const deepEqualJson = (a, b) => JSON.stringify(a) === JSON.stringify(b); + + if (!btp_state || !(btp_state.events instanceof Map) || !(btp_state.stages instanceof Map)) { + return callback(new Error("btp_state.events/stages missing or not a Map")); + } + + function normalizeEvent(ev) { + return { + id: Number(unwrap(ev.ID)), + name: String(unwrap(ev.Name)), + game_type_id: Number(unwrap(ev.GameTypeID)), + gender_id: Number(unwrap(ev.GenderID)), + min_age: Number(unwrap(ev.MinAge)), + max_age: Number(unwrap(ev.MaxAge)), + fee: Number(unwrap(ev.Fee)), + separate_seeding: Boolean(unwrap(ev.SeparateSeeding)), + allow_online_entry: Boolean(unwrap(ev.AllowOnlineEntry)), + grading_id: Number(unwrap(ev.GradingID)), + sub_grading_id: Number(unwrap(ev.SubGradingID)), + sub_grading2_id: Number(unwrap(ev.SubGrading2ID)), + }; + } + + function normalizeStage(st) { + return { + id: Number(unwrap(st.ID)), + name: String(unwrap(st.Name)), + event_id: Number(unwrap(st.EventID)), + stage_type: Number(unwrap(st.StageType)), + display_order: Number(unwrap(st.DisplayOrder)), + scoring_format: st.ScoringFormat !== undefined ? Number(unwrap(st.ScoringFormat)) : null, + }; + } + + // Build normalized payload: + // events: [{... , stages:[...]}] for convenient GUI + lookups + const eventsArr = Array.from(btp_state.events.values()) + .map(normalizeEvent) + .sort((a, b) => a.id - b.id); + + const stagesArr = Array.from(btp_state.stages.values()) + .map(normalizeStage) + .sort((a, b) => a.id - b.id); + + const stagesByEventId = new Map(); + for (const st of stagesArr) { + if (!stagesByEventId.has(st.event_id)) stagesByEventId.set(st.event_id, []); + stagesByEventId.get(st.event_id).push(st); + } + + // Keep stages sorted by display_order then id for stability + for (const [eventId, list] of stagesByEventId.entries()) { + list.sort((a, b) => (a.display_order - b.display_order) || (a.id - b.id)); + } + + const payload = { + events: eventsArr.map((ev) => ({ + ...ev, + stages: stagesByEventId.get(ev.id) || [], + })), + // optional: keep a flat list too, if you prefer later + // stages: stagesArr, + }; + + app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { + if (err) return callback(err); + if (!tournament) return callback(new Error(`Tournament not found for key: ${tkey}`)); + + if (!tournament.events) tournament.events = {}; + + const existing = tournament.events || null; + if (deepEqualJson(existing, payload)) { + return callback(null); + } + + tournament.events = payload; + + const toChange = { events: tournament.events }; + + app.db.tournaments.update({ key: tkey }, { $set: toChange }, {}, (err) => { + if (err) return callback(err); + + admin.notify_change(app, tkey, "update_btp_events", { + events: payload, + }); + + return callback(null); + }); + }); +} + + async function integrate_player_state(app, tkey, btp_state, callback) { const btp_manager = require('./btp_manager'); app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { @@ -1316,6 +1744,79 @@ async function integrate_now_on_court(app, tkey, callback) { const bupws = require('./bupws'); const match_utils = require('./match_utils'); + function matchHasPlayerOnCourtFlags(match) { + if (!match || !match.setup || !match.setup.teams) { + return false; + } + return match.setup.teams.some(team => + team.players && team.players.some(player => player.now_playing_on_court || player.now_tablet_on_court) + ); + } + + function collectActivePlayerIds(matches) { + const activeIds = new Set(); + matches.forEach(match => { + if (!match || !match.setup || !match.setup.teams) { + return; + } + match.setup.teams.forEach(team => { + if (!team.players) { + return; + } + team.players.forEach(player => { + if (player && player.btp_id) { + activeIds.add(player.btp_id); + } + }); + }); + if (match.setup.tabletoperators) { + match.setup.tabletoperators.forEach(player => { + if (player && player.btp_id) { + activeIds.add(player.btp_id); + } + }); + } + }); + return activeIds; + } + + function matchHasOnlyStalePlayerFlags(match, activePlayerIds) { + if (!match || !match.setup || !match.setup.teams) { + return false; + } + return match.setup.teams.some(team => + team.players && team.players.some(player => + (player.now_playing_on_court || player.now_tablet_on_court) && + (!player.btp_id || !activePlayerIds.has(player.btp_id)) + ) + ); + } + + function setPlayerStateForMatch(match) { + return new Promise((resolve, reject) => { + match_utils.set_player_on_court(app, tkey, match.setup, (err) => { + if (err) return reject(err); + match_utils.set_player_on_tablet(app, tkey, match.setup, (err) => { + if (err) return reject(err); + resolve(null); + }); + }); + }); + } + + function clearPlayerStateForMatch(match) { + const endTs = match.end_ts || Date.now(); + return new Promise((resolve, reject) => { + match_utils.remove_player_on_court(app, tkey, match._id, endTs, (err) => { + if (err) return reject(err); + match_utils.remove_tablet_on_court(app, tkey, match._id, endTs, (err) => { + if (err) return reject(err); + resolve(null); + }); + }); + }); + } + // TODO after switching to async, this should happen during court&match construction app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { if (err) { @@ -1326,7 +1827,8 @@ async function integrate_now_on_court(app, tkey, callback) { app.db.matches.find({ 'setup.now_on_court': true }, async (err, now_on_court_matches) => { if (err) return callback(err); - await Promise.all(now_on_court_matches.map(async (match) => { + const activeMatches = now_on_court_matches.filter(match => typeof match.team1_won !== 'boolean'); + await Promise.all(activeMatches.map(async (match) => { const court_id = match.setup.court_id; const match_id = match._id; @@ -1347,8 +1849,24 @@ async function integrate_now_on_court(app, tkey, callback) { }; app.db.courts.update(query, {$set: {match_id}}); } + await setPlayerStateForMatch(match); })); - callback(null); + + app.db.matches.find({ tournament_key: tkey }, async (err, matches) => { + if (err) return callback(err); + + const activePlayerIds = collectActivePlayerIds(activeMatches); + const staleMatches = matches.filter(match => + match && + match.setup && + match.setup.now_on_court !== true && + matchHasPlayerOnCourtFlags(match) && + matchHasOnlyStalePlayerFlags(match, activePlayerIds) + ); + + await Promise.all(staleMatches.map(match => clearPlayerStateForMatch(match))); + callback(null); + }); }); }); // TODO clear courts (better in async) @@ -1366,11 +1884,13 @@ async function sync_btp_data(app, tkey, response) { async.waterfall([ cb => integrate_btp_settings(app, tkey, btp_state, cb), + cb => integrate_events(app, tkey, btp_state, cb), cb => integrate_player_state(app, tkey, btp_state, cb), cb => integrate_umpires(app, tkey, btp_state, cb), - cb => integrate_locations(app, tkey, btp_state, cb), - (location_map, cb) => integrate_courts(app, tkey, btp_state, location_map, cb), - (location_map, court_map, cb) => integrate_matches(app, tkey, btp_state, location_map, court_map, cb), + cb => integrate_btp_scoring_formats(app, tkey, btp_state, cb), + (scoring_formats, cb) => integrate_locations(app, tkey, btp_state, scoring_formats, cb), + (scoring_formats, location_map, cb) => integrate_courts(app, tkey, btp_state, scoring_formats, location_map, cb), + (scoring_formats, location_map, court_map, cb) => integrate_matches(app, tkey, btp_state, scoring_formats, location_map, court_map, cb), cb => integrate_now_on_court(app, tkey, cb), cb => cleanup_entities(app, tkey, btp_state, cb), ], (err) => { @@ -1391,4 +1911,9 @@ module.exports = { time_str, // test only _integrate_umpires: integrate_umpires, + _fallback_scoring_format: fallbackScoringFormat, + _normalize_scoring_format: normalizeScoringFormat, + _merge_local_scoring_format: mergeLocalScoringFormat, + _sanitize_scoring_format: sanitizeScoringFormat, + _set_type_to_end_max: setTypeToEndMax, }; diff --git a/bts/bupws.js b/bts/bupws.js index cd90ca7..9959b8f 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -148,14 +148,13 @@ async function handle_score_update(app, ws, msg) { } catch { var match = null; } - if (match == null || match.setup.now_on_court == false) { + const finish_confirmed = score_data.finish_confirmed ? score_data.finish_confirmed : false; + const allow_finished_confirmation = finish_confirmed && (score_data.team1_won !== undefined && score_data.team1_won !== null); + if (match == null || (match.setup.now_on_court == false && !allow_finished_confirmation)) { send_error(ws, tournament_key, "Match not found or not on court actualy."); return; } - console.log(match); - console.log(match.setup.teams); - const update = { network_score: score_data.network_score, network_team1_left:score_data.network_team1_left, @@ -173,12 +172,14 @@ async function handle_score_update(app, ws, msg) { device_info.client_ip = client_ip; } - const finish_confirmed = score_data.finish_confirmed ? score_data.finish_confirmed : false; + const match_finished = score_data.team1_won !== undefined && score_data.team1_won !== null; if (finish_confirmed) { - update.team1_won = score_data.team1_won, - update.btp_winner = (update.team1_won === true) ? 1 : 2; - update.btp_needsync = true; update["setup.now_on_court"] = false; + update.team1_won = score_data.team1_won; + } + if (finish_confirmed) { + update.btp_winner = (update.team1_won === true) ? 1 : 2; + update.btp_needsync = true; } if (score_data.shuttle_count) { @@ -210,12 +211,14 @@ async function handle_score_update(app, ws, msg) { cb(null, match); }, (match, cb) => { - if (match) { - if (finish_confirmed) { - btp_manager.update_score(app, match); - update_queue.instance().execute(match_utils.reset_player_tabletoperator, app, tournament_key, match_id, update.end_ts) - .then(() => { - cb(null, match); + if (match) { + if (finish_confirmed) { + if (finish_confirmed) { + btp_manager.update_score(app, match); + } + update_queue.instance().execute(match_utils.reset_player_tabletoperator, app, tournament_key, match_id, update.end_ts) + .then(() => { + cb(null, match); }) .catch((err) => { console.error("Error in reset_player_tabletoperator:", err); @@ -278,8 +281,8 @@ async function handle_score_update(app, ws, msg) { if (!match) { return cb(new Error('Cannot find match ' + JSON.stringify(match))); } - if (finish_confirmed && match.team1_won != undefined && match.team1_won != null) { - const next_match = update_queue.instance().execute(match_utils.call_preparation_match_on_court,app, tournament_key, match.setup.court_id); + if (finish_confirmed) { + update_queue.instance().execute(match_utils.call_preparation_match_on_court, app, tournament_key, match.setup.court_id); } return cb(null, match, changed_court); }, @@ -953,4 +956,4 @@ module.exports = { add_display_status, create_match_representation, create_event_representation, -}; \ No newline at end of file +}; diff --git a/bts/match_utils.js b/bts/match_utils.js index 925931f..c12114e 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -646,6 +646,7 @@ function add_tabletoperator_to_tabletoperator_list_by_match(app, tournament_key, function remove_player_on_court (app, tkey, cur_match_id, end_ts = null, callback) { const admin = require('./admin'); // avoid dependency cycle + const btp_manager = require('./btp_manager'); app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { if (err) return callback(err); @@ -666,6 +667,8 @@ function remove_player_on_court (app, tkey, cur_match_id, end_ts = null, callbac } const match_id = match._id; + const players_to_change = []; + const is_finished_match = match_id === cur_match_id; let remove_btp_ids = [ cur_match.setup.teams[0].players[0].btp_id, cur_match.setup.teams[1].players[0].btp_id]; @@ -681,49 +684,54 @@ function remove_player_on_court (app, tkey, cur_match_id, end_ts = null, callbac if (match.setup.teams[0].players.length > 0 && remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && - match.setup.teams[0].players[0].now_playing_on_court) { + (match.setup.teams[0].players[0].now_playing_on_court || is_finished_match)) { match.setup.teams[0].players[0].now_playing_on_court = false; match.setup.teams[0].players[0].checked_in = false; if(end_ts) { match.setup.teams[0].players[0].last_time_on_court_ts = end_ts; } + players_to_change.push(match.setup.teams[0].players[0]); change = true; } if (match.setup.teams[0].players.length > 1 && remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id) && - match.setup.teams[0].players[1].now_playing_on_court) { + (match.setup.teams[0].players[1].now_playing_on_court || is_finished_match)) { match.setup.teams[0].players[1].now_playing_on_court = false; match.setup.teams[0].players[1].checked_in = false; if(end_ts) { match.setup.teams[0].players[1].last_time_on_court_ts = end_ts; } + players_to_change.push(match.setup.teams[0].players[1]); change = true; } if (match.setup.teams[1].players.length > 0 && remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && - match.setup.teams[1].players[0].now_playing_on_court) { + (match.setup.teams[1].players[0].now_playing_on_court || is_finished_match)) { match.setup.teams[1].players[0].now_playing_on_court = false; match.setup.teams[1].players[0].checked_in = false; if(end_ts) { match.setup.teams[1].players[0].last_time_on_court_ts = end_ts; } + players_to_change.push(match.setup.teams[1].players[0]); change = true; } if (match.setup.teams[1].players.length > 1 && remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id) && - match.setup.teams[1].players[1].now_playing_on_court) { + (match.setup.teams[1].players[1].now_playing_on_court || is_finished_match)) { match.setup.teams[1].players[1].now_playing_on_court = false; match.setup.teams[1].players[1].checked_in = false; if(end_ts) { match.setup.teams[1].players[1].last_time_on_court_ts = end_ts; } + players_to_change.push(match.setup.teams[1].players[1]); change = true; } if (change) { + btp_manager.update_players(app, tkey, players_to_change); const setup = match.setup; const match_q = {_id: match_id}; app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { @@ -1083,11 +1091,22 @@ function update_btp_courts(app, tournament_key, match, callback) { } function reset_player_tabletoperator(app, tournament_key, match_id, end_ts) { return new Promise((resolve, reject) => { + let current_match = null; async.waterfall([ + cb => fetch_match(app, tournament_key, match_id).then((match) => { + current_match = match; + cb(null); + }).catch(cb), cb => remove_player_on_court(app, tournament_key, match_id, end_ts, cb), cb => remove_tablet_on_court(app, tournament_key, match_id, end_ts, cb), cb => remove_umpire_on_court(app, tournament_key, match_id, end_ts, cb), - cb => add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts, cb) + cb => add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts, cb), + cb => { + if (!current_match) { + return cb(null); + } + update_btp_courts(app, tournament_key, current_match, cb); + }, ], function (err) { if (err) { return reject(err); @@ -1111,6 +1130,8 @@ module.exports ={ remove_player_on_court, remove_tablet_on_court, remove_umpire_on_court, + set_player_on_court, + set_player_on_tablet, set_umpire_to_standby, add_preparation_call_timestamp, remove_preparation_call_timestamp, @@ -1118,4 +1139,4 @@ module.exports ={ call_preparation_match_on_court, call_next_possible_match_for_preparation, call_match_in_preparation -}; \ No newline at end of file +}; diff --git a/bts/ticker_conn.js b/bts/ticker_conn.js index 3bfb274..e331d85 100644 --- a/bts/ticker_conn.js +++ b/bts/ticker_conn.js @@ -18,7 +18,7 @@ function craft_court(c) { function craft_match(m) { const res = utils.pluck(m, ['_id']); res.s = m.network_score; - res.c = m.setup.counting; + res.sf = m.setup.scoring_format; res.n = m.setup.event_name + ' ' + m.setup.match_name; m.setup.teams.forEach((t, tidx) => { res['p' + tidx] = t.players.map(p => p.name); diff --git a/static/cbts.html b/static/cbts.html index f55b036..c64482d 100644 --- a/static/cbts.html +++ b/static/cbts.html @@ -69,6 +69,7 @@ + @@ -83,4 +84,4 @@ - \ No newline at end of file + diff --git a/static/css/admin.css b/static/css/admin.css index 87cd009..21962e0 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -338,6 +338,99 @@ h2.edit { width: 100%; } +.scoring_formats_table th, +.scoring_formats_table td { + padding: 0.35em 0.5em; + vertical-align: middle; +} + +.scoring_formats_table th { + text-align: left; +} + +.scoring_formats_table tr.scoring_formats_row_group_odd > td, +.scoring_formats_table tr.scoring_formats_row_group_odd > th { + background-color: #bbb; +} + +.scoring_formats_table tr.scoring_formats_row_group_even > td, +.scoring_formats_table tr.scoring_formats_row_group_even > th { + background-color: #ccc; +} + +.scoring_formats_table .scoring_formats_subrow td { + border-top: 1px solid rgba(0, 0, 0, 0.08); +} + +.scoring_format_type_cell { + font-weight: 700; + white-space: nowrap; + text-align: left; +} + +.scoring_format_rule_cell { + white-space: nowrap; +} + +.scoring_format_name_cell { + text-align: left; +} + +.scoring_format_center_cell { + text-align: center; +} + +.scoring_format_right_cell { + text-align: right; +} + +.default_scoring_format_badge { + display: inline-block; + min-width: 1.6em; + padding: 0.1em 0.35em; + border-radius: 999px; + background: #ffd54f; + color: #4a3500; + font-weight: 700; + text-align: center; + white-space: nowrap; + box-shadow: inset 0 0 0 1px rgba(74, 53, 0, 0.18); +} + +.default_scoring_format_badge_inactive { + background: transparent; + color: #666; + box-shadow: none; + font-weight: 400; +} + +.scoring_format_edit_container { + display: grid; + gap: 1rem; + min-width: 36rem; +} + +.scoring_format_edit_row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.scoring_format_edit_row > input { + width: 10rem; +} + +.scoring_format_edit_section { + border: 1px solid #bbb; + padding: 0.75rem 1rem 1rem 1rem; +} + +.scoring_format_edit_section > legend { + padding: 0 0.35rem; + font-weight: 700; +} + .edit_display_setting_container{ text-align: left; } @@ -391,4 +484,4 @@ h2.edit { max-width: 150px; inline-size: 150px; overflow-wrap: break-word; -} \ No newline at end of file +} diff --git a/static/js/change.js b/static/js/change.js index dac95d2..cf284b9 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -31,8 +31,8 @@ function default_handler(rerender, special_funcs) { case 'free_announce': announce([c.val.text]); break; - case 'props': { - curt.name = c.val.name; + case 'props': { + curt.name = c.val.name; curt.is_team = c.val.is_team; curt.tguid = c.val.tguid; curt.is_nation_competition = c.val.is_nation_competition; @@ -46,9 +46,14 @@ function default_handler(rerender, special_funcs) { curt.btp_readonly = c.val.btp_readonly; curt.btp_ip = c.val.btp_ip; curt.ticker_enabled = c.val.ticker_enabled; - curt.ticker_url = c.val.ticker_url; - curt.ticker_password = c.val.ticker_password; - curt.logo_id = c.val.logo_id; + curt.ticker_url = c.val.ticker_url; + curt.ticker_password = c.val.ticker_password; + curt.logo_id = c.val.logo_id; + if (c.val.scoring_formats) { + curt.scoring_formats = c.val.scoring_formats; + ctournament.update_scoring_formats(); + ctournament.update_stages_scoring_formats(); + } uiu.qsEach('.ct_name', function(el) { if (el.tagName.toUpperCase() === 'INPUT') { @@ -308,6 +313,26 @@ function default_handler(rerender, special_funcs) { curt.btp_settings[key] = value; } break; + case 'update_btp_scoring_formats': + if(!curt.scoring_formats) { + curt.scoring_formats = {}; + } + const scoring_formats = c.val.scoring_formats; + for (const [key, value] of Object.entries(scoring_formats)) { + curt.scoring_formats[key] = value; + } + ctournament.update_scoring_formats(); + break; + case 'update_btp_events': + if(!curt.events) { + curt.events = {}; + } + const events = c.val.events; + for (const [key, value] of Object.entries(events)) { + curt.events[key] = value; + } + ctournament.update_stages_scoring_formats(); + break; case 'update_display_setting': const updated_setting = c.val.setting; const index = curt.displaysettings.findIndex(m => m.id === updated_setting.id); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 6198ccd..275cf32 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -211,12 +211,32 @@ var ci18n_de = { 'tournament:edit:upcoming_matches_settings': 'Spielübersichts Einstellungen', 'tournament:edit:upcoming_matches_animation_speed': 'Animationsgeschwindigkeit beim Scrollen der Spielübersichten', 'tournament:edit:upcoming_matches_animation_pause': 'Animationsunterbrechung am Anfang und Ende der Seite (sec)', - 'tournament:edit:upcoming_matches_max_count': 'Maximale Anzahl von Spielen in der Spielübersicht', - 'tournament:edit:call_preparation_matches_automatically_enabled': 'Spiele in Vorbereitung automatisch auf freien Felden aufrufen', - 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Nächst mögliches Spiel automatisch in Vorbereitung aufrufen', + 'tournament:edit:upcoming_matches_max_count': 'Maximale Anzahl von Spielen in der Spielübersicht', + 'tournament:edit:call_preparation_matches_automatically_enabled': 'Spiele in Vorbereitung automatisch auf freien Felden aufrufen', + 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Nächst mögliches Spiel automatisch in Vorbereitung aufrufen', + 'tournament:edit:scoring_formats': 'Punktsysteme', + 'tournament:edit:scoring_formats:dialog_title': 'Punktsystem bearbeiten', + 'tournament:edit:scoring_formats:dialog_hint': 'Aus BTP importierte Felder sind schreibgeschützt. Bearbeitet werden nur lokale Timing-Werte.', + 'tournament:edit:scoring_formats:name': 'Name', + 'tournament:edit:scoring_formats:num_sets': 'Sätze', + 'tournament:edit:scoring_formats:regular_sets': 'Normale Sätze', + 'tournament:edit:scoring_formats:last_set': 'Letzter Satz', + 'tournament:edit:scoring_formats:type': 'Typ', + 'tournament:edit:scoring_formats:type_regular': 'normal:', + 'tournament:edit:scoring_formats:type_last': 'letzter:', + 'tournament:edit:scoring_formats:default': 'Standard', + 'tournament:edit:scoring_formats:default_badge': '★', + 'tournament:edit:scoring_formats:edit': 'Bearbeiten', + 'tournament:edit:scoring_formats:end_max': 'Ende / Max', + 'tournament:edit:scoring_formats:end_points_label': 'Satzende bei', + 'tournament:edit:scoring_formats:max_points': 'Maximalpunkte', + 'tournament:edit:scoring_formats:break_in_set_enabled': 'Pause im Satz aktiv', + 'tournament:edit:scoring_formats:interval_at': 'Pause im Satz bei', + 'tournament:edit:scoring_formats:interval_duration': 'Pause im Satz', + 'tournament:edit:scoring_formats:break_before_set': 'Pause vor Satz', + + - - 'to_stats:header': 'Statistik der Technischen Offiziellen', 'to_stats:name': 'Name', 'to_stats:umpire': 'SR', @@ -271,4 +291,4 @@ var ci18n_de = { if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { module.exports = ci18n_de; } -/*/@DEV*/ \ No newline at end of file +/*/@DEV*/ diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 18880f5..6e39fd7 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -210,10 +210,30 @@ var ci18n_en = { 'tournament:edit:upcoming_matches_settings': 'Game Overview Settings', 'tournament:edit:upcoming_matches_animation_speed': 'Animationspeed for scroll on game overviews', 'tournament:edit:upcoming_matches_animation_pause': 'Animation interruption at the beginning and end of the page (sec)', - 'tournament:edit:upcoming_matches_max_count': 'Maximum number of games in the game overview', - 'tournament:edit:call_preparation_matches_automatically_enabled': 'Call games in preparation on free fields automatically', - 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Automatically call up next possible game in preparation', - 'to_stats:header': 'Technical Officials Statistics', + 'tournament:edit:upcoming_matches_max_count': 'Maximum number of games in the game overview', + 'tournament:edit:call_preparation_matches_automatically_enabled': 'Call games in preparation on free fields automatically', + 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Automatically call up next possible game in preparation', + 'tournament:edit:scoring_formats': 'Scoring formats', + 'tournament:edit:scoring_formats:dialog_title': 'Edit scoring format', + 'tournament:edit:scoring_formats:dialog_hint': 'Fields imported from BTP are read-only. Only local timing values can be edited.', + 'tournament:edit:scoring_formats:name': 'Name', + 'tournament:edit:scoring_formats:num_sets': 'Sets', + 'tournament:edit:scoring_formats:regular_sets': 'Regular sets', + 'tournament:edit:scoring_formats:last_set': 'Final set', + 'tournament:edit:scoring_formats:type': 'Type', + 'tournament:edit:scoring_formats:type_regular': 'regular:', + 'tournament:edit:scoring_formats:type_last': 'final:', + 'tournament:edit:scoring_formats:default': 'Default', + 'tournament:edit:scoring_formats:default_badge': '★', + 'tournament:edit:scoring_formats:edit': 'Edit', + 'tournament:edit:scoring_formats:end_max': 'Target / Max', + 'tournament:edit:scoring_formats:end_points_label': 'Set ends at', + 'tournament:edit:scoring_formats:max_points': 'Maximum points', + 'tournament:edit:scoring_formats:break_in_set_enabled': 'Break in set enabled', + 'tournament:edit:scoring_formats:interval_at': 'Break in set at', + 'tournament:edit:scoring_formats:interval_duration': 'Break in set', + 'tournament:edit:scoring_formats:break_before_set': 'Break before set', + 'to_stats:header': 'Technical Officials Statistics', 'to_stats:name': 'Name', 'to_stats:umpire': 'U', 'to_stats:service_judge': 'SJ', @@ -267,4 +287,4 @@ var ci18n_en = { if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { module.exports = ci18n_en; } -/*/@DEV*/ \ No newline at end of file +/*/@DEV*/ diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 599a00d..b5cb91a 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -531,7 +531,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } } - if(style == 'plain' && match.setup.counting == "3x21" && isMatchOver(match.network_score)) { + if(style == 'plain' && match_scoring.is_match_over(match.network_score, match.setup.scoring_format)) { create_match_button(timer_td, 'vlink match_confirm_button', 'Confirm_Finish', on_match_confirm_button_click, match._id); } } @@ -637,10 +637,10 @@ function update_match_score(m) { } } - if(m.setup.counting = "3x21" && isMatchOver(m.network_score)) { - create_match_button(timer_td, 'vlink match_confirm_button', 'Confirm_Finish', on_match_confirm_button_click, m._id); - } - }); + if (match_scoring.is_match_over(m.network_score, m.setup.scoring_format)) { + create_match_button(timer_td, 'vlink match_confirm_button', 'Confirm_Finish', on_match_confirm_button_click, m._id); + } + }); if( m.network_score && m.network_score.length > 0 && m.network_score[0].length > 1 && @@ -690,49 +690,6 @@ function on_match_confirm_button_click(e) { } } -function isMatchOver(sets) { - if(!sets){ - return false; - } - - let winsA = 0; - let winsB = 0; - - for (let [scoreA, scoreB] of sets) { - if (isSetOver(scoreA, scoreB)) { - if (scoreA > scoreB) { - winsA++; - } else { - winsB++; - } - - if (winsA === 2 || winsB === 2) { - return true; - } - } else { - // Satz ist noch nicht vorbei → Spiel auch nicht - return false; - } - } - - // Falls nicht abgebrochen wurde, prüfen ob überhaupt jemand 2 Sätze gewonnen hat - return winsA === 2 || winsB === 2; -} - -function isSetOver(scoreA, scoreB) { - const maxScore = 30; - const winningScore = 21; - - if (scoreA === maxScore || scoreB === maxScore) return true; - - if ((scoreA >= winningScore || scoreB >= winningScore) && Math.abs(scoreA - scoreB) >= 2) { - return true; - } - - return false; -} - - function render_players_el(parentNode, setup, team_id, match, show_player_status, style) { const team = setup.teams[team_id]; @@ -1145,7 +1102,7 @@ function _extract_match_timer_state(match) { let s = {}; s.settings = {}; s.settings.negative_timers = true; - s.lang = "de"; //TODO: Use the language of the BTS Settings + s.lang = (curt && curt.btp_settings && curt.btp_settings.language && curt.btp_settings.language !== 'auto') ? curt.btp_settings.language : "de"; var rs = calc.remote_state(s, match.setup, presses); return rs; @@ -2545,6 +2502,7 @@ if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var i18n = require('../bup/js/i18n'); var i18n_de = require('../bup/js/i18n_de'); var i18n_en = require('../bup/js/i18n_en'); + var match_scoring = require('./match_scoring'); var printing = require('../bup/js/printing'); var settings = require('../bup/js/settings'); var timer = require('../bup/js/timer'); diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 822332e..16f6372 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -1,6 +1,7 @@ 'use strict'; var curt; // current tournament +let scoring_formats_main = null; var ctournament = (function() { function _route_single(rex, func, handler) { @@ -1041,6 +1042,17 @@ var ctournament = (function() { } + + // scoring-formats-div############################################################################## + { + const scoring_div = uiu.el(form, "div", "settings"); + scoring_formats_main = scoring_div; + render_scoring_formats(scoring_div); + render_stages_scoring_formats(scoring_div) + } + + + // call-div################################################################################## { const call_div = uiu.el(form, 'div', 'settings'); @@ -1249,6 +1261,461 @@ var ctournament = (function() { }); } + function update_scoring_formats() { + if (!scoring_formats_main) { + if (typeof debug !== "undefined" && debug?.log) { + debug.log("update_scoring_formats: main container not initialized"); + } + return; + } + + // kompletten Bereich leeren + while (scoring_formats_main.firstChild) { + scoring_formats_main.removeChild(scoring_formats_main.firstChild); + } + + // vollständig neu rendern + render_scoring_formats(scoring_formats_main); + render_stages_scoring_formats(scoring_formats_main); + } + + function format_duration_ms(durationMs) { + const duration = Number(durationMs); + if (!Number.isFinite(duration) || duration < 0) { + return "—"; + } + if (duration === 0) { + return "0 s"; + } + return `${Math.round(duration / 1000)} s`; + } + + function format_set_rule_summary(setPoints) { + if (!setPoints) { + return "—"; + } + + const endPoints = setPoints.end_points ?? "—"; + const maxPoints = setPoints.max_points ?? "—"; + return `${endPoints} / ${maxPoints}`; + } + + function parse_nullable_number(value) { + if (value === undefined || value === null || value === "") { + return null; + } + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + + function duration_ms_to_seconds(value) { + const duration = parse_nullable_number(value); + if (duration === null) { + return ""; + } + return duration / 1000; + } + + function duration_seconds_to_ms(value) { + const duration = parse_nullable_number(value); + if (duration === null) { + return null; + } + return duration * 1000; + } + + function is_break_in_set_enabled(setPoints) { + if (!setPoints) { + return false; + } + if (typeof setPoints.interval_enabled === "boolean") { + return setPoints.interval_enabled; + } + return ( + setPoints.interval_at !== null && + setPoints.interval_at !== undefined && + setPoints.interval_duration_ms !== null && + setPoints.interval_duration_ms !== undefined + ); + } + + function clone_scoring_formats() { + const scoringFormats = curt?.scoring_formats || { formats: [], default_id: null }; + return structuredClone(scoringFormats); + } + + function _cancel_ui_edit_scoring_format() { + const dlg = document.querySelector('.scoring_format_edit_dialog'); + if (!dlg) { + return; + } + cbts_utils.esc_stack_pop(); + uiu.remove(dlg); + } + + function create_scoring_format_field(parent, label, name, value, type = "text", attrs = {}) { + const row = uiu.el(parent, 'label', 'scoring_format_edit_row'); + uiu.el(row, 'span', {}, label); + return uiu.el(row, 'input', Object.assign({ + type, + name, + value: value ?? '', + }, attrs)); + } + + function create_scoring_format_checkbox(parent, label, name, checked) { + const row = uiu.el(parent, 'label', 'scoring_format_edit_row'); + uiu.el(row, 'span', {}, label); + const attrs = { + type: 'checkbox', + name, + }; + if (checked) { + attrs.checked = 'checked'; + } + return uiu.el(row, 'input', attrs); + } + + function is_scoring_value_editable(setPoints, fieldName) { + return !!(setPoints && setPoints[`${fieldName}_editable`]); + } + + function render_scoring_format_edit_section(parent, prefix, title, setPoints) { + const fieldset = uiu.el(parent, 'fieldset', 'scoring_format_edit_section'); + uiu.el(fieldset, 'legend', {}, title); + const endPointAttrs = { min: 1, step: 1 }; + if (!is_scoring_value_editable(setPoints, "end_points")) { + endPointAttrs.disabled = 'disabled'; + } else { + endPointAttrs.required = 'required'; + } + const maxPointAttrs = { min: 1, step: 1 }; + if (!is_scoring_value_editable(setPoints, "max_points")) { + maxPointAttrs.disabled = 'disabled'; + } else { + maxPointAttrs.required = 'required'; + } + const endPointsInput = create_scoring_format_field(fieldset, ci18n("tournament:edit:scoring_formats:end_points_label"), `${prefix}_end_points`, setPoints?.end_points, "number", endPointAttrs); + const maxPointsInput = create_scoring_format_field(fieldset, ci18n("tournament:edit:scoring_formats:max_points"), `${prefix}_max_points`, setPoints?.max_points, "number", maxPointAttrs); + const hasBreakInSet = is_break_in_set_enabled(setPoints); + const breakEnabled = create_scoring_format_checkbox(fieldset, ci18n("tournament:edit:scoring_formats:break_in_set_enabled"), `${prefix}_break_in_set_enabled`, hasBreakInSet); + const intervalAtInput = create_scoring_format_field(fieldset, ci18n("tournament:edit:scoring_formats:interval_at"), `${prefix}_interval_at`, setPoints?.interval_at, "number", { min: 0, step: 1 }); + const intervalDurationInput = create_scoring_format_field(fieldset, `${ci18n("tournament:edit:scoring_formats:interval_duration")} (s)`, `${prefix}_interval_duration_s`, duration_ms_to_seconds(setPoints?.interval_duration_ms), "number", { min: 0, step: 1 }); + create_scoring_format_field(fieldset, `${ci18n("tournament:edit:scoring_formats:break_before_set")} (s)`, `${prefix}_break_before_set_duration_s`, duration_ms_to_seconds(setPoints?.break_before_set_duration_ms), "number", { min: 0, step: 1 }); + + function normalizeScoreInputs() { + if (!endPointsInput.disabled) { + let endPoints = Number(endPointsInput.value); + if (!Number.isFinite(endPoints) || endPoints < 1) { + endPoints = Math.max(1, Number(setPoints?.end_points) || 1); + } + endPointsInput.value = String(endPoints); + if (!maxPointsInput.disabled) { + maxPointsInput.min = String(endPoints); + let maxPoints = Number(maxPointsInput.value); + if (!Number.isFinite(maxPoints) || maxPoints < endPoints) { + maxPoints = endPoints; + } + maxPointsInput.value = String(maxPoints); + } + } else if (!maxPointsInput.disabled) { + let maxPoints = Number(maxPointsInput.value); + const minValue = Math.max(1, Number(setPoints?.end_points) || 1); + maxPointsInput.min = String(minValue); + if (!Number.isFinite(maxPoints) || maxPoints < minValue) { + maxPointsInput.value = String(minValue); + } + } + } + + if (!endPointsInput.disabled) { + endPointsInput.addEventListener('input', normalizeScoreInputs); + endPointsInput.addEventListener('blur', normalizeScoreInputs); + } + if (!maxPointsInput.disabled) { + maxPointsInput.addEventListener('input', normalizeScoreInputs); + maxPointsInput.addEventListener('blur', normalizeScoreInputs); + } + normalizeScoreInputs(); + + function updateBreakInSetUi() { + const enabled = breakEnabled.checked; + intervalAtInput.disabled = !enabled; + intervalDurationInput.disabled = !enabled; + } + + breakEnabled.addEventListener('change', updateBreakInSetUi); + updateBreakInSetUi(); + } + + function scoring_format_from_form_data(baseFormat, data) { + const scoringFormat = structuredClone(baseFormat); + + function update_set_points(target, prefix) { + if (is_scoring_value_editable(target, "end_points")) { + target.end_points = Math.max(1, Number(data[`${prefix}_end_points`])); + } + if (is_scoring_value_editable(target, "max_points")) { + const minPoints = Math.max(1, Number(target.end_points)); + target.max_points = Math.max(minPoints, Number(data[`${prefix}_max_points`])); + } + const hasBreakInSet = !!data[`${prefix}_break_in_set_enabled`]; + target.interval_enabled = hasBreakInSet; + if (hasBreakInSet) { + target.interval_at = parse_nullable_number(data[`${prefix}_interval_at`]); + target.interval_duration_ms = duration_seconds_to_ms(data[`${prefix}_interval_duration_s`]); + } + target.break_before_set_duration_ms = duration_seconds_to_ms(data[`${prefix}_break_before_set_duration_s`]); + } + + update_set_points(scoringFormat.set_points, 'set_points'); + update_set_points(scoringFormat.last_set_points, 'last_set_points'); + return scoringFormat; + } + + function save_scoring_format(scoringFormatId, scoringFormat, callback) { + const scoringFormats = clone_scoring_formats(); + const formats = Array.isArray(scoringFormats.formats) ? scoringFormats.formats : []; + const index = formats.findIndex(f => Number(f.id) === Number(scoringFormatId)); + if (index === -1) { + return callback(new Error(`Unknown scoring format ${scoringFormatId}`)); + } + formats[index] = scoringFormat; + scoringFormats.formats = formats; + + send({ + type: 'tournament_edit_props', + key: curt.key, + props: { + scoring_formats: scoringFormats, + }, + }, callback); + } + + function ui_edit_scoring_format(scoringFormatId) { + const scoringFormats = curt?.scoring_formats; + const baseFormat = structuredClone(utils.find((scoringFormats && scoringFormats.formats) || [], f => Number(f.id) === Number(scoringFormatId))); + if (!baseFormat) { + return; + } + + cbts_utils.esc_stack_push(_cancel_ui_edit_scoring_format); + + const body = uiu.qs('body'); + const dialogBg = uiu.el(body, 'div', 'dialog_bg scoring_format_edit_dialog', { + 'data-scoring-format-id': scoringFormatId, + }); + dialogBg.addEventListener('click', (e) => { + if (e.target === dialogBg) { + _cancel_ui_edit_scoring_format(); + } + }); + + const dialog = uiu.el(dialogBg, 'div', 'dialog'); + uiu.el(dialog, 'h3', {}, ci18n('tournament:edit:scoring_formats:dialog_title')); + + const form = uiu.el(dialog, 'form'); + const container = uiu.el(form, 'div', 'scoring_format_edit_container'); + uiu.el(container, 'div', 'hint', ci18n('tournament:edit:scoring_formats:dialog_hint')); + create_scoring_format_field(container, ci18n("tournament:edit:scoring_formats:name"), 'name', baseFormat.name, 'text', { disabled: 'disabled' }); + create_scoring_format_field(container, ci18n("tournament:edit:scoring_formats:num_sets"), 'numSets', baseFormat.numSets, 'number', { min: 1, step: 1, disabled: 'disabled' }); + render_scoring_format_edit_section(container, 'set_points', ci18n("tournament:edit:scoring_formats:regular_sets"), baseFormat.set_points); + render_scoring_format_edit_section(container, 'last_set_points', ci18n("tournament:edit:scoring_formats:last_set"), baseFormat.last_set_points); + + const buttons = uiu.el(form, 'div', { style: 'margin-top: 2em;' }); + uiu.el(buttons, 'button', { + 'class': 'match_save_button', + role: 'submit', + }, ci18n('Change')); + + form_utils.onsubmit(form, function(data) { + const scoringFormat = scoring_format_from_form_data(baseFormat, data); + save_scoring_format(scoringFormatId, scoringFormat, (err) => { + if (err) { + return cerror.net(err); + } + _cancel_ui_edit_scoring_format(); + }); + }); + + const cancelBtn = uiu.el(buttons, 'span', 'match_cancel_link vlink', ci18n('Cancel')); + cancelBtn.addEventListener('click', _cancel_ui_edit_scoring_format); + } + + function render_scoring_formats(main) { + uiu.el(main, "h2", "edit", ci18n("tournament:edit:scoring_formats")); + + const sf = curt?.scoring_formats || { formats: [], default_id: null }; + const formats = Array.isArray(sf.formats) ? sf.formats : []; + const defaultId = sf.default_id; + + const table = uiu.el(main, "table", "scoring_formats_table"); + const tbody = uiu.el(table, "tbody"); + + { + const tr = uiu.el(tbody, "tr"); + uiu.el(tr, "th", { class: "scoring_format_name_cell" }, ci18n("tournament:edit:scoring_formats:name")); + uiu.el(tr, "th", { class: "scoring_format_center_cell" }, ci18n("tournament:edit:scoring_formats:num_sets")); + uiu.el(tr, "th", { class: "scoring_format_type_cell" }, ci18n("tournament:edit:scoring_formats:type")); + uiu.el(tr, "th", { class: "scoring_format_center_cell" }, ci18n("tournament:edit:scoring_formats:end_max")); + uiu.el(tr, "th", { class: "scoring_format_center_cell" }, ci18n("tournament:edit:scoring_formats:interval_at")); + uiu.el(tr, "th", { class: "scoring_format_right_cell" }, ci18n("tournament:edit:scoring_formats:interval_duration")); + uiu.el(tr, "th", { class: "scoring_format_right_cell" }, ci18n("tournament:edit:scoring_formats:break_before_set")); + uiu.el(tr, "th", { class: "scoring_format_center_cell" }, ci18n("tournament:edit:scoring_formats:default")); + uiu.el(tr, "th", { class: "scoring_format_center_cell" }, ci18n("tournament:edit:scoring_formats:edit")); + } + + for (const [formatIndex, f] of formats.entries()) { + const rowClass = (formatIndex % 2 === 0) ? "scoring_formats_row_group_even" : "scoring_formats_row_group_odd"; + const regularTr = uiu.el(tbody, "tr", rowClass); + const lastTr = uiu.el(tbody, "tr", `scoring_formats_subrow ${rowClass}`); + const regularSetPoints = f?.set_points; + const lastSetPoints = f?.last_set_points; + const isDefault = Number(f.id) === Number(defaultId); + const canEdit = true; + + uiu.el(regularTr, "td", { rowspan: 2, class: "scoring_format_name_cell" }, f.name || ""); + uiu.el(regularTr, "td", { rowspan: 2, class: "scoring_format_center_cell" }, String(f.numSets ?? "")); + uiu.el(regularTr, "td", { class: "scoring_format_type_cell scoring_format_rule_cell" }, ci18n("tournament:edit:scoring_formats:type_regular")); + uiu.el(regularTr, "td", { class: "scoring_format_rule_cell scoring_format_center_cell" }, format_set_rule_summary(regularSetPoints)); + uiu.el(regularTr, "td", { class: "scoring_format_rule_cell scoring_format_center_cell" }, is_break_in_set_enabled(regularSetPoints) ? String(regularSetPoints.interval_at) : "—"); + uiu.el(regularTr, "td", { class: "scoring_format_rule_cell scoring_format_right_cell" }, is_break_in_set_enabled(regularSetPoints) ? format_duration_ms(regularSetPoints && regularSetPoints.interval_duration_ms) : "—"); + uiu.el(regularTr, "td", { class: "scoring_format_rule_cell scoring_format_right_cell" }, format_duration_ms(regularSetPoints && regularSetPoints.break_before_set_duration_ms)); + + const defTd = uiu.el(regularTr, "td", { rowspan: 2, class: "scoring_format_center_cell" }); + if (isDefault) { + uiu.el(defTd, "span", { + class: "default_scoring_format_badge", + title: ci18n("tournament:edit:scoring_formats:default"), + }, ci18n("tournament:edit:scoring_formats:default_badge")); + } else { + uiu.el(defTd, "span", { class: "default_scoring_format_badge default_scoring_format_badge_inactive" }, "—"); + } + + const actionsTd = uiu.el(regularTr, "td", { rowspan: 2, class: "scoring_format_center_cell" }); + const editBtn = uiu.el( + actionsTd, + "button", + { "data-scoring-format-id": f.id }, + ci18n("tournament:edit:scoring_formats:edit") + ); + + editBtn.addEventListener("click", (e) => { + const id = e.target.getAttribute("data-scoring-format-id"); + ui_edit_scoring_format(id); + }); + + uiu.el(lastTr, "td", { class: "scoring_format_type_cell scoring_format_rule_cell" }, ci18n("tournament:edit:scoring_formats:type_last")); + uiu.el(lastTr, "td", { class: "scoring_format_rule_cell scoring_format_center_cell" }, format_set_rule_summary(lastSetPoints)); + uiu.el(lastTr, "td", { class: "scoring_format_rule_cell scoring_format_center_cell" }, is_break_in_set_enabled(lastSetPoints) ? String(lastSetPoints.interval_at) : "—"); + uiu.el(lastTr, "td", { class: "scoring_format_rule_cell scoring_format_right_cell" }, is_break_in_set_enabled(lastSetPoints) ? format_duration_ms(lastSetPoints && lastSetPoints.interval_duration_ms) : "—"); + uiu.el(lastTr, "td", { class: "scoring_format_rule_cell scoring_format_right_cell" }, format_duration_ms(lastSetPoints && lastSetPoints.break_before_set_duration_ms)); + } + } + + function update_stages_scoring_formats() { + if (!scoring_formats_main) { + if (typeof debug !== "undefined" && debug?.log) { + debug.log("update_scoring_formats: main container not initialized"); + } + return; + } + + // kompletten Bereich leeren + while (scoring_formats_main.firstChild) { + scoring_formats_main.removeChild(scoring_formats_main.firstChild); + } + + // vollständig neu rendern + render_scoring_formats(scoring_formats_main); + render_stages_scoring_formats(scoring_formats_main); + } + + function render_stages_scoring_formats(main) { + const sf = curt?.scoring_formats || { formats: [], default_id: null }; + const defaultId = sf.default_id; + + // Build lookup: scoring_format_id -> scoring_format_name + const formatNameById = new Map(); + for (const f of sf.formats || []) { + formatNameById.set(Number(f.id), f.name || String(f.id)); + } + + const eventsPayload = curt?.events?.events || []; + const deviations = []; + + for (const ev of eventsPayload) { + const eventName = ev?.name || ""; + const stages = Array.isArray(ev?.stages) ? ev.stages : []; + + for (const st of stages) { + // Missing/null scoring_format => default + const stageSfId = + st && st.scoring_format !== undefined && st.scoring_format !== null + ? Number(st.scoring_format) + : null; + + if ( + stageSfId !== null && + defaultId !== null && + defaultId !== undefined && + stageSfId !== Number(defaultId) + ) { + deviations.push({ + event_name: eventName, + stage_name: st?.name || "", + scoring_format_id: stageSfId, + scoring_format_name: formatNameById.get(stageSfId) || String(stageSfId), + }); + } + } + } + + deviations.sort((a, b) => { + const e = (a.event_name || "").localeCompare(b.event_name || ""); + if (e) return e; + const s = (a.stage_name || "").localeCompare(b.stage_name || ""); + if (s) return s; + return (a.scoring_format_id || 0) - (b.scoring_format_id || 0); + }); + + uiu.el(main, "h3", "edit", "Abweichungen vom Default"); + + if (defaultId === null || defaultId === undefined) { + uiu.el( + main, + "div", + "hint", + "Kein Default-Scoring-Format gefunden (scoring_formats.default_id ist leer)." + ); + return; + } + + if (deviations.length === 0) { + uiu.el(main, "div", "hint", "Keine Stages weichen vom Default-Scoring-Format ab."); + return; + } + + const devTable = uiu.el(main, "table", "scoring_format_deviations_table"); + const devBody = uiu.el(devTable, "tbody"); + + { + const tr = uiu.el(devBody, "tr"); + uiu.el(tr, "th", {}, "Event"); + uiu.el(tr, "th", {}, "Stage"); + uiu.el(tr, "th", {}, "Verwendete Zählweise"); + } + + for (const d of deviations) { + const tr = uiu.el(devBody, "tr"); + uiu.el(tr, "td", {}, d.event_name); + uiu.el(tr, "td", {}, d.stage_name); + uiu.el(tr, "td", {}, `${d.scoring_format_name} (#${d.scoring_format_id})`); + } + } + + + function render_normalisation_values(main) { uiu.el(main, 'h2','edit', ci18n('tournament:edit:normalizations')); @@ -2766,6 +3233,8 @@ var ctournament = (function() { update_location, update_location_logo, update_court, + update_scoring_formats, + update_stages_scoring_formats, btp_status_changed, ticker_status_changed, bts_status_changed, diff --git a/static/js/match_scoring.js b/static/js/match_scoring.js new file mode 100644 index 0000000..198e939 --- /dev/null +++ b/static/js/match_scoring.js @@ -0,0 +1,92 @@ +'use strict'; + +var match_scoring = (function() { + function fallback_scoring_format() { + return { + numSets: 3, + set_points: { + end_points: 21, + max_points: 30, + }, + last_set_points: { + end_points: 21, + max_points: 30, + }, + }; + } + + function normalize_set_points(setPoints, fallbackSetPoints) { + const normalized = setPoints || {}; + const fallback = fallbackSetPoints || {}; + + return { + end_points: Number.isFinite(normalized.end_points) ? normalized.end_points : fallback.end_points, + max_points: Number.isFinite(normalized.max_points) ? normalized.max_points : fallback.max_points, + }; + } + + function is_set_over(scoreA, scoreB, setPoints) { + const maxScore = setPoints?.max_points; + const winningScore = setPoints?.end_points; + + if (Number.isFinite(maxScore) && (scoreA === maxScore || scoreB === maxScore)) return true; + + if (Number.isFinite(winningScore) && + (scoreA >= winningScore || scoreB >= winningScore) && + Math.abs(scoreA - scoreB) >= 2) { + return true; + } + + return false; + } + + function is_match_over(sets, scoringFormat) { + if (!sets) { + return false; + } + + const format = scoringFormat || fallback_scoring_format(); + const totalSets = Number.isFinite(format.numSets) && format.numSets > 0 ? format.numSets : 3; + const requiredWins = Math.floor(totalSets / 2) + 1; + const fallbackSetPoints = fallback_scoring_format().set_points; + + let winsA = 0; + let winsB = 0; + + for (let idx = 0; idx < sets.length; idx++) { + const [scoreA, scoreB] = sets[idx]; + const isLastPossibleSet = idx === totalSets - 1; + const setPoints = normalize_set_points( + isLastPossibleSet ? format.last_set_points : format.set_points, + fallbackSetPoints + ); + + if (is_set_over(scoreA, scoreB, setPoints)) { + if (scoreA > scoreB) { + winsA++; + } else { + winsB++; + } + + if (winsA >= requiredWins || winsB >= requiredWins) { + return true; + } + } else { + return false; + } + } + + return winsA >= requiredWins || winsB >= requiredWins; + } + + return { + is_match_over, + is_set_over, + }; +})(); + +/*@DEV*/ +if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { + module.exports = match_scoring; +} +/*/@DEV*/ diff --git a/test/test_btp_sync.js b/test/test_btp_sync.js new file mode 100644 index 0000000..6e18f3c --- /dev/null +++ b/test/test_btp_sync.js @@ -0,0 +1,274 @@ +'use strict'; + +const assert = require('assert'); + +const {_describe, _it} = require('./tutils.js'); + +const btp_sync = require('../bts/btp_sync'); + +_describe('btp_sync', () => { + _it('normalizes standard scoring formats from BTP fields', () => { + const normalized = btp_sync._normalize_scoring_format({ + ID: ['10'], + Name: ['Best of 3 to 21'], + NumSets: ['3'], + SetType: ['0'], + LastSetType: ['0'], + Score: ['21'], + IsDefault: [true], + }); + + assert.deepStrictEqual(normalized, { + id: 10, + name: 'Best of 3 to 21', + numSets: 3, + score: 21, + isDefault: true, + setType: 0, + lastSetType: 0, + set_points: { + end_points: 21, + max_points: 30, + end_points_editable: false, + max_points_editable: false, + interval_at: 11, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }, + last_set_points: { + end_points: 21, + max_points: 30, + end_points_editable: false, + max_points_editable: false, + interval_at: 11, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }, + }); + }); + + _it('normalizes editable scoring formats using the score fallback', () => { + const normalized = btp_sync._normalize_scoring_format({ + ID: ['11'], + Name: ['Custom 1x17'], + NumSets: ['1'], + SetType: ['999'], + LastSetType: ['999'], + Score: ['17'], + IsDefault: [false], + }); + + assert.deepStrictEqual(normalized.set_points, { + end_points: 17, + max_points: 17, + end_points_editable: false, + max_points_editable: true, + defaults_from_score: true, + interval_at: 9, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + assert.deepStrictEqual(normalized.last_set_points, { + end_points: 17, + max_points: 17, + end_points_editable: false, + max_points_editable: true, + defaults_from_score: true, + interval_at: 9, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + }); + + _it('normalizes fully editable set rules for set type 1000', () => { + const normalized = btp_sync._normalize_scoring_format({ + ID: ['13'], + Name: ['Custom free'], + NumSets: ['3'], + SetType: ['1000'], + LastSetType: ['1000'], + Score: ['0'], + IsDefault: [false], + }); + + assert.deepStrictEqual(normalized.set_points, { + end_points: 1, + max_points: 1, + end_points_editable: true, + max_points_editable: true, + interval_at: 1, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + assert.deepStrictEqual(normalized.last_set_points, { + end_points: 1, + max_points: 1, + end_points_editable: true, + max_points_editable: true, + interval_at: 1, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + }); + + _it('defaults interval point to rounded-up half of end points', () => { + const normalized = btp_sync._normalize_scoring_format({ + ID: ['14'], + Name: ['Custom 1x15'], + NumSets: ['1'], + SetType: ['999'], + LastSetType: ['999'], + Score: ['15'], + IsDefault: [false], + }); + + assert.strictEqual(normalized.set_points.interval_at, 8); + assert.strictEqual(normalized.last_set_points.interval_at, 8); + }); + + _it('sanitizes end_points to be at least 1 and max_points to be at least end_points', () => { + const sanitized = btp_sync._sanitize_scoring_format({ + id: 99, + name: 'Broken', + numSets: 1, + score: 0, + isDefault: false, + setType: 1000, + lastSetType: 1000, + set_points: { + end_points: 0, + max_points: 0, + }, + last_set_points: { + end_points: -5, + max_points: 2, + }, + }); + + assert.strictEqual(sanitized.set_points.end_points, 1); + assert.strictEqual(sanitized.set_points.max_points, 1); + assert.strictEqual(sanitized.last_set_points.end_points, 1); + assert.strictEqual(sanitized.last_set_points.max_points, 2); + }); + + _it('normalizes different rules for the last set', () => { + const normalized = btp_sync._normalize_scoring_format({ + ID: ['12'], + Name: ['2x21+11'], + NumSets: ['3'], + SetType: ['0'], + LastSetType: ['304'], + Score: ['21'], + IsDefault: [false], + }); + + assert.deepStrictEqual(normalized.set_points, { + end_points: 21, + max_points: 30, + end_points_editable: false, + max_points_editable: false, + interval_at: 11, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + assert.deepStrictEqual(normalized.last_set_points, { + end_points: 11, + max_points: 15, + end_points_editable: false, + max_points_editable: false, + interval_at: 6, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + }); + + _it('provides a complete 3x21 fallback scoring format', () => { + const normalized = btp_sync._fallback_scoring_format(); + + assert.deepStrictEqual(normalized, { + id: null, + name: '3x21', + numSets: 3, + score: 21, + isDefault: false, + setType: 0, + lastSetType: 0, + set_points: { + end_points: 21, + max_points: 30, + end_points_editable: false, + max_points_editable: false, + interval_at: 11, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }, + last_set_points: { + end_points: 21, + max_points: 30, + end_points_editable: false, + max_points_editable: false, + interval_at: 11, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }, + }); + }); + + _it('keeps locally edited timing values when BTP scoring formats are normalized again', () => { + const existing = { + id: 11, + name: 'Custom 1x17', + numSets: 1, + score: 17, + isDefault: false, + setType: 999, + lastSetType: 999, + set_points: { + end_points: 17, + max_points: 19, + end_points_editable: false, + max_points_editable: true, + defaults_from_score: true, + interval_at: 9, + interval_duration_ms: 45000, + break_before_set_duration_ms: 30000, + interval_enabled: false, + }, + last_set_points: { + end_points: 17, + max_points: 21, + end_points_editable: false, + max_points_editable: true, + defaults_from_score: true, + interval_at: 8, + interval_duration_ms: 40000, + break_before_set_duration_ms: 35000, + interval_enabled: true, + }, + }; + + const normalized = btp_sync._normalize_scoring_format({ + ID: ['11'], + Name: ['Custom 1x17'], + NumSets: ['1'], + SetType: ['999'], + LastSetType: ['999'], + Score: ['17'], + IsDefault: [false], + }); + + const merged = btp_sync._merge_local_scoring_format(existing, normalized); + + assert.strictEqual(merged.set_points.end_points, 17); + assert.strictEqual(merged.set_points.max_points, 19); + assert.strictEqual(merged.set_points.interval_at, 9); + assert.strictEqual(merged.set_points.interval_duration_ms, 45000); + assert.strictEqual(merged.set_points.break_before_set_duration_ms, 30000); + assert.strictEqual(merged.set_points.interval_enabled, false); + assert.strictEqual(merged.last_set_points.max_points, 21); + assert.strictEqual(merged.last_set_points.interval_at, 8); + assert.strictEqual(merged.last_set_points.interval_duration_ms, 40000); + assert.strictEqual(merged.last_set_points.break_before_set_duration_ms, 35000); + assert.strictEqual(merged.last_set_points.interval_enabled, true); + }); + }); diff --git a/test/test_cmatch.js b/test/test_cmatch.js new file mode 100644 index 0000000..c38f690 --- /dev/null +++ b/test/test_cmatch.js @@ -0,0 +1,36 @@ +'use strict'; + +const assert = require('assert'); + +const {_describe, _it} = require('./tutils.js'); + +const match_scoring = require('../static/js/match_scoring'); + +_describe('cmatch', () => { + _it('detects match end for default 3x21 fallback', () => { + assert.strictEqual(match_scoring.is_match_over([[21, 10], [21, 18]], null), true); + assert.strictEqual(match_scoring.is_match_over([[21, 10], [18, 21]], null), false); + }); + + _it('detects match end for 1x21 scoring format', () => { + const scoringFormat = { + numSets: 1, + set_points: { end_points: 21, max_points: 30 }, + last_set_points: { end_points: 21, max_points: 30 }, + }; + + assert.strictEqual(match_scoring.is_match_over([[21, 19]], scoringFormat), true); + assert.strictEqual(match_scoring.is_match_over([[20, 19]], scoringFormat), false); + }); + + _it('uses last-set limits for deciding the final set', () => { + const scoringFormat = { + numSets: 3, + set_points: { end_points: 21, max_points: 30 }, + last_set_points: { end_points: 11, max_points: 15 }, + }; + + assert.strictEqual(match_scoring.is_match_over([[21, 18], [19, 21], [11, 9]], scoringFormat), true); + assert.strictEqual(match_scoring.is_match_over([[21, 18], [19, 21], [10, 9]], scoringFormat), false); + }); +}); diff --git a/ticker/tdata.js b/ticker/tdata.js index a1c5739..da4f244 100644 --- a/ticker/tdata.js +++ b/ticker/tdata.js @@ -4,12 +4,21 @@ const async = require('async'); const utils = require('../bts/utils'); +function max_game_count(match) { + const scoringFormat = match && match.setup && match.setup.scoring_format; + const numSets = Number(scoringFormat && scoringFormat.numSets); + if (Number.isFinite(numSets) && numSets > 0) { + return numSets; + } + return 3; +} + function prepare_mustache(m) { m.p0str = m.p0.join('\n'); m.p1str = m.p1.join('\n'); - const max_game_count = 3; // TODO look it up from counting - m.gamesplus2 = max_game_count + 2; - const game_ids = utils.range(max_game_count); + const maxGames = max_game_count(m); + m.gamesplus2 = maxGames + 2; + const game_ids = utils.range(maxGames); for (const team_id of [0, 1]) { m['team' + team_id + 'scores'] = game_ids.map((game_idx) => { if (!m.s) return ''; From af103a6491fe98ada09e6a2134d2356f26d1e659 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 21 Feb 2026 22:51:28 +0100 Subject: [PATCH 210/224] Add querry ?location=Halle to filter current_matches, next_matces and upcoming views by url --- static/js/cmatch.js | 47 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index a976e32..34f4905 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -205,7 +205,6 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ return; } - if (!court && match.setup.court_id) { court = curt.courts_by_id[match.setup.court_id]; } @@ -1824,7 +1823,6 @@ function render_unassigned(container) { } function render_upcoming_matches(container) { - console.warn("Rendere upcoming"); const UPCOMING_MATCH_COUNT = parseInt(curt.upcoming_matches_max_count ? curt.upcoming_matches_max_count : 15); uiu.empty(container); @@ -1834,8 +1832,26 @@ function render_upcoming_matches(container) { const upcoming_table = uiu.el(container, 'table', 'upcoming_table'); const upcoming_tbody = uiu.el(upcoming_table, 'tbody', 'upcoming_tbody'); - const unassigned_matches = curt.matches.filter(m => calc_section(m) === 'unassigned'); - + + const locationById = Object.fromEntries( + curt.locations.map(l => [l._id, l]) + ); + + const params = new URLSearchParams(window.location.search); + const param_location = params.get("location"); + + const unassigned_matches = curt.matches.filter(m => { + if (calc_section(m) !== 'unassigned') return false; + if (!param_location) return true; + + const loc = locationById[m.setup.location_id]; + + // KEINE Location → trotzdem anzeigen + if (!loc) return true; + + // Location vorhanden → muss matchen + return loc.name === param_location; + }); var resizable_rows = []; for (const match of unassigned_matches.slice(0, UPCOMING_MATCH_COUNT)) { @@ -1933,7 +1949,28 @@ function render_courts(container, style) { const table = uiu.el(container, 'table', 'match_table'); const tbody = uiu.el(table, 'tbody'); var resizable_rows = []; - for (const c of curt.courts) { + + const locationById = Object.fromEntries( + curt.locations.map(l => [l._id, l]) + ); + + const params = new URLSearchParams(window.location.search); + const param_location = params.get("location"); + + + const courts = curt.courts.filter(c => { + if (!param_location) return true; + + const loc = locationById[c.location_id]; + + // KEINE Location → trotzdem anzeigen + if (!loc) return true; + + // Location vorhanden → muss matchen + return loc.name === param_location; + }); + + for (const c of courts) { const expected_section = 'court_' + c._id; const court_matches = curt.matches.filter(m => calc_section(m) === expected_section); From 78dc792a07049fa4aae1accd9ffa1bca898d45f8 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 24 Jan 2026 14:00:11 +0100 Subject: [PATCH 211/224] Import the scoring format from BTP and use this scoring format for all internal calculations. --- bts/admin.js | 13 +- bts/btp_conn.js | 2 +- bts/btp_parse.js | 6 + bts/btp_sync.js | 571 +++++++++++++++++++++++++++++++++++-- bts/bupws.js | 34 ++- bts/match_utils.js | 33 ++- bts/ticker_conn.js | 2 +- static/cbts.html | 3 +- static/css/admin.css | 93 ++++++ static/js/change.js | 31 +- static/js/ci18n_de.js | 32 ++- static/js/ci18n_en.js | 30 +- static/js/cmatch.js | 56 +--- static/js/ctournament.js | 469 ++++++++++++++++++++++++++++++ static/js/match_scoring.js | 92 ++++++ test/test_btp_sync.js | 274 ++++++++++++++++++ test/test_cmatch.js | 36 +++ ticker/tdata.js | 15 +- 18 files changed, 1677 insertions(+), 115 deletions(-) create mode 100644 static/js/match_scoring.js create mode 100644 test/test_btp_sync.js create mode 100644 test/test_cmatch.js diff --git a/bts/admin.js b/bts/admin.js index 8181083..968dbcd 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -80,7 +80,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'annoncement_include_event', 'annoncement_include_round','annoncement_include_matchnumber', 'preparation_meetingpoint_enabled', 'preparation_tabletoperator_setup_enabled', 'call_preparation_matches_automatically_enabled', 'call_next_possible_scheduled_match_in_preparation' , - 'logo_background_color', 'logo_foreground_color']); + 'logo_background_color', 'logo_foreground_color', 'scoring_formats']); if (msg.props.btp_timezone) { props.btp_timezone = msg.props.btp_timezone === 'system' ? undefined : msg.props.btp_timezone; @@ -352,6 +352,13 @@ function handle_tournament_get(app, ws, msg) { cb(err); }); }], function(err) { + if (tournament.scoring_formats && Array.isArray(tournament.scoring_formats.formats)) { + const btp_sync = require('./btp_sync'); + tournament.scoring_formats = { + ...tournament.scoring_formats, + formats: tournament.scoring_formats.formats.map(f => btp_sync._sanitize_scoring_format(f)), + }; + } tournament.btp_status = btp_manager.get_status(tournament.key); tournament.ticker_status = ticker_manager.get_status(tournament.key); _annotate_tournament(tournament); @@ -391,6 +398,7 @@ function _extract_setup(msg_setup) { 'links', 'scheduled_time_str', 'scheduled_date', + 'scoring_format', 'called_timestamp', 'preparation_call_timestamp', 'location_id', @@ -405,7 +413,6 @@ function _extract_setup(msg_setup) { if (!setup.match_name && setup.match_num) { setup.match_name = '# ' + setup.match_num; } - setup.counting = '3x21'; return setup; } @@ -1735,4 +1742,4 @@ module.exports = { generate_tournament_web_url, on_close, on_connect, -}; \ No newline at end of file +}; diff --git a/bts/btp_conn.js b/bts/btp_conn.js index e74f090..8201c0e 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -260,7 +260,7 @@ class BTPConn { }); }, (umpire_btp_id, service_judge_btp_id, cb) => { - if (!match.setup || !match.setup.court_id) { + if (!match.setup || !match.setup.court_id || match.setup.now_on_court !== true) { return cb(null, umpire_btp_id, service_judge_btp_id, null); } diff --git a/bts/btp_parse.js b/bts/btp_parse.js index 5dd9171..aba3c47 100644 --- a/bts/btp_parse.js +++ b/bts/btp_parse.js @@ -218,6 +218,7 @@ function get_btp_state(response) { const all_btp_entries = btp_t.Entries ? btp_t.Entries[0].Entry : []; const all_btp_stage_entries = btp_t.StageEntries ? btp_t.StageEntries[0].StageEntry : []; const all_btp_events = btp_t.Events ? btp_t.Events[0].Event : []; + const all_btp_stages = btp_t.Stages ? btp_t.Stages[0].Stage : []; const all_btp_players = btp_t.Players ? btp_t.Players[0].Player : []; const all_btp_draws = btp_t.Draws ? btp_t.Draws[0].Draw : []; const all_btp_officials = btp_t.Officials ? btp_t.Officials[0].Official : []; @@ -226,6 +227,7 @@ function get_btp_state(response) { const all_btp_settings = btp_t.Settings ? btp_t.Settings[0].Setting : []; const all_btp_clubs = btp_t.Clubs ? btp_t.Clubs[0].Club : []; const all_btp_districts = btp_t.Districts ? btp_t.Districts[0].District : []; + const all_btp_scoring_formats = btp_t.ScoringFormats ? btp_t.ScoringFormats[0].ScoringFormat : []; const on_court_match_ids = new Set(); for (const c of all_btp_courts) { @@ -239,6 +241,7 @@ function get_btp_state(response) { const entries = utils.make_index(all_btp_entries, e => e.ID[0]); const stage_entries = utils.make_index(all_btp_stage_entries, s => s.ID[0]); const events = utils.make_index(all_btp_events, e => e.ID[0]); + const stages = utils.make_index(all_btp_stages, e => e.ID[0]); const draws = utils.make_index(all_btp_draws, d => d.ID[0]); let team_matches = undefined; @@ -262,6 +265,7 @@ function get_btp_state(response) { const locations = utils.make_index(all_btp_locations, l => l.ID[0]); const clubs = utils.make_index(all_btp_clubs, c => c.ID[0]); const districts = utils.make_index(all_btp_districts,d => d.ID[0]); + const scoring_formats = utils.make_index(all_btp_scoring_formats, sf => sf.ID[0]); const btp_settings = utils.make_index(all_btp_settings, s =>s.ID[0]); for (const bm of matches) { @@ -272,6 +276,7 @@ function get_btp_state(response) { locations, draws, events, + stages, matches, links, officials, @@ -281,6 +286,7 @@ function get_btp_state(response) { teams, clubs, districts, + scoring_formats, // Testing only _matches_by_pid: matches_by_pid, btp_settings diff --git a/bts/btp_sync.js b/bts/btp_sync.js index d140046..b79369a 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -18,7 +18,7 @@ function date_str(dt) { return utils.pad(dt.year, 2, '0') + '-' + utils.pad(dt.month, 2, '0') + '-' + utils.pad(dt.day, 2, '0'); } -async function craft_match(app, tkey, btp_id, location_map, court_map, event, draw, btp_links, officials, clubs, districts, bm, match_ids_on_court, match_types, is_league) { +async function craft_match(app, tkey, btp_id, location_map, court_map, event, stage, scoring_formats, draw, btp_links, officials, clubs, districts, bm, match_ids_on_court, match_types, is_league) { return new Promise((resolve, reject) => { const stournament = require('./stournament'); // avoid dependency cycle @@ -127,17 +127,30 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, dr } } + + let scoring_format = null; + + if (stage.ScoringFormat) { + scoring_format = scoring_formats.get(Number(stage.ScoringFormat)); + } else { + scoring_format = findDefaultScoringFormat(scoring_formats); + } + + // Fallback, falls gar nichts gefunden wird + if (!scoring_format) { + scoring_format = fallbackScoringFormat(); + } + const setup = { is_match: (bm.IsMatch && bm.IsMatch[0] ? true : false), incomplete: !bm.bts_complete, is_doubles: (gtid === 2), match_num: bm.MatchNr[0], - counting: '3x21', + scoring_format: scoring_format, team_competition: false, - //match_name, event_name, teams, - warmup: 'none', + warmup: "none", links: links, highlight: bm.Highlight[0], }; @@ -147,11 +160,6 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, dr if (err) { reject(err); } - - if (tournament.warmup) { - setup.warmup = tournament.warmup; - } - if (tournament.warmup) { setup.warmup = tournament.warmup; } @@ -256,6 +264,17 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, dr }); } +function findDefaultScoringFormat(scoringFormatMap) { + + for (const entry of scoringFormatMap.entries()) { + const id = entry[0]; + const sf = entry[1]; + + if (sf && sf.isDefault) return sf; + } + return null; +} + function _craft_team(par) { if (!par) { return { players: [] }; @@ -501,10 +520,10 @@ function get_umpire(app, tkey, umpires , btp_id) { return returnValue; } -async function integrate_matches(app, tkey, btp_state, location_map, court_map, callback) { +async function integrate_matches(app, tkey, btp_state, scoring_formats, location_map, court_map, callback) { const admin = require('./admin'); // avoid dependency cycle const match_utils = require('./match_utils'); - const { draws, events } = btp_state; + const { draws, events, stages } = btp_state; const match_ids_on_court = calculate_match_ids_on_court(btp_state); @@ -525,6 +544,9 @@ async function integrate_matches(app, tkey, btp_state, location_map, court_map, const event = events.get(draw.EventID[0]); assert(event); + const stage = stages.get(draw.StageID[0]); + assert(stage); + const btp_id = calculate_btp_match_id(tkey, bm, draws, events); if (bm.ReverseHomeAway) { @@ -547,7 +569,7 @@ async function integrate_matches(app, tkey, btp_state, location_map, court_map, return; } - craft_match(app, tkey, btp_id, location_map, court_map, event, draw, btp_state.links, officials, clubs, districts, bm, match_ids_on_court).then(match => { + craft_match(app, tkey, btp_id, location_map, court_map, event, stage, scoring_formats, draw, btp_state.links, officials, clubs, districts, bm, match_ids_on_court).then(match => { match.setup.state = 'unscheduled'; @@ -571,6 +593,18 @@ async function integrate_matches(app, tkey, btp_state, location_map, court_map, if (cur_match.btp_winner) { match.setup.state = 'finished'; } + if (typeof cur_match.team1_won === 'boolean' || cur_match.btp_winner || cur_match.btp_needsync) { + match.setup.now_on_court = false; + match.setup.state = 'finished'; + } else if (cur_match.setup.now_on_court === true) { + // Keep the local on-court state until the result is explicitly confirmed. + match.setup.now_on_court = true; + if (cur_match.setup.state === 'blocked') { + match.setup.state = 'blocked'; + } else if (cur_match.setup.called_timestamp) { + match.setup.state = 'oncourt'; + } + } if (!match.network_score && cur_match.network_score) { match.network_score = cur_match.network_score; @@ -866,7 +900,7 @@ function generateHallAbbreviation(name) { return abbreviation.trim(); } -function integrate_locations(app, tournament_key, btp_state, callback) { +function integrate_locations(app, tournament_key, btp_state, scoring_formats, callback) { const admin = require('./admin'); // avoid dependency cycle const stournament = require('./stournament'); // avoid dependency cycle @@ -963,10 +997,10 @@ function integrate_locations(app, tournament_key, btp_state, callback) { if (changed) { stournament.get_locations(app.db, tournament_key, function (err, all_locations) { admin.notify_change(app, tournament_key, 'location_changed', { all_locations }); - callback(err, res); + callback(err, scoring_formats, res); }); } else { - callback(err, res); + callback(err, scoring_formats, res); } }); } @@ -975,7 +1009,7 @@ function integrate_locations(app, tournament_key, btp_state, callback) { // Returns a map btp_court_id => court._id -function integrate_courts(app, tournament_key, btp_state, location_map, callback) { +function integrate_courts(app, tournament_key, btp_state, scoring_formats, location_map, callback) { const admin = require('./admin'); // avoid dependency cycle const stournament = require('./stournament'); // avoid dependency cycle @@ -1044,10 +1078,10 @@ function integrate_courts(app, tournament_key, btp_state, location_map, callback if (changed) { stournament.get_courts(app.db, tournament_key, function (err, all_courts) { admin.notify_change(app, tournament_key, 'courts_changed', { all_courts }); - callback(err, location_map, res); + callback(err, scoring_formats, location_map, res); }); } else { - callback(err, location_map, res); + callback(err, scoring_formats, location_map, res); } }); } @@ -1105,6 +1139,400 @@ function integrate_btp_settings(app, tkey, btp_state, callback) { }); } +function buildScoringFormatMap(formats) { + const map = new Map(); + for (const f of formats) { + map.set(Number(f.id), f); + } + return map; +} + +function setTypeToEndMax(setType, score) { + const t = Number(setType); + switch (t) { + case 0: return { end_points: 21, max_points: 30, end_points_editable: false, max_points_editable: false }; + case 301: return { end_points: 11, max_points: 11, end_points_editable: false, max_points_editable: false }; + case 304: return { end_points: 11, max_points: 15, end_points_editable: false, max_points_editable: false }; + case 305: return { end_points: 11, max_points: 13, end_points_editable: false, max_points_editable: false }; + case 306: return { end_points: 15, max_points: 21, end_points_editable: false, max_points_editable: false }; + case 1000: + return { + end_points: null, + max_points: null, + end_points_editable: true, + max_points_editable: true, + }; + case 999: { + const s = Number(score); + const fallback = Number.isFinite(s) && s > 0 ? s : null; + return { + end_points: fallback, + max_points: fallback, + end_points_editable: false, + max_points_editable: true, + defaults_from_score: true, + }; + } + default: + return { + end_points: null, + max_points: null, + end_points_editable: false, + max_points_editable: false, + raw: t, + }; + } +} + +function inferSetTiming(name, numSets, setType, isLastSet) { + const normalizedName = String(name || ''); + const t = Number(setType); + const singleSet = Number(numSets) === 1; + + function elevenSetTiming() { + if (singleSet || isLastSet) { + return { + interval_at: 6, + interval_duration_ms: normalizedName.includes('^90') ? 90000 : 60000, + }; + } + return { + interval_at: null, + interval_duration_ms: null, + }; + } + + let timing; + switch (t) { + case 0: + timing = { + interval_at: 11, + interval_duration_ms: 60000, + }; + break; + case 301: + case 304: + case 305: + timing = elevenSetTiming(); + break; + case 306: + timing = { + interval_at: 8, + interval_duration_ms: 60000, + }; + break; + default: + timing = { + interval_at: null, + interval_duration_ms: null, + }; + break; + } + + let breakBeforeSetDurationMs = null; + if (!singleSet) { + if (normalizedName.includes('2x21+11') && isLastSet) { + breakBeforeSetDurationMs = 120000; + } else if (normalizedName.includes('^90')) { + breakBeforeSetDurationMs = 90000; + } else if ( + normalizedName.includes('~NLA') || + t === 0 || + t === 306 + ) { + breakBeforeSetDurationMs = 120000; + } else if (t === 301 || t === 304 || t === 305) { + breakBeforeSetDurationMs = 60000; + } + } + + return { + ...timing, + break_before_set_duration_ms: breakBeforeSetDurationMs, + }; +} + +function applyDefaultSetTiming(setPoints) { + const merged = { + ...setPoints, + }; + const endPoints = Number(merged.end_points); + if (merged.interval_at == null && Number.isFinite(endPoints) && endPoints > 0) { + merged.interval_at = Math.ceil(endPoints / 2); + } + if (merged.interval_duration_ms == null) { + merged.interval_duration_ms = 60000; + } + if (merged.break_before_set_duration_ms == null) { + merged.break_before_set_duration_ms = 120000; + } + return merged; +} + +function sanitizeSetPoints(setPoints) { + const merged = applyDefaultSetTiming(setPoints); + let endPoints = Number(merged.end_points); + if (!Number.isFinite(endPoints) || endPoints < 1) { + endPoints = 1; + } + let maxPoints = Number(merged.max_points); + if (!Number.isFinite(maxPoints) || maxPoints < endPoints) { + maxPoints = endPoints; + } + merged.end_points = endPoints; + merged.max_points = maxPoints; + if (merged.interval_at == null) { + merged.interval_at = Math.ceil(endPoints / 2); + } + return merged; +} + +function sanitizeScoringFormat(scoringFormat) { + if (!scoringFormat) { + return scoringFormat; + } + return { + ...scoringFormat, + set_points: sanitizeSetPoints(scoringFormat.set_points || {}), + last_set_points: sanitizeSetPoints(scoringFormat.last_set_points || {}), + }; +} + +function normalizeScoringFormat(sf, unwrap = (v) => (Array.isArray(v) && v.length === 1 ? v[0] : v)) { + const id = Number(unwrap(sf.ID)); + const name = String(unwrap(sf.Name)); + const numSets = Number(unwrap(sf.NumSets)); + const setType = Number(unwrap(sf.SetType)); + const lastSetType = Number(unwrap(sf.LastSetType)); + const score = Number(unwrap(sf.Score)); + const isDefault = Boolean(unwrap(sf.IsDefault)); + + return sanitizeScoringFormat({ + id, + name, + numSets, + score, + isDefault, + setType, + lastSetType, + set_points: applyDefaultSetTiming({ + ...setTypeToEndMax(setType, score), + ...inferSetTiming(name, numSets, setType, false), + }), + last_set_points: applyDefaultSetTiming({ + ...setTypeToEndMax(lastSetType, score), + ...inferSetTiming(name, numSets, lastSetType, true), + }), + }); +} + +function fallbackScoringFormat() { + const scoringFormat = normalizeScoringFormat({ + ID: [0], + Name: ['3x21'], + NumSets: ['3'], + SetType: ['0'], + LastSetType: ['0'], + Score: ['21'], + IsDefault: [false], + }); + scoringFormat.id = null; + return scoringFormat; +} + +function mergeLocalSetPoints(existingSetPoints, normalizedSetPoints) { + const merged = { + ...normalizedSetPoints, + }; + if (!existingSetPoints) { + return merged; + } + + if (normalizedSetPoints.end_points_editable) { + merged.end_points = existingSetPoints.end_points ?? merged.end_points; + } + if (normalizedSetPoints.max_points_editable) { + merged.max_points = existingSetPoints.max_points ?? merged.max_points; + } + + merged.interval_at = existingSetPoints.interval_at ?? merged.interval_at; + merged.interval_duration_ms = existingSetPoints.interval_duration_ms ?? merged.interval_duration_ms; + merged.break_before_set_duration_ms = existingSetPoints.break_before_set_duration_ms ?? merged.break_before_set_duration_ms; + if (existingSetPoints.interval_enabled !== undefined) { + merged.interval_enabled = existingSetPoints.interval_enabled; + } + + return merged; +} + +function mergeLocalScoringFormat(existingFormat, normalizedFormat) { + if (!existingFormat) { + return sanitizeScoringFormat(normalizedFormat); + } + return sanitizeScoringFormat({ + ...normalizedFormat, + set_points: mergeLocalSetPoints(existingFormat.set_points, normalizedFormat.set_points), + last_set_points: mergeLocalSetPoints(existingFormat.last_set_points, normalizedFormat.last_set_points), + }); +} + +function integrate_btp_scoring_formats(app, tkey, btp_state, callback) { + const admin = require("./admin"); // avoid dependency cycle + + const unwrap = (v) => (Array.isArray(v) && v.length === 1 ? v[0] : v); + + const deepEqualJson = (a, b) => JSON.stringify(a) === JSON.stringify(b); + + app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { + if (err) return callback(err); + if (!tournament) return callback(new Error(`Tournament not found for key: ${tkey}`)); + + if (!tournament.scoring_formats) tournament.scoring_formats = {}; + + if (!btp_state?.scoring_formats || !(btp_state.scoring_formats instanceof Map)) { + return callback(new Error("btp_state.scoring_formats is missing or not a Map")); + } + + const existingFormatsById = new Map( + (((tournament.scoring_formats || {}).formats) || []).map(f => [Number(f.id), f]) + ); + + const formats = Array.from(btp_state.scoring_formats.values()) + .map(sf => normalizeScoringFormat(sf, unwrap)) + .map(sf => mergeLocalScoringFormat(existingFormatsById.get(Number(sf.id)), sf)) + .sort((a, b) => a.id - b.id); + + const defaultFormat = formats.find(f => f.isDefault) || null; + + const scoringFormatsPayload = { + formats, + default_id: defaultFormat ? defaultFormat.id : null, + }; + + const scoringFormatMap = buildScoringFormatMap(formats); + + const existing = tournament.scoring_formats || null; + + // No change + if (deepEqualJson(existing, scoringFormatsPayload)) { + return callback(null, scoringFormatMap); + } + + tournament.scoring_formats = scoringFormatsPayload; + + app.db.tournaments.update( + { key: tkey }, + { $set: { scoring_formats: tournament.scoring_formats } }, + {}, + (err) => { + if (err) return callback(err); + + admin.notify_change(app, tkey, "update_btp_scoring_formats", { + scoring_formats: scoringFormatsPayload, + }); + + return callback(null, scoringFormatMap); + } + ); + }); +} + +function integrate_events(app, tkey, btp_state, callback) { + const admin = require("./admin"); // avoid dependency cycle + + const unwrap = (v) => (Array.isArray(v) && v.length === 1 ? v[0] : v); + const deepEqualJson = (a, b) => JSON.stringify(a) === JSON.stringify(b); + + if (!btp_state || !(btp_state.events instanceof Map) || !(btp_state.stages instanceof Map)) { + return callback(new Error("btp_state.events/stages missing or not a Map")); + } + + function normalizeEvent(ev) { + return { + id: Number(unwrap(ev.ID)), + name: String(unwrap(ev.Name)), + game_type_id: Number(unwrap(ev.GameTypeID)), + gender_id: Number(unwrap(ev.GenderID)), + min_age: Number(unwrap(ev.MinAge)), + max_age: Number(unwrap(ev.MaxAge)), + fee: Number(unwrap(ev.Fee)), + separate_seeding: Boolean(unwrap(ev.SeparateSeeding)), + allow_online_entry: Boolean(unwrap(ev.AllowOnlineEntry)), + grading_id: Number(unwrap(ev.GradingID)), + sub_grading_id: Number(unwrap(ev.SubGradingID)), + sub_grading2_id: Number(unwrap(ev.SubGrading2ID)), + }; + } + + function normalizeStage(st) { + return { + id: Number(unwrap(st.ID)), + name: String(unwrap(st.Name)), + event_id: Number(unwrap(st.EventID)), + stage_type: Number(unwrap(st.StageType)), + display_order: Number(unwrap(st.DisplayOrder)), + scoring_format: st.ScoringFormat !== undefined ? Number(unwrap(st.ScoringFormat)) : null, + }; + } + + // Build normalized payload: + // events: [{... , stages:[...]}] for convenient GUI + lookups + const eventsArr = Array.from(btp_state.events.values()) + .map(normalizeEvent) + .sort((a, b) => a.id - b.id); + + const stagesArr = Array.from(btp_state.stages.values()) + .map(normalizeStage) + .sort((a, b) => a.id - b.id); + + const stagesByEventId = new Map(); + for (const st of stagesArr) { + if (!stagesByEventId.has(st.event_id)) stagesByEventId.set(st.event_id, []); + stagesByEventId.get(st.event_id).push(st); + } + + // Keep stages sorted by display_order then id for stability + for (const [eventId, list] of stagesByEventId.entries()) { + list.sort((a, b) => (a.display_order - b.display_order) || (a.id - b.id)); + } + + const payload = { + events: eventsArr.map((ev) => ({ + ...ev, + stages: stagesByEventId.get(ev.id) || [], + })), + // optional: keep a flat list too, if you prefer later + // stages: stagesArr, + }; + + app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { + if (err) return callback(err); + if (!tournament) return callback(new Error(`Tournament not found for key: ${tkey}`)); + + if (!tournament.events) tournament.events = {}; + + const existing = tournament.events || null; + if (deepEqualJson(existing, payload)) { + return callback(null); + } + + tournament.events = payload; + + const toChange = { events: tournament.events }; + + app.db.tournaments.update({ key: tkey }, { $set: toChange }, {}, (err) => { + if (err) return callback(err); + + admin.notify_change(app, tkey, "update_btp_events", { + events: payload, + }); + + return callback(null); + }); + }); +} + + async function integrate_player_state(app, tkey, btp_state, callback) { const btp_manager = require('./btp_manager'); app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { @@ -1343,6 +1771,79 @@ async function integrate_now_on_court(app, tkey, callback) { const bupws = require('./bupws'); const match_utils = require('./match_utils'); + function matchHasPlayerOnCourtFlags(match) { + if (!match || !match.setup || !match.setup.teams) { + return false; + } + return match.setup.teams.some(team => + team.players && team.players.some(player => player.now_playing_on_court || player.now_tablet_on_court) + ); + } + + function collectActivePlayerIds(matches) { + const activeIds = new Set(); + matches.forEach(match => { + if (!match || !match.setup || !match.setup.teams) { + return; + } + match.setup.teams.forEach(team => { + if (!team.players) { + return; + } + team.players.forEach(player => { + if (player && player.btp_id) { + activeIds.add(player.btp_id); + } + }); + }); + if (match.setup.tabletoperators) { + match.setup.tabletoperators.forEach(player => { + if (player && player.btp_id) { + activeIds.add(player.btp_id); + } + }); + } + }); + return activeIds; + } + + function matchHasOnlyStalePlayerFlags(match, activePlayerIds) { + if (!match || !match.setup || !match.setup.teams) { + return false; + } + return match.setup.teams.some(team => + team.players && team.players.some(player => + (player.now_playing_on_court || player.now_tablet_on_court) && + (!player.btp_id || !activePlayerIds.has(player.btp_id)) + ) + ); + } + + function setPlayerStateForMatch(match) { + return new Promise((resolve, reject) => { + match_utils.set_player_on_court(app, tkey, match.setup, (err) => { + if (err) return reject(err); + match_utils.set_player_on_tablet(app, tkey, match.setup, (err) => { + if (err) return reject(err); + resolve(null); + }); + }); + }); + } + + function clearPlayerStateForMatch(match) { + const endTs = match.end_ts || Date.now(); + return new Promise((resolve, reject) => { + match_utils.remove_player_on_court(app, tkey, match._id, endTs, (err) => { + if (err) return reject(err); + match_utils.remove_tablet_on_court(app, tkey, match._id, endTs, (err) => { + if (err) return reject(err); + resolve(null); + }); + }); + }); + } + // TODO after switching to async, this should happen during court&match construction app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { if (err) { @@ -1353,7 +1854,8 @@ async function integrate_now_on_court(app, tkey, callback) { app.db.matches.find({ 'setup.now_on_court': true }, async (err, now_on_court_matches) => { if (err) return callback(err); - await Promise.all(now_on_court_matches.map(async (match) => { + const activeMatches = now_on_court_matches.filter(match => typeof match.team1_won !== 'boolean'); + await Promise.all(activeMatches.map(async (match) => { const court_id = match.setup.court_id; const match_id = match._id; @@ -1374,8 +1876,24 @@ async function integrate_now_on_court(app, tkey, callback) { }; app.db.courts.update(query, {$set: {match_id}}); } + await setPlayerStateForMatch(match); })); - callback(null); + + app.db.matches.find({ tournament_key: tkey }, async (err, matches) => { + if (err) return callback(err); + + const activePlayerIds = collectActivePlayerIds(activeMatches); + const staleMatches = matches.filter(match => + match && + match.setup && + match.setup.now_on_court !== true && + matchHasPlayerOnCourtFlags(match) && + matchHasOnlyStalePlayerFlags(match, activePlayerIds) + ); + + await Promise.all(staleMatches.map(match => clearPlayerStateForMatch(match))); + callback(null); + }); }); }); // TODO clear courts (better in async) @@ -1393,11 +1911,13 @@ async function sync_btp_data(app, tkey, response) { async.waterfall([ cb => integrate_btp_settings(app, tkey, btp_state, cb), + cb => integrate_events(app, tkey, btp_state, cb), cb => integrate_player_state(app, tkey, btp_state, cb), cb => integrate_umpires(app, tkey, btp_state, cb), - cb => integrate_locations(app, tkey, btp_state, cb), - (location_map, cb) => integrate_courts(app, tkey, btp_state, location_map, cb), - (location_map, court_map, cb) => integrate_matches(app, tkey, btp_state, location_map, court_map, cb), + cb => integrate_btp_scoring_formats(app, tkey, btp_state, cb), + (scoring_formats, cb) => integrate_locations(app, tkey, btp_state, scoring_formats, cb), + (scoring_formats, location_map, cb) => integrate_courts(app, tkey, btp_state, scoring_formats, location_map, cb), + (scoring_formats, location_map, court_map, cb) => integrate_matches(app, tkey, btp_state, scoring_formats, location_map, court_map, cb), cb => integrate_now_on_court(app, tkey, cb), cb => cleanup_entities(app, tkey, btp_state, cb), ], (err) => { @@ -1418,4 +1938,9 @@ module.exports = { time_str, // test only _integrate_umpires: integrate_umpires, + _fallback_scoring_format: fallbackScoringFormat, + _normalize_scoring_format: normalizeScoringFormat, + _merge_local_scoring_format: mergeLocalScoringFormat, + _sanitize_scoring_format: sanitizeScoringFormat, + _set_type_to_end_max: setTypeToEndMax, }; diff --git a/bts/bupws.js b/bts/bupws.js index a311044..9959b8f 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -148,7 +148,9 @@ async function handle_score_update(app, ws, msg) { } catch { var match = null; } - if (match == null || match.setup.now_on_court == false) { + const finish_confirmed = score_data.finish_confirmed ? score_data.finish_confirmed : false; + const allow_finished_confirmation = finish_confirmed && (score_data.team1_won !== undefined && score_data.team1_won !== null); + if (match == null || (match.setup.now_on_court == false && !allow_finished_confirmation)) { send_error(ws, tournament_key, "Match not found or not on court actualy."); return; } @@ -170,12 +172,14 @@ async function handle_score_update(app, ws, msg) { device_info.client_ip = client_ip; } - const finish_confirmed = score_data.finish_confirmed ? score_data.finish_confirmed : false; + const match_finished = score_data.team1_won !== undefined && score_data.team1_won !== null; if (finish_confirmed) { - update.team1_won = score_data.team1_won, - update.btp_winner = (update.team1_won === true) ? 1 : 2; - update.btp_needsync = true; update["setup.now_on_court"] = false; + update.team1_won = score_data.team1_won; + } + if (finish_confirmed) { + update.btp_winner = (update.team1_won === true) ? 1 : 2; + update.btp_needsync = true; } if (score_data.shuttle_count) { @@ -207,12 +211,14 @@ async function handle_score_update(app, ws, msg) { cb(null, match); }, (match, cb) => { - if (match) { - if (finish_confirmed) { - btp_manager.update_score(app, match); - update_queue.instance().execute(match_utils.reset_player_tabletoperator, app, tournament_key, match_id, update.end_ts) - .then(() => { - cb(null, match); + if (match) { + if (finish_confirmed) { + if (finish_confirmed) { + btp_manager.update_score(app, match); + } + update_queue.instance().execute(match_utils.reset_player_tabletoperator, app, tournament_key, match_id, update.end_ts) + .then(() => { + cb(null, match); }) .catch((err) => { console.error("Error in reset_player_tabletoperator:", err); @@ -275,8 +281,8 @@ async function handle_score_update(app, ws, msg) { if (!match) { return cb(new Error('Cannot find match ' + JSON.stringify(match))); } - if (finish_confirmed && match.team1_won != undefined && match.team1_won != null) { - const next_match = update_queue.instance().execute(match_utils.call_preparation_match_on_court,app, tournament_key, match.setup.court_id); + if (finish_confirmed) { + update_queue.instance().execute(match_utils.call_preparation_match_on_court, app, tournament_key, match.setup.court_id); } return cb(null, match, changed_court); }, @@ -950,4 +956,4 @@ module.exports = { add_display_status, create_match_representation, create_event_representation, -}; \ No newline at end of file +}; diff --git a/bts/match_utils.js b/bts/match_utils.js index 6ebcf37..461b178 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -674,6 +674,7 @@ function add_tabletoperator_to_tabletoperator_list_by_match(app, tournament_key, function remove_player_on_court (app, tkey, cur_match_id, end_ts = null, callback) { const admin = require('./admin'); // avoid dependency cycle + const btp_manager = require('./btp_manager'); app.db.matches.findOne({'tournament_key': tkey, '_id': cur_match_id}, (err, cur_match) => { if (err) return callback(err); @@ -694,6 +695,8 @@ function remove_player_on_court (app, tkey, cur_match_id, end_ts = null, callbac } const match_id = match._id; + const players_to_change = []; + const is_finished_match = match_id === cur_match_id; let remove_btp_ids = [ cur_match.setup.teams[0].players[0].btp_id, cur_match.setup.teams[1].players[0].btp_id]; @@ -709,49 +712,54 @@ function remove_player_on_court (app, tkey, cur_match_id, end_ts = null, callbac if (match.setup.teams[0].players.length > 0 && remove_btp_ids.includes(match.setup.teams[0].players[0].btp_id) && - match.setup.teams[0].players[0].now_playing_on_court) { + (match.setup.teams[0].players[0].now_playing_on_court || is_finished_match)) { match.setup.teams[0].players[0].now_playing_on_court = false; match.setup.teams[0].players[0].checked_in = false; if(end_ts) { match.setup.teams[0].players[0].last_time_on_court_ts = end_ts; } + players_to_change.push(match.setup.teams[0].players[0]); change = true; } if (match.setup.teams[0].players.length > 1 && remove_btp_ids.includes(match.setup.teams[0].players[1].btp_id) && - match.setup.teams[0].players[1].now_playing_on_court) { + (match.setup.teams[0].players[1].now_playing_on_court || is_finished_match)) { match.setup.teams[0].players[1].now_playing_on_court = false; match.setup.teams[0].players[1].checked_in = false; if(end_ts) { match.setup.teams[0].players[1].last_time_on_court_ts = end_ts; } + players_to_change.push(match.setup.teams[0].players[1]); change = true; } if (match.setup.teams[1].players.length > 0 && remove_btp_ids.includes(match.setup.teams[1].players[0].btp_id) && - match.setup.teams[1].players[0].now_playing_on_court) { + (match.setup.teams[1].players[0].now_playing_on_court || is_finished_match)) { match.setup.teams[1].players[0].now_playing_on_court = false; match.setup.teams[1].players[0].checked_in = false; if(end_ts) { match.setup.teams[1].players[0].last_time_on_court_ts = end_ts; } + players_to_change.push(match.setup.teams[1].players[0]); change = true; } if (match.setup.teams[1].players.length > 1 && remove_btp_ids.includes(match.setup.teams[1].players[1].btp_id) && - match.setup.teams[1].players[1].now_playing_on_court) { + (match.setup.teams[1].players[1].now_playing_on_court || is_finished_match)) { match.setup.teams[1].players[1].now_playing_on_court = false; match.setup.teams[1].players[1].checked_in = false; if(end_ts) { match.setup.teams[1].players[1].last_time_on_court_ts = end_ts; } + players_to_change.push(match.setup.teams[1].players[1]); change = true; } if (change) { + btp_manager.update_players(app, tkey, players_to_change); const setup = match.setup; const match_q = {_id: match_id}; app.db.matches.update(match_q, {$set: {setup}}, {}, (err) => { @@ -1162,11 +1170,22 @@ function update_btp_courts(app, tournament_key, match, callback) { } function reset_player_tabletoperator(app, tournament_key, match_id, end_ts) { return new Promise((resolve, reject) => { + let current_match = null; async.waterfall([ + cb => fetch_match(app, tournament_key, match_id).then((match) => { + current_match = match; + cb(null); + }).catch(cb), cb => remove_player_on_court(app, tournament_key, match_id, end_ts, cb), cb => remove_tablet_on_court(app, tournament_key, match_id, end_ts, cb), cb => remove_umpire_on_court(app, tournament_key, match_id, end_ts, cb), - cb => add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts, cb) + cb => add_player_to_tabletoperator_list(app, tournament_key, match_id, end_ts, cb), + cb => { + if (!current_match) { + return cb(null); + } + update_btp_courts(app, tournament_key, current_match, cb); + }, ], function (err) { if (err) { return reject(err); @@ -1190,6 +1209,8 @@ module.exports ={ remove_player_on_court, remove_tablet_on_court, remove_umpire_on_court, + set_player_on_court, + set_player_on_tablet, set_umpire_to_standby, add_preparation_call_timestamp, remove_preparation_call_timestamp, @@ -1197,4 +1218,4 @@ module.exports ={ call_preparation_match_on_court, call_next_possible_match_for_preparation, call_match_in_preparation -}; \ No newline at end of file +}; diff --git a/bts/ticker_conn.js b/bts/ticker_conn.js index 3bfb274..e331d85 100644 --- a/bts/ticker_conn.js +++ b/bts/ticker_conn.js @@ -18,7 +18,7 @@ function craft_court(c) { function craft_match(m) { const res = utils.pluck(m, ['_id']); res.s = m.network_score; - res.c = m.setup.counting; + res.sf = m.setup.scoring_format; res.n = m.setup.event_name + ' ' + m.setup.match_name; m.setup.teams.forEach((t, tidx) => { res['p' + tidx] = t.players.map(p => p.name); diff --git a/static/cbts.html b/static/cbts.html index f55b036..c64482d 100644 --- a/static/cbts.html +++ b/static/cbts.html @@ -69,6 +69,7 @@ + @@ -83,4 +84,4 @@ - \ No newline at end of file + diff --git a/static/css/admin.css b/static/css/admin.css index 6eda8ae..c32e8e9 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -363,6 +363,99 @@ h2.edit { width: 100%; } +.scoring_formats_table th, +.scoring_formats_table td { + padding: 0.35em 0.5em; + vertical-align: middle; +} + +.scoring_formats_table th { + text-align: left; +} + +.scoring_formats_table tr.scoring_formats_row_group_odd > td, +.scoring_formats_table tr.scoring_formats_row_group_odd > th { + background-color: #bbb; +} + +.scoring_formats_table tr.scoring_formats_row_group_even > td, +.scoring_formats_table tr.scoring_formats_row_group_even > th { + background-color: #ccc; +} + +.scoring_formats_table .scoring_formats_subrow td { + border-top: 1px solid rgba(0, 0, 0, 0.08); +} + +.scoring_format_type_cell { + font-weight: 700; + white-space: nowrap; + text-align: left; +} + +.scoring_format_rule_cell { + white-space: nowrap; +} + +.scoring_format_name_cell { + text-align: left; +} + +.scoring_format_center_cell { + text-align: center; +} + +.scoring_format_right_cell { + text-align: right; +} + +.default_scoring_format_badge { + display: inline-block; + min-width: 1.6em; + padding: 0.1em 0.35em; + border-radius: 999px; + background: #ffd54f; + color: #4a3500; + font-weight: 700; + text-align: center; + white-space: nowrap; + box-shadow: inset 0 0 0 1px rgba(74, 53, 0, 0.18); +} + +.default_scoring_format_badge_inactive { + background: transparent; + color: #666; + box-shadow: none; + font-weight: 400; +} + +.scoring_format_edit_container { + display: grid; + gap: 1rem; + min-width: 36rem; +} + +.scoring_format_edit_row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.scoring_format_edit_row > input { + width: 10rem; +} + +.scoring_format_edit_section { + border: 1px solid #bbb; + padding: 0.75rem 1rem 1rem 1rem; +} + +.scoring_format_edit_section > legend { + padding: 0 0.35rem; + font-weight: 700; +} + .edit_display_setting_container{ text-align: left; } diff --git a/static/js/change.js b/static/js/change.js index 198d878..bf8a28a 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -58,9 +58,14 @@ function default_handler(rerender, special_funcs) { curt.btp_readonly = c.val.btp_readonly; curt.btp_ip = c.val.btp_ip; curt.ticker_enabled = c.val.ticker_enabled; - curt.ticker_url = c.val.ticker_url; - curt.ticker_password = c.val.ticker_password; - curt.logo_id = c.val.logo_id; + curt.ticker_url = c.val.ticker_url; + curt.ticker_password = c.val.ticker_password; + curt.logo_id = c.val.logo_id; + if (c.val.scoring_formats) { + curt.scoring_formats = c.val.scoring_formats; + ctournament.update_scoring_formats(); + ctournament.update_stages_scoring_formats(); + } uiu.qsEach('.ct_name', function(el) { if (el.tagName.toUpperCase() === 'INPUT') { @@ -359,6 +364,26 @@ function default_handler(rerender, special_funcs) { curt.btp_settings[key] = value; } break; + case 'update_btp_scoring_formats': + if(!curt.scoring_formats) { + curt.scoring_formats = {}; + } + const scoring_formats = c.val.scoring_formats; + for (const [key, value] of Object.entries(scoring_formats)) { + curt.scoring_formats[key] = value; + } + ctournament.update_scoring_formats(); + break; + case 'update_btp_events': + if(!curt.events) { + curt.events = {}; + } + const events = c.val.events; + for (const [key, value] of Object.entries(events)) { + curt.events[key] = value; + } + ctournament.update_stages_scoring_formats(); + break; case 'update_display_setting': const updated_setting = c.val.setting; const index = curt.displaysettings.findIndex(m => m.id === updated_setting.id); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 27d1c60..10ef470 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -215,12 +215,32 @@ var ci18n_de = { 'tournament:edit:upcoming_matches_settings': 'Spielübersichts Einstellungen', 'tournament:edit:upcoming_matches_animation_speed': 'Animationsgeschwindigkeit beim Scrollen der Spielübersichten', 'tournament:edit:upcoming_matches_animation_pause': 'Animationsunterbrechung am Anfang und Ende der Seite (sec)', - 'tournament:edit:upcoming_matches_max_count': 'Maximale Anzahl von Spielen in der Spielübersicht', - 'tournament:edit:call_preparation_matches_automatically_enabled': 'Spiele in Vorbereitung automatisch auf freien Felden aufrufen', - 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Nächst mögliches Spiel automatisch in Vorbereitung aufrufen', + 'tournament:edit:upcoming_matches_max_count': 'Maximale Anzahl von Spielen in der Spielübersicht', + 'tournament:edit:call_preparation_matches_automatically_enabled': 'Spiele in Vorbereitung automatisch auf freien Felden aufrufen', + 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Nächst mögliches Spiel automatisch in Vorbereitung aufrufen', + 'tournament:edit:scoring_formats': 'Punktsysteme', + 'tournament:edit:scoring_formats:dialog_title': 'Punktsystem bearbeiten', + 'tournament:edit:scoring_formats:dialog_hint': 'Aus BTP importierte Felder sind schreibgeschützt. Bearbeitet werden nur lokale Timing-Werte.', + 'tournament:edit:scoring_formats:name': 'Name', + 'tournament:edit:scoring_formats:num_sets': 'Sätze', + 'tournament:edit:scoring_formats:regular_sets': 'Normale Sätze', + 'tournament:edit:scoring_formats:last_set': 'Letzter Satz', + 'tournament:edit:scoring_formats:type': 'Typ', + 'tournament:edit:scoring_formats:type_regular': 'normal:', + 'tournament:edit:scoring_formats:type_last': 'letzter:', + 'tournament:edit:scoring_formats:default': 'Standard', + 'tournament:edit:scoring_formats:default_badge': '★', + 'tournament:edit:scoring_formats:edit': 'Bearbeiten', + 'tournament:edit:scoring_formats:end_max': 'Ende / Max', + 'tournament:edit:scoring_formats:end_points_label': 'Satzende bei', + 'tournament:edit:scoring_formats:max_points': 'Maximalpunkte', + 'tournament:edit:scoring_formats:break_in_set_enabled': 'Pause im Satz aktiv', + 'tournament:edit:scoring_formats:interval_at': 'Pause im Satz bei', + 'tournament:edit:scoring_formats:interval_duration': 'Pause im Satz', + 'tournament:edit:scoring_formats:break_before_set': 'Pause vor Satz', + + - - 'to_stats:header': 'Statistik der Technischen Offiziellen', 'to_stats:name': 'Name', 'to_stats:umpire': 'SR', @@ -276,4 +296,4 @@ var ci18n_de = { if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { module.exports = ci18n_de; } -/*/@DEV*/ \ No newline at end of file +/*/@DEV*/ diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 6e73bee..a652780 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -213,10 +213,30 @@ var ci18n_en = { 'tournament:edit:upcoming_matches_settings': 'Game Overview Settings', 'tournament:edit:upcoming_matches_animation_speed': 'Animationspeed for scroll on game overviews', 'tournament:edit:upcoming_matches_animation_pause': 'Animation interruption at the beginning and end of the page (sec)', - 'tournament:edit:upcoming_matches_max_count': 'Maximum number of games in the game overview', - 'tournament:edit:call_preparation_matches_automatically_enabled': 'Call games in preparation on free fields automatically', - 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Automatically call up next possible game in preparation', - 'to_stats:header': 'Technical Officials Statistics', + 'tournament:edit:upcoming_matches_max_count': 'Maximum number of games in the game overview', + 'tournament:edit:call_preparation_matches_automatically_enabled': 'Call games in preparation on free fields automatically', + 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Automatically call up next possible game in preparation', + 'tournament:edit:scoring_formats': 'Scoring formats', + 'tournament:edit:scoring_formats:dialog_title': 'Edit scoring format', + 'tournament:edit:scoring_formats:dialog_hint': 'Fields imported from BTP are read-only. Only local timing values can be edited.', + 'tournament:edit:scoring_formats:name': 'Name', + 'tournament:edit:scoring_formats:num_sets': 'Sets', + 'tournament:edit:scoring_formats:regular_sets': 'Regular sets', + 'tournament:edit:scoring_formats:last_set': 'Final set', + 'tournament:edit:scoring_formats:type': 'Type', + 'tournament:edit:scoring_formats:type_regular': 'regular:', + 'tournament:edit:scoring_formats:type_last': 'final:', + 'tournament:edit:scoring_formats:default': 'Default', + 'tournament:edit:scoring_formats:default_badge': '★', + 'tournament:edit:scoring_formats:edit': 'Edit', + 'tournament:edit:scoring_formats:end_max': 'Target / Max', + 'tournament:edit:scoring_formats:end_points_label': 'Set ends at', + 'tournament:edit:scoring_formats:max_points': 'Maximum points', + 'tournament:edit:scoring_formats:break_in_set_enabled': 'Break in set enabled', + 'tournament:edit:scoring_formats:interval_at': 'Break in set at', + 'tournament:edit:scoring_formats:interval_duration': 'Break in set', + 'tournament:edit:scoring_formats:break_before_set': 'Break before set', + 'to_stats:header': 'Technical Officials Statistics', 'to_stats:name': 'Name', 'to_stats:umpire': 'U', 'to_stats:service_judge': 'SJ', @@ -271,4 +291,4 @@ var ci18n_en = { if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { module.exports = ci18n_en; } -/*/@DEV*/ \ No newline at end of file +/*/@DEV*/ diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 34f4905..f2ac2fa 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -535,7 +535,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ } } - if(style == 'plain' && match.setup.counting == "3x21" && isMatchOver(match.network_score)) { + if(style == 'plain' && match_scoring.is_match_over(match.network_score, match.setup.scoring_format)) { create_match_button(timer_td, 'vlink match_confirm_button', 'Confirm_Finish', on_match_confirm_button_click, match._id); } } @@ -656,10 +656,10 @@ function update_match_score(m) { } } - if(m.setup.counting = "3x21" && isMatchOver(m.network_score)) { - create_match_button(timer_td, 'vlink match_confirm_button', 'Confirm_Finish', on_match_confirm_button_click, m._id); - } - }); + if (match_scoring.is_match_over(m.network_score, m.setup.scoring_format)) { + create_match_button(timer_td, 'vlink match_confirm_button', 'Confirm_Finish', on_match_confirm_button_click, m._id); + } + }); if( m.network_score && m.network_score.length > 0 && m.network_score[0].length > 1 && @@ -721,49 +721,6 @@ function on_match_confirm_button_click(e) { } } -function isMatchOver(sets) { - if(!sets){ - return false; - } - - let winsA = 0; - let winsB = 0; - - for (let [scoreA, scoreB] of sets) { - if (isSetOver(scoreA, scoreB)) { - if (scoreA > scoreB) { - winsA++; - } else { - winsB++; - } - - if (winsA === 2 || winsB === 2) { - return true; - } - } else { - // Satz ist noch nicht vorbei → Spiel auch nicht - return false; - } - } - - // Falls nicht abgebrochen wurde, prüfen ob überhaupt jemand 2 Sätze gewonnen hat - return winsA === 2 || winsB === 2; -} - -function isSetOver(scoreA, scoreB) { - const maxScore = 30; - const winningScore = 21; - - if (scoreA === maxScore || scoreB === maxScore) return true; - - if ((scoreA >= winningScore || scoreB >= winningScore) && Math.abs(scoreA - scoreB) >= 2) { - return true; - } - - return false; -} - - function render_players_el(parentNode, setup, team_id, match, show_player_status, style) { const team = setup.teams[team_id]; @@ -1176,7 +1133,7 @@ function _extract_match_timer_state(match) { let s = {}; s.settings = {}; s.settings.negative_timers = true; - s.lang = "de"; //TODO: Use the language of the BTS Settings + s.lang = (curt && curt.btp_settings && curt.btp_settings.language && curt.btp_settings.language !== 'auto') ? curt.btp_settings.language : "de"; var rs = calc.remote_state(s, match.setup, presses); return rs; @@ -2655,6 +2612,7 @@ if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var i18n = require('../bup/js/i18n'); var i18n_de = require('../bup/js/i18n_de'); var i18n_en = require('../bup/js/i18n_en'); + var match_scoring = require('./match_scoring'); var printing = require('../bup/js/printing'); var settings = require('../bup/js/settings'); var timer = require('../bup/js/timer'); diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 05bb8a6..197c501 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -2,6 +2,7 @@ var curt; // current tournament let current_view = null; +let scoring_formats_main = null; var ctournament = (function() { function _route_single(rex, func, handler) { @@ -1093,6 +1094,17 @@ var ctournament = (function() { } + + // scoring-formats-div############################################################################## + { + const scoring_div = uiu.el(form, "div", "settings"); + scoring_formats_main = scoring_div; + render_scoring_formats(scoring_div); + render_stages_scoring_formats(scoring_div) + } + + + // call-div################################################################################## { const call_div = uiu.el(form, 'div', 'settings'); @@ -1309,6 +1321,461 @@ var ctournament = (function() { }); } + function update_scoring_formats() { + if (!scoring_formats_main) { + if (typeof debug !== "undefined" && debug?.log) { + debug.log("update_scoring_formats: main container not initialized"); + } + return; + } + + // kompletten Bereich leeren + while (scoring_formats_main.firstChild) { + scoring_formats_main.removeChild(scoring_formats_main.firstChild); + } + + // vollständig neu rendern + render_scoring_formats(scoring_formats_main); + render_stages_scoring_formats(scoring_formats_main); + } + + function format_duration_ms(durationMs) { + const duration = Number(durationMs); + if (!Number.isFinite(duration) || duration < 0) { + return "—"; + } + if (duration === 0) { + return "0 s"; + } + return `${Math.round(duration / 1000)} s`; + } + + function format_set_rule_summary(setPoints) { + if (!setPoints) { + return "—"; + } + + const endPoints = setPoints.end_points ?? "—"; + const maxPoints = setPoints.max_points ?? "—"; + return `${endPoints} / ${maxPoints}`; + } + + function parse_nullable_number(value) { + if (value === undefined || value === null || value === "") { + return null; + } + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + + function duration_ms_to_seconds(value) { + const duration = parse_nullable_number(value); + if (duration === null) { + return ""; + } + return duration / 1000; + } + + function duration_seconds_to_ms(value) { + const duration = parse_nullable_number(value); + if (duration === null) { + return null; + } + return duration * 1000; + } + + function is_break_in_set_enabled(setPoints) { + if (!setPoints) { + return false; + } + if (typeof setPoints.interval_enabled === "boolean") { + return setPoints.interval_enabled; + } + return ( + setPoints.interval_at !== null && + setPoints.interval_at !== undefined && + setPoints.interval_duration_ms !== null && + setPoints.interval_duration_ms !== undefined + ); + } + + function clone_scoring_formats() { + const scoringFormats = curt?.scoring_formats || { formats: [], default_id: null }; + return structuredClone(scoringFormats); + } + + function _cancel_ui_edit_scoring_format() { + const dlg = document.querySelector('.scoring_format_edit_dialog'); + if (!dlg) { + return; + } + cbts_utils.esc_stack_pop(); + uiu.remove(dlg); + } + + function create_scoring_format_field(parent, label, name, value, type = "text", attrs = {}) { + const row = uiu.el(parent, 'label', 'scoring_format_edit_row'); + uiu.el(row, 'span', {}, label); + return uiu.el(row, 'input', Object.assign({ + type, + name, + value: value ?? '', + }, attrs)); + } + + function create_scoring_format_checkbox(parent, label, name, checked) { + const row = uiu.el(parent, 'label', 'scoring_format_edit_row'); + uiu.el(row, 'span', {}, label); + const attrs = { + type: 'checkbox', + name, + }; + if (checked) { + attrs.checked = 'checked'; + } + return uiu.el(row, 'input', attrs); + } + + function is_scoring_value_editable(setPoints, fieldName) { + return !!(setPoints && setPoints[`${fieldName}_editable`]); + } + + function render_scoring_format_edit_section(parent, prefix, title, setPoints) { + const fieldset = uiu.el(parent, 'fieldset', 'scoring_format_edit_section'); + uiu.el(fieldset, 'legend', {}, title); + const endPointAttrs = { min: 1, step: 1 }; + if (!is_scoring_value_editable(setPoints, "end_points")) { + endPointAttrs.disabled = 'disabled'; + } else { + endPointAttrs.required = 'required'; + } + const maxPointAttrs = { min: 1, step: 1 }; + if (!is_scoring_value_editable(setPoints, "max_points")) { + maxPointAttrs.disabled = 'disabled'; + } else { + maxPointAttrs.required = 'required'; + } + const endPointsInput = create_scoring_format_field(fieldset, ci18n("tournament:edit:scoring_formats:end_points_label"), `${prefix}_end_points`, setPoints?.end_points, "number", endPointAttrs); + const maxPointsInput = create_scoring_format_field(fieldset, ci18n("tournament:edit:scoring_formats:max_points"), `${prefix}_max_points`, setPoints?.max_points, "number", maxPointAttrs); + const hasBreakInSet = is_break_in_set_enabled(setPoints); + const breakEnabled = create_scoring_format_checkbox(fieldset, ci18n("tournament:edit:scoring_formats:break_in_set_enabled"), `${prefix}_break_in_set_enabled`, hasBreakInSet); + const intervalAtInput = create_scoring_format_field(fieldset, ci18n("tournament:edit:scoring_formats:interval_at"), `${prefix}_interval_at`, setPoints?.interval_at, "number", { min: 0, step: 1 }); + const intervalDurationInput = create_scoring_format_field(fieldset, `${ci18n("tournament:edit:scoring_formats:interval_duration")} (s)`, `${prefix}_interval_duration_s`, duration_ms_to_seconds(setPoints?.interval_duration_ms), "number", { min: 0, step: 1 }); + create_scoring_format_field(fieldset, `${ci18n("tournament:edit:scoring_formats:break_before_set")} (s)`, `${prefix}_break_before_set_duration_s`, duration_ms_to_seconds(setPoints?.break_before_set_duration_ms), "number", { min: 0, step: 1 }); + + function normalizeScoreInputs() { + if (!endPointsInput.disabled) { + let endPoints = Number(endPointsInput.value); + if (!Number.isFinite(endPoints) || endPoints < 1) { + endPoints = Math.max(1, Number(setPoints?.end_points) || 1); + } + endPointsInput.value = String(endPoints); + if (!maxPointsInput.disabled) { + maxPointsInput.min = String(endPoints); + let maxPoints = Number(maxPointsInput.value); + if (!Number.isFinite(maxPoints) || maxPoints < endPoints) { + maxPoints = endPoints; + } + maxPointsInput.value = String(maxPoints); + } + } else if (!maxPointsInput.disabled) { + let maxPoints = Number(maxPointsInput.value); + const minValue = Math.max(1, Number(setPoints?.end_points) || 1); + maxPointsInput.min = String(minValue); + if (!Number.isFinite(maxPoints) || maxPoints < minValue) { + maxPointsInput.value = String(minValue); + } + } + } + + if (!endPointsInput.disabled) { + endPointsInput.addEventListener('input', normalizeScoreInputs); + endPointsInput.addEventListener('blur', normalizeScoreInputs); + } + if (!maxPointsInput.disabled) { + maxPointsInput.addEventListener('input', normalizeScoreInputs); + maxPointsInput.addEventListener('blur', normalizeScoreInputs); + } + normalizeScoreInputs(); + + function updateBreakInSetUi() { + const enabled = breakEnabled.checked; + intervalAtInput.disabled = !enabled; + intervalDurationInput.disabled = !enabled; + } + + breakEnabled.addEventListener('change', updateBreakInSetUi); + updateBreakInSetUi(); + } + + function scoring_format_from_form_data(baseFormat, data) { + const scoringFormat = structuredClone(baseFormat); + + function update_set_points(target, prefix) { + if (is_scoring_value_editable(target, "end_points")) { + target.end_points = Math.max(1, Number(data[`${prefix}_end_points`])); + } + if (is_scoring_value_editable(target, "max_points")) { + const minPoints = Math.max(1, Number(target.end_points)); + target.max_points = Math.max(minPoints, Number(data[`${prefix}_max_points`])); + } + const hasBreakInSet = !!data[`${prefix}_break_in_set_enabled`]; + target.interval_enabled = hasBreakInSet; + if (hasBreakInSet) { + target.interval_at = parse_nullable_number(data[`${prefix}_interval_at`]); + target.interval_duration_ms = duration_seconds_to_ms(data[`${prefix}_interval_duration_s`]); + } + target.break_before_set_duration_ms = duration_seconds_to_ms(data[`${prefix}_break_before_set_duration_s`]); + } + + update_set_points(scoringFormat.set_points, 'set_points'); + update_set_points(scoringFormat.last_set_points, 'last_set_points'); + return scoringFormat; + } + + function save_scoring_format(scoringFormatId, scoringFormat, callback) { + const scoringFormats = clone_scoring_formats(); + const formats = Array.isArray(scoringFormats.formats) ? scoringFormats.formats : []; + const index = formats.findIndex(f => Number(f.id) === Number(scoringFormatId)); + if (index === -1) { + return callback(new Error(`Unknown scoring format ${scoringFormatId}`)); + } + formats[index] = scoringFormat; + scoringFormats.formats = formats; + + send({ + type: 'tournament_edit_props', + key: curt.key, + props: { + scoring_formats: scoringFormats, + }, + }, callback); + } + + function ui_edit_scoring_format(scoringFormatId) { + const scoringFormats = curt?.scoring_formats; + const baseFormat = structuredClone(utils.find((scoringFormats && scoringFormats.formats) || [], f => Number(f.id) === Number(scoringFormatId))); + if (!baseFormat) { + return; + } + + cbts_utils.esc_stack_push(_cancel_ui_edit_scoring_format); + + const body = uiu.qs('body'); + const dialogBg = uiu.el(body, 'div', 'dialog_bg scoring_format_edit_dialog', { + 'data-scoring-format-id': scoringFormatId, + }); + dialogBg.addEventListener('click', (e) => { + if (e.target === dialogBg) { + _cancel_ui_edit_scoring_format(); + } + }); + + const dialog = uiu.el(dialogBg, 'div', 'dialog'); + uiu.el(dialog, 'h3', {}, ci18n('tournament:edit:scoring_formats:dialog_title')); + + const form = uiu.el(dialog, 'form'); + const container = uiu.el(form, 'div', 'scoring_format_edit_container'); + uiu.el(container, 'div', 'hint', ci18n('tournament:edit:scoring_formats:dialog_hint')); + create_scoring_format_field(container, ci18n("tournament:edit:scoring_formats:name"), 'name', baseFormat.name, 'text', { disabled: 'disabled' }); + create_scoring_format_field(container, ci18n("tournament:edit:scoring_formats:num_sets"), 'numSets', baseFormat.numSets, 'number', { min: 1, step: 1, disabled: 'disabled' }); + render_scoring_format_edit_section(container, 'set_points', ci18n("tournament:edit:scoring_formats:regular_sets"), baseFormat.set_points); + render_scoring_format_edit_section(container, 'last_set_points', ci18n("tournament:edit:scoring_formats:last_set"), baseFormat.last_set_points); + + const buttons = uiu.el(form, 'div', { style: 'margin-top: 2em;' }); + uiu.el(buttons, 'button', { + 'class': 'match_save_button', + role: 'submit', + }, ci18n('Change')); + + form_utils.onsubmit(form, function(data) { + const scoringFormat = scoring_format_from_form_data(baseFormat, data); + save_scoring_format(scoringFormatId, scoringFormat, (err) => { + if (err) { + return cerror.net(err); + } + _cancel_ui_edit_scoring_format(); + }); + }); + + const cancelBtn = uiu.el(buttons, 'span', 'match_cancel_link vlink', ci18n('Cancel')); + cancelBtn.addEventListener('click', _cancel_ui_edit_scoring_format); + } + + function render_scoring_formats(main) { + uiu.el(main, "h2", "edit", ci18n("tournament:edit:scoring_formats")); + + const sf = curt?.scoring_formats || { formats: [], default_id: null }; + const formats = Array.isArray(sf.formats) ? sf.formats : []; + const defaultId = sf.default_id; + + const table = uiu.el(main, "table", "scoring_formats_table"); + const tbody = uiu.el(table, "tbody"); + + { + const tr = uiu.el(tbody, "tr"); + uiu.el(tr, "th", { class: "scoring_format_name_cell" }, ci18n("tournament:edit:scoring_formats:name")); + uiu.el(tr, "th", { class: "scoring_format_center_cell" }, ci18n("tournament:edit:scoring_formats:num_sets")); + uiu.el(tr, "th", { class: "scoring_format_type_cell" }, ci18n("tournament:edit:scoring_formats:type")); + uiu.el(tr, "th", { class: "scoring_format_center_cell" }, ci18n("tournament:edit:scoring_formats:end_max")); + uiu.el(tr, "th", { class: "scoring_format_center_cell" }, ci18n("tournament:edit:scoring_formats:interval_at")); + uiu.el(tr, "th", { class: "scoring_format_right_cell" }, ci18n("tournament:edit:scoring_formats:interval_duration")); + uiu.el(tr, "th", { class: "scoring_format_right_cell" }, ci18n("tournament:edit:scoring_formats:break_before_set")); + uiu.el(tr, "th", { class: "scoring_format_center_cell" }, ci18n("tournament:edit:scoring_formats:default")); + uiu.el(tr, "th", { class: "scoring_format_center_cell" }, ci18n("tournament:edit:scoring_formats:edit")); + } + + for (const [formatIndex, f] of formats.entries()) { + const rowClass = (formatIndex % 2 === 0) ? "scoring_formats_row_group_even" : "scoring_formats_row_group_odd"; + const regularTr = uiu.el(tbody, "tr", rowClass); + const lastTr = uiu.el(tbody, "tr", `scoring_formats_subrow ${rowClass}`); + const regularSetPoints = f?.set_points; + const lastSetPoints = f?.last_set_points; + const isDefault = Number(f.id) === Number(defaultId); + const canEdit = true; + + uiu.el(regularTr, "td", { rowspan: 2, class: "scoring_format_name_cell" }, f.name || ""); + uiu.el(regularTr, "td", { rowspan: 2, class: "scoring_format_center_cell" }, String(f.numSets ?? "")); + uiu.el(regularTr, "td", { class: "scoring_format_type_cell scoring_format_rule_cell" }, ci18n("tournament:edit:scoring_formats:type_regular")); + uiu.el(regularTr, "td", { class: "scoring_format_rule_cell scoring_format_center_cell" }, format_set_rule_summary(regularSetPoints)); + uiu.el(regularTr, "td", { class: "scoring_format_rule_cell scoring_format_center_cell" }, is_break_in_set_enabled(regularSetPoints) ? String(regularSetPoints.interval_at) : "—"); + uiu.el(regularTr, "td", { class: "scoring_format_rule_cell scoring_format_right_cell" }, is_break_in_set_enabled(regularSetPoints) ? format_duration_ms(regularSetPoints && regularSetPoints.interval_duration_ms) : "—"); + uiu.el(regularTr, "td", { class: "scoring_format_rule_cell scoring_format_right_cell" }, format_duration_ms(regularSetPoints && regularSetPoints.break_before_set_duration_ms)); + + const defTd = uiu.el(regularTr, "td", { rowspan: 2, class: "scoring_format_center_cell" }); + if (isDefault) { + uiu.el(defTd, "span", { + class: "default_scoring_format_badge", + title: ci18n("tournament:edit:scoring_formats:default"), + }, ci18n("tournament:edit:scoring_formats:default_badge")); + } else { + uiu.el(defTd, "span", { class: "default_scoring_format_badge default_scoring_format_badge_inactive" }, "—"); + } + + const actionsTd = uiu.el(regularTr, "td", { rowspan: 2, class: "scoring_format_center_cell" }); + const editBtn = uiu.el( + actionsTd, + "button", + { "data-scoring-format-id": f.id }, + ci18n("tournament:edit:scoring_formats:edit") + ); + + editBtn.addEventListener("click", (e) => { + const id = e.target.getAttribute("data-scoring-format-id"); + ui_edit_scoring_format(id); + }); + + uiu.el(lastTr, "td", { class: "scoring_format_type_cell scoring_format_rule_cell" }, ci18n("tournament:edit:scoring_formats:type_last")); + uiu.el(lastTr, "td", { class: "scoring_format_rule_cell scoring_format_center_cell" }, format_set_rule_summary(lastSetPoints)); + uiu.el(lastTr, "td", { class: "scoring_format_rule_cell scoring_format_center_cell" }, is_break_in_set_enabled(lastSetPoints) ? String(lastSetPoints.interval_at) : "—"); + uiu.el(lastTr, "td", { class: "scoring_format_rule_cell scoring_format_right_cell" }, is_break_in_set_enabled(lastSetPoints) ? format_duration_ms(lastSetPoints && lastSetPoints.interval_duration_ms) : "—"); + uiu.el(lastTr, "td", { class: "scoring_format_rule_cell scoring_format_right_cell" }, format_duration_ms(lastSetPoints && lastSetPoints.break_before_set_duration_ms)); + } + } + + function update_stages_scoring_formats() { + if (!scoring_formats_main) { + if (typeof debug !== "undefined" && debug?.log) { + debug.log("update_scoring_formats: main container not initialized"); + } + return; + } + + // kompletten Bereich leeren + while (scoring_formats_main.firstChild) { + scoring_formats_main.removeChild(scoring_formats_main.firstChild); + } + + // vollständig neu rendern + render_scoring_formats(scoring_formats_main); + render_stages_scoring_formats(scoring_formats_main); + } + + function render_stages_scoring_formats(main) { + const sf = curt?.scoring_formats || { formats: [], default_id: null }; + const defaultId = sf.default_id; + + // Build lookup: scoring_format_id -> scoring_format_name + const formatNameById = new Map(); + for (const f of sf.formats || []) { + formatNameById.set(Number(f.id), f.name || String(f.id)); + } + + const eventsPayload = curt?.events?.events || []; + const deviations = []; + + for (const ev of eventsPayload) { + const eventName = ev?.name || ""; + const stages = Array.isArray(ev?.stages) ? ev.stages : []; + + for (const st of stages) { + // Missing/null scoring_format => default + const stageSfId = + st && st.scoring_format !== undefined && st.scoring_format !== null + ? Number(st.scoring_format) + : null; + + if ( + stageSfId !== null && + defaultId !== null && + defaultId !== undefined && + stageSfId !== Number(defaultId) + ) { + deviations.push({ + event_name: eventName, + stage_name: st?.name || "", + scoring_format_id: stageSfId, + scoring_format_name: formatNameById.get(stageSfId) || String(stageSfId), + }); + } + } + } + + deviations.sort((a, b) => { + const e = (a.event_name || "").localeCompare(b.event_name || ""); + if (e) return e; + const s = (a.stage_name || "").localeCompare(b.stage_name || ""); + if (s) return s; + return (a.scoring_format_id || 0) - (b.scoring_format_id || 0); + }); + + uiu.el(main, "h3", "edit", "Abweichungen vom Default"); + + if (defaultId === null || defaultId === undefined) { + uiu.el( + main, + "div", + "hint", + "Kein Default-Scoring-Format gefunden (scoring_formats.default_id ist leer)." + ); + return; + } + + if (deviations.length === 0) { + uiu.el(main, "div", "hint", "Keine Stages weichen vom Default-Scoring-Format ab."); + return; + } + + const devTable = uiu.el(main, "table", "scoring_format_deviations_table"); + const devBody = uiu.el(devTable, "tbody"); + + { + const tr = uiu.el(devBody, "tr"); + uiu.el(tr, "th", {}, "Event"); + uiu.el(tr, "th", {}, "Stage"); + uiu.el(tr, "th", {}, "Verwendete Zählweise"); + } + + for (const d of deviations) { + const tr = uiu.el(devBody, "tr"); + uiu.el(tr, "td", {}, d.event_name); + uiu.el(tr, "td", {}, d.stage_name); + uiu.el(tr, "td", {}, `${d.scoring_format_name} (#${d.scoring_format_id})`); + } + } + + + function render_normalisation_values(main) { uiu.el(main, 'h2','edit', ci18n('tournament:edit:normalizations')); @@ -3344,6 +3811,8 @@ function update_officials() { update_location_logo, update_court, update_emergency_btn, + update_scoring_formats, + update_stages_scoring_formats, btp_status_changed, ticker_status_changed, bts_status_changed, diff --git a/static/js/match_scoring.js b/static/js/match_scoring.js new file mode 100644 index 0000000..198e939 --- /dev/null +++ b/static/js/match_scoring.js @@ -0,0 +1,92 @@ +'use strict'; + +var match_scoring = (function() { + function fallback_scoring_format() { + return { + numSets: 3, + set_points: { + end_points: 21, + max_points: 30, + }, + last_set_points: { + end_points: 21, + max_points: 30, + }, + }; + } + + function normalize_set_points(setPoints, fallbackSetPoints) { + const normalized = setPoints || {}; + const fallback = fallbackSetPoints || {}; + + return { + end_points: Number.isFinite(normalized.end_points) ? normalized.end_points : fallback.end_points, + max_points: Number.isFinite(normalized.max_points) ? normalized.max_points : fallback.max_points, + }; + } + + function is_set_over(scoreA, scoreB, setPoints) { + const maxScore = setPoints?.max_points; + const winningScore = setPoints?.end_points; + + if (Number.isFinite(maxScore) && (scoreA === maxScore || scoreB === maxScore)) return true; + + if (Number.isFinite(winningScore) && + (scoreA >= winningScore || scoreB >= winningScore) && + Math.abs(scoreA - scoreB) >= 2) { + return true; + } + + return false; + } + + function is_match_over(sets, scoringFormat) { + if (!sets) { + return false; + } + + const format = scoringFormat || fallback_scoring_format(); + const totalSets = Number.isFinite(format.numSets) && format.numSets > 0 ? format.numSets : 3; + const requiredWins = Math.floor(totalSets / 2) + 1; + const fallbackSetPoints = fallback_scoring_format().set_points; + + let winsA = 0; + let winsB = 0; + + for (let idx = 0; idx < sets.length; idx++) { + const [scoreA, scoreB] = sets[idx]; + const isLastPossibleSet = idx === totalSets - 1; + const setPoints = normalize_set_points( + isLastPossibleSet ? format.last_set_points : format.set_points, + fallbackSetPoints + ); + + if (is_set_over(scoreA, scoreB, setPoints)) { + if (scoreA > scoreB) { + winsA++; + } else { + winsB++; + } + + if (winsA >= requiredWins || winsB >= requiredWins) { + return true; + } + } else { + return false; + } + } + + return winsA >= requiredWins || winsB >= requiredWins; + } + + return { + is_match_over, + is_set_over, + }; +})(); + +/*@DEV*/ +if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { + module.exports = match_scoring; +} +/*/@DEV*/ diff --git a/test/test_btp_sync.js b/test/test_btp_sync.js new file mode 100644 index 0000000..6e18f3c --- /dev/null +++ b/test/test_btp_sync.js @@ -0,0 +1,274 @@ +'use strict'; + +const assert = require('assert'); + +const {_describe, _it} = require('./tutils.js'); + +const btp_sync = require('../bts/btp_sync'); + +_describe('btp_sync', () => { + _it('normalizes standard scoring formats from BTP fields', () => { + const normalized = btp_sync._normalize_scoring_format({ + ID: ['10'], + Name: ['Best of 3 to 21'], + NumSets: ['3'], + SetType: ['0'], + LastSetType: ['0'], + Score: ['21'], + IsDefault: [true], + }); + + assert.deepStrictEqual(normalized, { + id: 10, + name: 'Best of 3 to 21', + numSets: 3, + score: 21, + isDefault: true, + setType: 0, + lastSetType: 0, + set_points: { + end_points: 21, + max_points: 30, + end_points_editable: false, + max_points_editable: false, + interval_at: 11, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }, + last_set_points: { + end_points: 21, + max_points: 30, + end_points_editable: false, + max_points_editable: false, + interval_at: 11, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }, + }); + }); + + _it('normalizes editable scoring formats using the score fallback', () => { + const normalized = btp_sync._normalize_scoring_format({ + ID: ['11'], + Name: ['Custom 1x17'], + NumSets: ['1'], + SetType: ['999'], + LastSetType: ['999'], + Score: ['17'], + IsDefault: [false], + }); + + assert.deepStrictEqual(normalized.set_points, { + end_points: 17, + max_points: 17, + end_points_editable: false, + max_points_editable: true, + defaults_from_score: true, + interval_at: 9, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + assert.deepStrictEqual(normalized.last_set_points, { + end_points: 17, + max_points: 17, + end_points_editable: false, + max_points_editable: true, + defaults_from_score: true, + interval_at: 9, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + }); + + _it('normalizes fully editable set rules for set type 1000', () => { + const normalized = btp_sync._normalize_scoring_format({ + ID: ['13'], + Name: ['Custom free'], + NumSets: ['3'], + SetType: ['1000'], + LastSetType: ['1000'], + Score: ['0'], + IsDefault: [false], + }); + + assert.deepStrictEqual(normalized.set_points, { + end_points: 1, + max_points: 1, + end_points_editable: true, + max_points_editable: true, + interval_at: 1, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + assert.deepStrictEqual(normalized.last_set_points, { + end_points: 1, + max_points: 1, + end_points_editable: true, + max_points_editable: true, + interval_at: 1, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + }); + + _it('defaults interval point to rounded-up half of end points', () => { + const normalized = btp_sync._normalize_scoring_format({ + ID: ['14'], + Name: ['Custom 1x15'], + NumSets: ['1'], + SetType: ['999'], + LastSetType: ['999'], + Score: ['15'], + IsDefault: [false], + }); + + assert.strictEqual(normalized.set_points.interval_at, 8); + assert.strictEqual(normalized.last_set_points.interval_at, 8); + }); + + _it('sanitizes end_points to be at least 1 and max_points to be at least end_points', () => { + const sanitized = btp_sync._sanitize_scoring_format({ + id: 99, + name: 'Broken', + numSets: 1, + score: 0, + isDefault: false, + setType: 1000, + lastSetType: 1000, + set_points: { + end_points: 0, + max_points: 0, + }, + last_set_points: { + end_points: -5, + max_points: 2, + }, + }); + + assert.strictEqual(sanitized.set_points.end_points, 1); + assert.strictEqual(sanitized.set_points.max_points, 1); + assert.strictEqual(sanitized.last_set_points.end_points, 1); + assert.strictEqual(sanitized.last_set_points.max_points, 2); + }); + + _it('normalizes different rules for the last set', () => { + const normalized = btp_sync._normalize_scoring_format({ + ID: ['12'], + Name: ['2x21+11'], + NumSets: ['3'], + SetType: ['0'], + LastSetType: ['304'], + Score: ['21'], + IsDefault: [false], + }); + + assert.deepStrictEqual(normalized.set_points, { + end_points: 21, + max_points: 30, + end_points_editable: false, + max_points_editable: false, + interval_at: 11, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + assert.deepStrictEqual(normalized.last_set_points, { + end_points: 11, + max_points: 15, + end_points_editable: false, + max_points_editable: false, + interval_at: 6, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }); + }); + + _it('provides a complete 3x21 fallback scoring format', () => { + const normalized = btp_sync._fallback_scoring_format(); + + assert.deepStrictEqual(normalized, { + id: null, + name: '3x21', + numSets: 3, + score: 21, + isDefault: false, + setType: 0, + lastSetType: 0, + set_points: { + end_points: 21, + max_points: 30, + end_points_editable: false, + max_points_editable: false, + interval_at: 11, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }, + last_set_points: { + end_points: 21, + max_points: 30, + end_points_editable: false, + max_points_editable: false, + interval_at: 11, + interval_duration_ms: 60000, + break_before_set_duration_ms: 120000, + }, + }); + }); + + _it('keeps locally edited timing values when BTP scoring formats are normalized again', () => { + const existing = { + id: 11, + name: 'Custom 1x17', + numSets: 1, + score: 17, + isDefault: false, + setType: 999, + lastSetType: 999, + set_points: { + end_points: 17, + max_points: 19, + end_points_editable: false, + max_points_editable: true, + defaults_from_score: true, + interval_at: 9, + interval_duration_ms: 45000, + break_before_set_duration_ms: 30000, + interval_enabled: false, + }, + last_set_points: { + end_points: 17, + max_points: 21, + end_points_editable: false, + max_points_editable: true, + defaults_from_score: true, + interval_at: 8, + interval_duration_ms: 40000, + break_before_set_duration_ms: 35000, + interval_enabled: true, + }, + }; + + const normalized = btp_sync._normalize_scoring_format({ + ID: ['11'], + Name: ['Custom 1x17'], + NumSets: ['1'], + SetType: ['999'], + LastSetType: ['999'], + Score: ['17'], + IsDefault: [false], + }); + + const merged = btp_sync._merge_local_scoring_format(existing, normalized); + + assert.strictEqual(merged.set_points.end_points, 17); + assert.strictEqual(merged.set_points.max_points, 19); + assert.strictEqual(merged.set_points.interval_at, 9); + assert.strictEqual(merged.set_points.interval_duration_ms, 45000); + assert.strictEqual(merged.set_points.break_before_set_duration_ms, 30000); + assert.strictEqual(merged.set_points.interval_enabled, false); + assert.strictEqual(merged.last_set_points.max_points, 21); + assert.strictEqual(merged.last_set_points.interval_at, 8); + assert.strictEqual(merged.last_set_points.interval_duration_ms, 40000); + assert.strictEqual(merged.last_set_points.break_before_set_duration_ms, 35000); + assert.strictEqual(merged.last_set_points.interval_enabled, true); + }); + }); diff --git a/test/test_cmatch.js b/test/test_cmatch.js new file mode 100644 index 0000000..c38f690 --- /dev/null +++ b/test/test_cmatch.js @@ -0,0 +1,36 @@ +'use strict'; + +const assert = require('assert'); + +const {_describe, _it} = require('./tutils.js'); + +const match_scoring = require('../static/js/match_scoring'); + +_describe('cmatch', () => { + _it('detects match end for default 3x21 fallback', () => { + assert.strictEqual(match_scoring.is_match_over([[21, 10], [21, 18]], null), true); + assert.strictEqual(match_scoring.is_match_over([[21, 10], [18, 21]], null), false); + }); + + _it('detects match end for 1x21 scoring format', () => { + const scoringFormat = { + numSets: 1, + set_points: { end_points: 21, max_points: 30 }, + last_set_points: { end_points: 21, max_points: 30 }, + }; + + assert.strictEqual(match_scoring.is_match_over([[21, 19]], scoringFormat), true); + assert.strictEqual(match_scoring.is_match_over([[20, 19]], scoringFormat), false); + }); + + _it('uses last-set limits for deciding the final set', () => { + const scoringFormat = { + numSets: 3, + set_points: { end_points: 21, max_points: 30 }, + last_set_points: { end_points: 11, max_points: 15 }, + }; + + assert.strictEqual(match_scoring.is_match_over([[21, 18], [19, 21], [11, 9]], scoringFormat), true); + assert.strictEqual(match_scoring.is_match_over([[21, 18], [19, 21], [10, 9]], scoringFormat), false); + }); +}); diff --git a/ticker/tdata.js b/ticker/tdata.js index a1c5739..da4f244 100644 --- a/ticker/tdata.js +++ b/ticker/tdata.js @@ -4,12 +4,21 @@ const async = require('async'); const utils = require('../bts/utils'); +function max_game_count(match) { + const scoringFormat = match && match.setup && match.setup.scoring_format; + const numSets = Number(scoringFormat && scoringFormat.numSets); + if (Number.isFinite(numSets) && numSets > 0) { + return numSets; + } + return 3; +} + function prepare_mustache(m) { m.p0str = m.p0.join('\n'); m.p1str = m.p1.join('\n'); - const max_game_count = 3; // TODO look it up from counting - m.gamesplus2 = max_game_count + 2; - const game_ids = utils.range(max_game_count); + const maxGames = max_game_count(m); + m.gamesplus2 = maxGames + 2; + const game_ids = utils.range(maxGames); for (const team_id of [0, 1]) { m['team' + team_id + 'scores'] = game_ids.map((game_idx) => { if (!m.s) return ''; From 8e344c4ef710281557c0df46b6691092da0d3a06 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 2 Apr 2026 02:59:39 +0200 Subject: [PATCH 212/224] Fix wrong variable name while rendering! --- static/js/cmatch.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/js/cmatch.js b/static/js/cmatch.js index f2ac2fa..440b89d 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -652,14 +652,14 @@ function update_match_score(m) { var preparation_timer_state = _extract_preparation_timer_state(m); var preparation_timer = create_timer(preparation_timer_state, timer_td, "#cccccc", "#ff0000"); if (preparation_timer) { - active_timers.matches[match._id] = preparation_timer; + active_timers.matches[m._id] = preparation_timer; } } - if (match_scoring.is_match_over(m.network_score, m.setup.scoring_format)) { - create_match_button(timer_td, 'vlink match_confirm_button', 'Confirm_Finish', on_match_confirm_button_click, m._id); - } - }); + if (match_scoring.is_match_over(m.network_score, m.setup.scoring_format)) { + create_match_button(timer_td, 'vlink match_confirm_button', 'Confirm_Finish', on_match_confirm_button_click, m._id); + } + }); if( m.network_score && m.network_score.length > 0 && m.network_score[0].length > 1 && From bf0d53392b88b6c70b58497fbd59fb3b8b3d9347 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Thu, 2 Apr 2026 04:42:40 +0200 Subject: [PATCH 213/224] Converted the tournament settings page from explicit save actions to live per-field persistence with immediate change propagation to other clients. Added a shared settings status area that is now used by both the new live field updates and the older direct-save actions, and improved client-side rerendering/dependency handling so related sections update immediately without requiring a manual reload. --- bts/admin.js | 123 ++++++++ static/css/admin.css | 18 ++ static/js/change.js | 179 +++++++----- static/js/ci18n_de.js | 5 + static/js/ci18n_en.js | 5 + static/js/ctournament.js | 603 ++++++++++++++++++++++++++------------- 6 files changed, 662 insertions(+), 271 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 968dbcd..cbad72b 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -114,6 +114,127 @@ function handle_tournament_edit_props(app, ws, msg) { }); } +function handle_tournament_edit_prop(app, ws, msg) { + if (! msg.key) { + return ws.respond(msg, {message: 'Missing key'}); + } + if (typeof msg.field === 'undefined') { + return ws.respond(msg, {message: 'Missing field'}); + } + + const allowed_fields = new Set([ + 'name', 'tguid', + 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', + 'btp_ip', 'btp_password', 'btp_autofetch_timeout_intervall', 'btp_timezone', + 'is_team', 'is_nation_competition', + 'warmup', 'warmup_ready', 'warmup_start', + 'upcoming_matches_animation_speed', 'upcoming_matches_max_count', 'upcoming_matches_animation_pause', + 'ticker_enabled', 'ticker_url', 'ticker_password', + 'language', 'dm_style', 'displaysettings_general', + 'tabletoperator_enabled', 'tabletoperator_break_seconds', + 'announcement_speed', 'announcement_pause_time_ms', + 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_with_state_enabled', + 'tabletoperator_with_state_from_match_enabled', + 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', + 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', + 'annoncement_include_event', 'annoncement_include_round', 'annoncement_include_matchnumber', + 'preparation_meetingpoint_enabled', 'preparation_tabletoperator_setup_enabled', + 'call_preparation_matches_automatically_enabled', 'call_next_possible_scheduled_match_in_preparation', + 'logo_background_color', 'logo_foreground_color', + ]); + + const field = msg.field; + if (!allowed_fields.has(field)) { + return ws.respond(msg, {message: 'Unsupported field ' + field}); + } + + const key = msg.key; + let value = msg.value; + if (field === 'btp_timezone') { + value = value === 'system' ? undefined : value; + } + const props = {}; + props[field] = value; + + app.db.tournaments.findOne({ key }, async (err, tournament) => { + if (err || !tournament) { + ws.respond(msg, err); + return; + } + app.db.tournaments.update({ key }, { $set: props }, { returnUpdatedDocs: true }, function (err, num, t) { + if (err) { + ws.respond(msg, err); + return; + } + if (/^btp_/.test(field)) { + btp_manager.reconfigure(app, t); + } + if (/^ticker_/.test(field)) { + ticker_manager.reconfigure(app, t); + } + notify_change(app, key, 'prop_changed', { field, value: t[field] }); + + if (!tournament.displaysettings_general || (field === 'displaysettings_general' && tournament.displaysettings_general != t.displaysettings_general)){ + const bupws = require('./bupws'); + bupws.change_default_display_mode(app, t, tournament.displaysettings_general, t.displaysettings_general); + } + + ws.respond(msg, err); + }); + }); +} + +function handle_tournament_edit_scoring_format(app, ws, msg) { + if (! msg.key) { + return ws.respond(msg, {message: 'Missing key'}); + } + if (! msg.scoring_format) { + return ws.respond(msg, {message: 'Missing scoring_format'}); + } + + const key = msg.key; + const scoring_format = msg.scoring_format; + app.db.tournaments.findOne({ key }, async (err, tournament) => { + if (err || !tournament) { + ws.respond(msg, err); + return; + } + + const btp_sync = require('./btp_sync'); + const scoring_formats = tournament.scoring_formats || { formats: [], default_id: null }; + const formats = Array.isArray(scoring_formats.formats) ? scoring_formats.formats.slice() : []; + const index = formats.findIndex(f => Number(f.id) === Number(scoring_format.id)); + if (index === -1) { + return ws.respond(msg, {message: 'Unknown scoring format ' + scoring_format.id}); + } + + formats[index] = btp_sync._sanitize_scoring_format(scoring_format); + const updated_scoring_formats = { + ...scoring_formats, + formats, + }; + + app.db.tournaments.update( + { key }, + { $set: { scoring_formats: updated_scoring_formats } }, + { returnUpdatedDocs: true }, + function (err) { + if (err) { + ws.respond(msg, err); + return; + } + notify_change(app, key, 'scoring_format_changed', { + scoring_format: formats[index], + }); + notify_change(app, key, 'props', { + scoring_formats: updated_scoring_formats, + }); + ws.respond(msg, err); + } + ); + }); +} + function handle_tournament_edit_logo(app, ws, msg) { if (! msg.key) { @@ -1732,6 +1853,8 @@ module.exports = { handle_second_preparation_call_team_two, handle_tournament_get, handle_tournament_list, + handle_tournament_edit_prop, + handle_tournament_edit_scoring_format, handle_tournament_edit_props, handle_tournament_edit_logo, handle_display_delete, diff --git a/static/css/admin.css b/static/css/admin.css index c32e8e9..84ccca2 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -189,6 +189,24 @@ a:hover { margin: 10px; } +.live_settings_status { + margin: 0.5em 0 1em 0; + font-size: 0.9em; + font-weight: bold; +} + +.live_settings_status_saving { + color: #8a5a00; +} + +.live_settings_status_saved { + color: #2c6e2f; +} + +.live_settings_status_error { + color: #9b1c1c; +} + .dialog_bg { position: fixed; left: 0; diff --git a/static/js/change.js b/static/js/change.js index bf8a28a..da4cee6 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -6,6 +6,54 @@ function default_handler(rerender, special_funcs) { }; } + function _apply_tournament_field_change(field, value) { + curt[field] = value; + + if (field === 'name') { + uiu.qsEach('.ct_name', function(el) { + if (el.tagName.toUpperCase() === 'INPUT') { + el.value = value || ''; + } else { + uiu.text(el, value || ''); + } + }); + } + + uiu.qsEach('input[name="' + field + '"]', function(el) { + if (el.type === 'checkbox') { + el.checked = !!value; + } else { + el.value = value ?? ''; + } + }); + uiu.qsEach('select[name="' + field + '"]', function(el) { + el.value = value ?? (field === 'btp_timezone' ? 'system' : ''); + }); + } + + function _after_tournament_field_change(field, value, change_obj) { + if (current_view === 'edit') { + ctournament.update_edit_dependencies(); + } + if (field === 'displaysettings_general') { + ctournament.update_general_displaysettings(change_obj); + } + if (field === 'language') { + if (value && value !== 'auto') { + ci18n.switch_language(value); + } + ctournament.refresh_current_view(); + } + if (field === 'tabletoperator_enabled') { + ctournament.refresh_current_view(); + } + if (['btp_enabled', 'ticker_enabled'].includes(field)) { + if (current_view === 'show') { + ctournament.update_metadata_settings(); + } + } + } + function change_score(cval) { const match_id = cval.match_id; @@ -43,67 +91,28 @@ function default_handler(rerender, special_funcs) { cerror.silent('Evakuierungsdurchsage wurde gestoppt'); } break; - case 'props': { - curt.name = c.val.name; - curt.is_team = c.val.is_team; - curt.tguid = c.val.tguid; - curt.is_nation_competition = c.val.is_nation_competition; - curt.btp_timezone = c.val.btp_timezone; - curt.btp_autofetch_timeout_intervall = c.val.btp_autofetch_timeout_intervall; - curt.warmup = c.val.warmup; - curt.warmup_ready = c.val.warmup_ready; - curt.warmup_start = c.val.warmup_start; - curt.btp_enabled = c.val.btp_enabled; - curt.btp_autofetch_enabled = c.val.btp_autofetch_enabled; - curt.btp_readonly = c.val.btp_readonly; - curt.btp_ip = c.val.btp_ip; - curt.ticker_enabled = c.val.ticker_enabled; - curt.ticker_url = c.val.ticker_url; - curt.ticker_password = c.val.ticker_password; - curt.logo_id = c.val.logo_id; + case 'props': { + for (const [field, value] of Object.entries(c.val)) { + _apply_tournament_field_change(field, value); + } + if (c.val.scoring_formats) { - curt.scoring_formats = c.val.scoring_formats; + ctournament.close_scoring_format_dialog_if_open(undefined, 'tournament:edit:scoring_formats:dialog_closed_external_change'); ctournament.update_scoring_formats(); ctournament.update_stages_scoring_formats(); } - - uiu.qsEach('.ct_name', function(el) { - if (el.tagName.toUpperCase() === 'INPUT') { - el.value = c.val.name; - } else { - uiu.text(el, c.val.name); + for (const [field, value] of Object.entries(c.val)) { + _after_tournament_field_change(field, value, c); } - }); - const CHECKBOXES = [ - 'is_team', 'is_nation_competition', - 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', - 'ticker_enabled', 'tabletoperator_enabled', 'tabletoperator_with_state_enabled', - 'tabletoperator_with_state_from_match_enabled', - 'tabletoperator_winner_of_quaterfinals_enabled', 'tabletoperator_split_doubles', - 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds', - 'announcement_speed', 'announcement_pause_time_ms', - 'upcoming_matches_animation_speed', 'upcoming_matches_max_count', 'upcoming_matches_animation_pause', - 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', - 'annoncement_include_event', 'annoncement_include_round', 'annoncement_include_matchnumber', - 'call_preparation_matches_automatically_enabled','call_next_possible_scheduled_match_in_preparation', - 'preparation_meetingpoint_enabled','preparation_tabletoperator_setup_enabled']; - for (const cb_name of CHECKBOXES) { - uiu.qsEach('input[name="' + cb_name + '"]', function(el) { - el.checked = curt[cb_name]; - }); + break; } - uiu.qsEach('input[name="btp_ip"]', function(el) { - el.value = curt.btp_ip; - }); - - uiu.qsEach('input[name="ticker_url"]', function(el) { - el.value = curt.ticker_url; - }); - uiu.qsEach('input[name="ticker_password"]', function(el) { - el.value = curt.ticker_password; - }); - break;} - case 'logo_changed': + case 'prop_changed': { + const field = c.val.field; + _apply_tournament_field_change(field, c.val.value); + _after_tournament_field_change(field, c.val.value, c); + break; + } + case 'logo_changed': if(c.val.logo_background_color != undefined) { curt.logo_background_color = c.val.logo_background_color; } @@ -355,26 +364,46 @@ function default_handler(rerender, special_funcs) { change_score(c.val); // Most dialogs don't show any matches, so do not rerender break; - case 'update_btp_settings': - if(!curt.btp_settings) { - curt.btp_settings = {}; - } - const btp_settings = c.val.btp_settings; - for (const [key, value] of Object.entries(btp_settings)) { - curt.btp_settings[key] = value; - } - break; - case 'update_btp_scoring_formats': - if(!curt.scoring_formats) { - curt.scoring_formats = {}; - } - const scoring_formats = c.val.scoring_formats; - for (const [key, value] of Object.entries(scoring_formats)) { - curt.scoring_formats[key] = value; - } - ctournament.update_scoring_formats(); - break; - case 'update_btp_events': + case 'update_btp_settings': + if(!curt.btp_settings) { + curt.btp_settings = {}; + } + const btp_settings = c.val.btp_settings; + for (const [key, value] of Object.entries(btp_settings)) { + curt.btp_settings[key] = value; + } + ctournament.update_btp_settings_ui(); + break; + case 'update_btp_scoring_formats': + if(!curt.scoring_formats) { + curt.scoring_formats = {}; + } + const scoring_formats = c.val.scoring_formats; + for (const [key, value] of Object.entries(scoring_formats)) { + curt.scoring_formats[key] = value; + } + ctournament.close_scoring_format_dialog_if_open(undefined, 'tournament:edit:scoring_formats:dialog_closed_btp_sync'); + ctournament.update_scoring_formats(); + break; + case 'scoring_format_changed': + if (!curt.scoring_formats) { + curt.scoring_formats = { formats: [], default_id: null }; + } + if (!Array.isArray(curt.scoring_formats.formats)) { + curt.scoring_formats.formats = []; + } + const changed_scoring_format = c.val.scoring_format; + const changed_index = curt.scoring_formats.formats.findIndex(f => Number(f.id) === Number(changed_scoring_format.id)); + if (changed_index === -1) { + curt.scoring_formats.formats.push(changed_scoring_format); + } else { + curt.scoring_formats.formats[changed_index] = changed_scoring_format; + } + ctournament.close_scoring_format_dialog_if_open(changed_scoring_format.id, 'tournament:edit:scoring_formats:dialog_closed_external_change'); + ctournament.update_scoring_formats(); + ctournament.update_stages_scoring_formats(); + break; + case 'update_btp_events': if(!curt.events) { curt.events = {}; } diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 10ef470..2a43b25 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -141,6 +141,9 @@ var ci18n_de = { 'tournament:edit': 'Einstellungen Verwalten', 'tournament:edit:save': 'Speichern', 'tournament:edit:save_and_back': 'Speichern und zur Turnierübersicht', + 'tournament:edit:live_status:saving': 'Speichert ...', + 'tournament:edit:live_status:saved': 'Alle Änderungen gespeichert', + 'tournament:edit:live_status:error': 'Speichern fehlgeschlagen', 'tournament:edit:tournament:type': 'Turnier-Typ:', @@ -221,6 +224,8 @@ var ci18n_de = { 'tournament:edit:scoring_formats': 'Punktsysteme', 'tournament:edit:scoring_formats:dialog_title': 'Punktsystem bearbeiten', 'tournament:edit:scoring_formats:dialog_hint': 'Aus BTP importierte Felder sind schreibgeschützt. Bearbeitet werden nur lokale Timing-Werte.', + 'tournament:edit:scoring_formats:dialog_closed_external_change': 'Der Dialog wurde geschlossen, weil dieses Punktsystem in einem anderen Fenster geändert wurde.', + 'tournament:edit:scoring_formats:dialog_closed_btp_sync': 'Der Dialog wurde geschlossen, weil die Punktsysteme durch den BTP-Abgleich aktualisiert wurden.', 'tournament:edit:scoring_formats:name': 'Name', 'tournament:edit:scoring_formats:num_sets': 'Sätze', 'tournament:edit:scoring_formats:regular_sets': 'Normale Sätze', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index a652780..e777d0b 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -139,6 +139,9 @@ var ci18n_en = { 'tournament:edit': 'Manage settings', 'tournament:edit:save': 'Save', 'tournament:edit:save_and_back': 'Save and back to the tournament overview', + 'tournament:edit:live_status:saving': 'Saving ...', + 'tournament:edit:live_status:saved': 'All changes saved', + 'tournament:edit:live_status:error': 'Saving failed', 'tournament:edit:tournament:type': 'Tournament type:', 'tournament:edit:id': 'Tournament id:', @@ -219,6 +222,8 @@ var ci18n_en = { 'tournament:edit:scoring_formats': 'Scoring formats', 'tournament:edit:scoring_formats:dialog_title': 'Edit scoring format', 'tournament:edit:scoring_formats:dialog_hint': 'Fields imported from BTP are read-only. Only local timing values can be edited.', + 'tournament:edit:scoring_formats:dialog_closed_external_change': 'The dialog was closed because this scoring format was changed in another window.', + 'tournament:edit:scoring_formats:dialog_closed_btp_sync': 'The dialog was closed because the scoring formats were updated by the BTP sync.', 'tournament:edit:scoring_formats:name': 'Name', 'tournament:edit:scoring_formats:num_sets': 'Sets', 'tournament:edit:scoring_formats:regular_sets': 'Regular sets', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 4f723dc..6f580bd 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -1,11 +1,10 @@ 'use strict'; var curt; // current tournament -<<<<<<< HEAD let current_view = null; -======= ->>>>>>> 2ba5ec7f5e5800ad694cac1720ee9329e4592f04 let scoring_formats_main = null; +let live_settings_status_el = null; +let live_settings_pending_requests = 0; var ctournament = (function() { function _route_single(rex, func, handler) { @@ -315,6 +314,165 @@ var ctournament = (function() { update_general_displaysettings(uiu.qs('.general_displaysettings')); } + function refresh_current_view() { + switch (current_view) { + case 'edit': + ui_edit(); + break; + case 'show': + ui_show(); + break; + case 'upcoming': + ui_upcoming(); + break; + case 'current_matches': + ui_current_matches(); + break; + case 'next_matches': + ui_next_matches(); + break; + default: + break; + } + } + + function _set_disabled_by_name(field_name, disabled) { + uiu.qsEach('[name="' + field_name + '"]', function(el) { + el.disabled = !!disabled; + }); + } + + function update_edit_dependencies() { + if (current_view !== 'edit') { + return; + } + + const warmup_select = document.querySelector('[name="warmup"]'); + if (warmup_select) { + const custom_warmup = ['choise', 'call-down']; + const is_custom = custom_warmup.includes(warmup_select.value); + _set_disabled_by_name('warmup_ready', !is_custom); + _set_disabled_by_name('warmup_start', !is_custom); + } + + const btp_enabled = !!curt.btp_enabled; + _set_disabled_by_name('btp_autofetch_enabled', !btp_enabled); + _set_disabled_by_name('btp_readonly', !btp_enabled); + _set_disabled_by_name('btp_ip', !btp_enabled); + _set_disabled_by_name('btp_password', !btp_enabled); + _set_disabled_by_name('btp_timezone', !btp_enabled); + _set_disabled_by_name('btp_autofetch_timeout_intervall', !btp_enabled || !curt.btp_autofetch_enabled); + + const ticker_enabled = !!curt.ticker_enabled; + _set_disabled_by_name('ticker_url', !ticker_enabled); + _set_disabled_by_name('ticker_password', !ticker_enabled); + + const tabletoperator_enabled = !!curt.tabletoperator_enabled; + [ + 'tabletoperator_with_umpire_enabled', + 'tabletoperator_winner_of_quaterfinals_enabled', + 'tabletoperator_use_manual_counting_boards_enabled', + 'tabletoperator_split_doubles', + 'tabletoperator_with_state_enabled', + 'tabletoperator_with_state_from_match_enabled', + 'tabletoperator_set_break_after_tabletservice', + 'tabletoperator_break_seconds', + ].forEach(field_name => _set_disabled_by_name(field_name, !tabletoperator_enabled)); + } + + function set_live_settings_status(status_key) { + if (!live_settings_status_el) { + return; + } + live_settings_status_el.className = 'live_settings_status live_settings_status_' + status_key; + uiu.text(live_settings_status_el, ci18n('tournament:edit:live_status:' + status_key)); + } + + function set_live_settings_status_message(message, status_key) { + if (!live_settings_status_el) { + return false; + } + live_settings_status_el.className = 'live_settings_status live_settings_status_' + (status_key || 'saved'); + uiu.text(live_settings_status_el, message); + return true; + } + + function begin_live_settings_request() { + live_settings_pending_requests += 1; + set_live_settings_status('saving'); + } + + function end_live_settings_request(err) { + live_settings_pending_requests = Math.max(0, live_settings_pending_requests - 1); + if (err) { + set_live_settings_status('error'); + return; + } + if (live_settings_pending_requests === 0) { + set_live_settings_status('saved'); + } else { + set_live_settings_status('saving'); + } + } + + function send_single_prop(field, value, callback) { + begin_live_settings_request(); + send({ + type: 'tournament_edit_prop', + key: curt.key, + field, + value, + }, (err) => { + end_live_settings_request(err); + if (callback) { + callback(err); + } + }); + } + + function send_with_live_status(msg, callback) { + begin_live_settings_request(); + send(msg, function(err, response) { + end_live_settings_request(err); + if (callback) { + return callback(err, response); + } + }); + } + + function bind_live_prop(el, field, options) { + options = options || {}; + const event_name = options.event_name || 'change'; + const get_value = options.get_value || function(input_el) { + if (input_el.type === 'checkbox') { + return input_el.checked; + } + return input_el.value; + }; + const on_before_send = options.on_before_send || function() {}; + const on_success = options.on_success || function() {}; + const on_error = options.on_error || function(input_el, old_value) { + if (input_el.type === 'checkbox') { + input_el.checked = !!old_value; + } else { + input_el.value = old_value ?? ''; + } + }; + + el.addEventListener(event_name, function() { + const old_value = curt[field]; + on_before_send(el); + const value = get_value(el); + send_single_prop(field, value, function(err) { + if (err) { + on_error(el, old_value); + return cerror.net(err); + } + on_success(el, value); + }); + }); + } + function _update_all_ui_elements_upcoming() { cmatch.render_courts(uiu.qs('.courts_container'), 'public'); cmatch.render_upcoming_matches(uiu.qs('.upcoming_container')); @@ -339,6 +497,52 @@ var ctournament = (function() { } } + function update_show_tabletoperators() { + if (current_view !== 'show') { + return; + } + const meta_div = document.querySelector('.metadata_container'); + if (!meta_div) { + return; + } + let container = meta_div.querySelector('.unassigned_tableoperators_container'); + if (curt.tabletoperator_enabled) { + if (!container) { + container = document.createElement('div'); + container.className = 'unassigned_tableoperators_container'; + meta_div.insertBefore(container, meta_div.firstChild); + } else { + container.innerHTML = ''; + } + _show_render_tabletoperators(); + } else if (container) { + container.remove(); + } + } + + function update_btp_settings_ui() { + switch (current_view) { + case 'show': + _show_render_matches(); + _show_render_umpires(); + break; + case 'upcoming': + _update_all_ui_elements_upcoming(); + break; + case 'current_matches': + _update_all_ui_elements_current_matches(); + break; + case 'next_matches': + _update_all_ui_elements_next_matches(); + break; + case 'edit': + update_edit_dependencies(); + break; + default: + break; + } + } + function _show_render_umpires() { cumpires.ui_status(uiu.qs('.umpire_container')); } @@ -741,6 +945,19 @@ var ctournament = (function() { } } + function update_metadata_settings() { + if (current_view !== 'show') { + return; + } + const settings_div = document.querySelector('.metadata_right_container_2'); + if (!settings_div) { + return; + } + const target = settings_div.parentNode; + settings_div.remove(); + render_settings(target); + } + function btp_status_changed(c) { set_service_status('btp_status', c); } @@ -771,7 +988,7 @@ var ctournament = (function() { const reader = new FileReader(); reader.readAsDataURL(input.files[0]); reader.onload = () => { - send({ + send_with_live_status({ type: 'tournament_upload_logo', tournament_key: curt.key, data_url: reader.result, @@ -828,23 +1045,25 @@ var ctournament = (function() { const name_label = uiu.el(tournament_div, 'label'); uiu.el(name_label, 'span', {}, ci18n('tournament:edit:name')); - input.name = uiu.el(name_label, 'input', { + input.name = uiu.el(name_label, 'input', { type: 'text', name: 'name', required: 'required', value: curt.name || curt.key, - 'class': 'ct_name', - }); + 'class': 'ct_name', + }); + bind_live_prop(input.name, 'name', { event_name: 'blur' }); const name_tguid = uiu.el(tournament_div, 'label'); uiu.el(name_tguid, 'span', {}, ci18n('tournament:edit:tguid')); - input.tguid = uiu.el(name_tguid, 'input', { + input.tguid = uiu.el(name_tguid, 'input', { type: 'text', name: 'tguid', value: curt.tguid ? curt.tguid : "", - 'class': 'ct_tguid', - }); + 'class': 'ct_tguid', + }); + bind_live_prop(input.tguid, 'tguid', { event_name: 'blur' }); // Tournament language selection const language_label = uiu.el(tournament_div, 'label'); @@ -864,7 +1083,8 @@ var ctournament = (function() { } uiu.el(language_select, 'option', l_attrs, l._name); } - input.language = language_select; + input.language = language_select; + bind_live_prop(input.language, 'language'); // Team competition? const is_team_label = uiu.el(tournament_div, 'label'); @@ -877,8 +1097,9 @@ var ctournament = (function() { is_team_attrs.checked = 'checked'; } - input.is_team = uiu.el(is_team_label, 'input', is_team_attrs); - uiu.el(is_team_label, 'span', {}, ci18n('team competition')); + input.is_team = uiu.el(is_team_label, 'input', is_team_attrs); + uiu.el(is_team_label, 'span', {}, ci18n('team competition')); + bind_live_prop(input.is_team, 'is_team'); // Nation competition? const is_nation_competition_label = uiu.el(tournament_div, 'label'); @@ -891,8 +1112,9 @@ var ctournament = (function() { } uiu.el(is_nation_competition_label, 'span', {}, ''); - input.is_nation_competition = uiu.el(is_nation_competition_label, 'input', is_nation_competition_attrs); - uiu.el(is_nation_competition_label, 'span', {}, ci18n('nation competition')); + input.is_nation_competition = uiu.el(is_nation_competition_label, 'input', is_nation_competition_attrs); + uiu.el(is_nation_competition_label, 'span', {}, ci18n('nation competition')); + bind_live_prop(input.is_nation_competition, 'is_nation_competition'); } // btp-connection-div################################################################################## @@ -910,8 +1132,9 @@ var ctournament = (function() { if (curt.btp_enabled) { ba_attrs.checked = 'checked'; } - input.btp_enabled = uiu.el(btp_enabled_label, 'input', ba_attrs); - uiu.el(btp_enabled_label, 'span', {}, ci18n('tournament:edit:btp:enabled')); + input.btp_enabled = uiu.el(btp_enabled_label, 'input', ba_attrs); + uiu.el(btp_enabled_label, 'span', {}, ci18n('tournament:edit:btp:enabled')); + bind_live_prop(input.btp_enabled, 'btp_enabled'); const btp_autofetch_enabled_label = uiu.el(btp_fieldset, 'label'); const bae_attrs = { @@ -921,8 +1144,9 @@ var ctournament = (function() { if (curt.btp_autofetch_enabled) { bae_attrs.checked = 'checked'; } - input.btp_autofetch_enabled = uiu.el(btp_autofetch_enabled_label, 'input', bae_attrs); - uiu.el(btp_autofetch_enabled_label, 'span', {}, ci18n('tournament:edit:btp:autofetch_enabled')); + input.btp_autofetch_enabled = uiu.el(btp_autofetch_enabled_label, 'input', bae_attrs); + uiu.el(btp_autofetch_enabled_label, 'span', {}, ci18n('tournament:edit:btp:autofetch_enabled')); + bind_live_prop(input.btp_autofetch_enabled, 'btp_autofetch_enabled'); const btp_readonly_label = uiu.el(btp_fieldset, 'label'); const bro_attrs = { @@ -932,29 +1156,32 @@ var ctournament = (function() { if (curt.btp_readonly) { bro_attrs.checked = 'checked'; } - if (!curt['btp_autofetch_timeout_intervall']) { - curt['btp_autofetch_timeout_intervall'] = 30000; - } - input.btp_autofetch_timeout_intervall = create_input(curt, "number", btp_connection_div, 'btp_autofetch_timeout_intervall') + if (!curt['btp_autofetch_timeout_intervall']) { + curt['btp_autofetch_timeout_intervall'] = 30000; + } + input.btp_autofetch_timeout_intervall = create_input(curt, "number", btp_connection_div, 'btp_autofetch_timeout_intervall') - input.btp_readonly = uiu.el(btp_readonly_label, 'input', bro_attrs); - uiu.el(btp_readonly_label, 'span', {}, ci18n('tournament:edit:btp:readonly')); + input.btp_readonly = uiu.el(btp_readonly_label, 'input', bro_attrs); + uiu.el(btp_readonly_label, 'span', {}, ci18n('tournament:edit:btp:readonly')); + bind_live_prop(input.btp_readonly, 'btp_readonly'); const btp_ip_label = uiu.el(btp_fieldset, 'label'); uiu.el(btp_ip_label, 'span', {}, ci18n('tournament:edit:btp:ip')); - input.btp_ip = uiu.el(btp_ip_label, 'input', { + input.btp_ip = uiu.el(btp_ip_label, 'input', { type: 'text', name: 'btp_ip', - value: (curt.btp_ip || ''), - }); + value: (curt.btp_ip || ''), + }); + bind_live_prop(input.btp_ip, 'btp_ip', { event_name: 'blur' }); const btp_password_label = uiu.el(btp_fieldset, 'label'); uiu.el(btp_password_label, 'span', {}, ci18n('tournament:edit:btp:password')); - input.btp_password = uiu.el(btp_password_label, 'input', { + input.btp_password = uiu.el(btp_password_label, 'input', { type: 'text', name: 'btp_password', - value: (curt.btp_password || ''), - }); + value: (curt.btp_password || ''), + }); + bind_live_prop(input.btp_password, 'btp_password', { event_name: 'blur' }); // BTP timezone const btp_timezone_label = uiu.el(btp_fieldset, 'label'); @@ -978,7 +1205,8 @@ var ctournament = (function() { uiu.el(btp_timezone_select, 'option', attrs, tz); } - input.btp_timezone = btp_timezone_select; + input.btp_timezone = btp_timezone_select; + bind_live_prop(input.btp_timezone, 'btp_timezone'); } // tournament-flow-div################################################################################## @@ -1010,7 +1238,7 @@ var ctournament = (function() { }); uiu.el(warmup_timer_select, 'option', { value: warmup_options[0][0] }, ci18n('tournament:edit:warmup_timer_behavior:' + warmup_options[0][0]), { wo: warmup_options[0][0] }); let warmup_marked = false; - input.warmup = warmup_timer_select; + input.warmup = warmup_timer_select; const warmup_ready = uiu.el(tournament_flow_div, 'label'); uiu.el(warmup_ready, 'span', {}, ci18n('tournament:edit:warmup_ready')); @@ -1021,7 +1249,10 @@ var ctournament = (function() { disabled: warmup_options[0][3], value: warmup_options[0][1], }); - input.warmup_ready = warmup_ready_input; + input.warmup_ready = warmup_ready_input; + bind_live_prop(input.warmup_ready, 'warmup_ready', { + get_value: input_el => Number(input_el.value), + }); const warmup_start = uiu.el(tournament_flow_div, 'label'); uiu.el(warmup_start, 'span', {}, ci18n('tournament:edit:warmup_start')); @@ -1032,7 +1263,10 @@ var ctournament = (function() { disabled: warmup_options[0][3], value: warmup_options[0][2], }); - input.warmup_start = warmup_start_input; + input.warmup_start = warmup_start_input; + bind_live_prop(input.warmup_start, 'warmup_start', { + get_value: input_el => Number(input_el.value), + }); for (const wo of warmup_options.slice(1)) { const attrs = { @@ -1054,7 +1288,7 @@ var ctournament = (function() { uiu.el(warmup_timer_select, 'option', attrs, ci18n('tournament:edit:warmup_timer_behavior:' + wo[0])); } - warmup_timer_select.onchange = function () { + warmup_timer_select.onchange = function () { if (!last_selected_warmup[3]) { for (const wo of warmup_options) { if (!wo[3]) { @@ -1071,10 +1305,25 @@ var ctournament = (function() { warmup_start_input.value = wo[2]; warmup_start_input.disabled = wo[3]; - last_selected_warmup = wo; + last_selected_warmup = wo; + } } - } - }; + send_single_prop('warmup', warmup_timer_select.value, function(err) { + if (err) { + return cerror.net(err); + } + }); + send_single_prop('warmup_ready', Number(warmup_ready_input.value), function(err) { + if (err) { + return cerror.net(err); + } + }); + send_single_prop('warmup_start', Number(warmup_start_input.value), function(err) { + if (err) { + return cerror.net(err); + } + }); + }; const bts_fieldset = uiu.el(tournament_flow_div, 'fieldset'); input.call_preparation_matches_automatically_enabled = create_checkbox(curt, bts_fieldset, 'call_preparation_matches_automatically_enabled'); @@ -1173,12 +1422,14 @@ var ctournament = (function() { } uiu.el(dm_style_select, 'option', s_attrs, s); } - input.dm_style = dm_style_select; + input.dm_style = dm_style_select; + bind_live_prop(input.dm_style, 'dm_style'); const displaysettings_style_label = uiu.el(default_display_fieldset, 'label'); uiu.el(displaysettings_style_label, 'span', {}, ci18n('tournament:edit:displaysettings_general')); - input.displaysettings_general = createGeneralDisplaySettingsSelectBox(displaysettings_style_label, curt.displaysettings_general ? curt.displaysettings_general : "default"); + input.displaysettings_general = createGeneralDisplaySettingsSelectBox(displaysettings_style_label, curt.displaysettings_general ? curt.displaysettings_general : "default"); + bind_live_prop(input.displaysettings_general, 'displaysettings_general'); const general_displaysettings_div = uiu.el(devices_div, 'div', 'general_displaysettings'); render_general_displaysettings(general_displaysettings_div); @@ -1215,116 +1466,53 @@ var ctournament = (function() { if (curt.ticker_enabled) { te_attrs.checked = 'checked'; } - input.ticker_enabled = uiu.el(ticker_enabled_label, 'input', te_attrs); - uiu.el(ticker_enabled_label, 'span', {}, ci18n('tournament:edit:ticker_enabled')); + input.ticker_enabled = uiu.el(ticker_enabled_label, 'input', te_attrs); + uiu.el(ticker_enabled_label, 'span', {}, ci18n('tournament:edit:ticker_enabled')); + bind_live_prop(input.ticker_enabled, 'ticker_enabled'); const ticker_url_label = uiu.el(ticker_fieldset, 'label'); uiu.el(ticker_url_label, 'span', {}, ci18n('tournament:edit:ticker_url')); - input.ticker_url = uiu.el(ticker_url_label, 'input', { + input.ticker_url = uiu.el(ticker_url_label, 'input', { type: 'text', name: 'ticker_url', - value: (curt.ticker_url || ''), - }); + value: (curt.ticker_url || ''), + }); + bind_live_prop(input.ticker_url, 'ticker_url', { event_name: 'blur' }); const ticker_password_label = uiu.el(ticker_fieldset, 'label'); uiu.el(ticker_password_label, 'span', {}, ci18n('tournament:edit:ticker_password')); - input.ticker_password = uiu.el(ticker_password_label, 'input', { + input.ticker_password = uiu.el(ticker_password_label, 'input', { type: 'text', name: 'ticker_password', - value: (curt.ticker_password || ''), - }); - } - - // save-div################################################################################## - { - const save_div = uiu.el(form, 'div', 'settings'); - uiu.el(save_div, 'h2', 'edit', ci18n('tournament:edit')); - - const save_btn = uiu.el(save_div, 'button', { - role: 'button', - }, ci18n('tournament:edit:save')); - save_btn.addEventListener('click', () => { - send_props(input, (err) => { - if (err) { - cerror.net(err); // Fehlerbehandlung - } + value: (curt.ticker_password || ''), }); - }); - - const save_and_back_btn = uiu.el(save_div, 'button', { - role: 'button', - }, ci18n('tournament:edit:save_and_back')); - save_and_back_btn.addEventListener('click', () => { - send_props(input, (err) => { - if (err) { - cerror.net(err); // Fehlerbehandlung - } + bind_live_prop(input.ticker_password, 'ticker_password', { event_name: 'blur' }); + } + // save-div################################################################################## + { + const save_div = uiu.el(form, 'div', 'settings'); + uiu.el(save_div, 'h2', 'edit', ci18n('tournament:edit')); + live_settings_pending_requests = 0; + live_settings_status_el = uiu.el(save_div, 'div', { + class: 'live_settings_status live_settings_status_saved', + }, ci18n('tournament:edit:live_status:saved')); + + const back_btn = uiu.el(save_div, 'button', { + role: 'button', + }, ci18n('Back')); + back_btn.addEventListener('click', () => { ui_show(); }); - }); - } - } + } + update_edit_dependencies(); + } _route_single(/t\/([a-z0-9]+)\/edit$/, ui_edit, change.default_handler(_update_all_ui_elements_edit, { update_general_displaysettings: update_general_displaysettings, update_player_status: update_player_status, })); - function send_props(input, callback) { - const props = { - name : input.name.value, - tguid: input.tguid.value, - language: input.language.value, - is_team: input.is_team.checked, - is_nation_competition: input.is_nation_competition.checked, - btp_enabled: input.btp_enabled.checked, - btp_autofetch_enabled: input.btp_autofetch_enabled.checked, - btp_autofetch_timeout_intervall: input.btp_autofetch_timeout_intervall.value, - btp_readonly: input.btp_readonly.checked, - btp_ip: input.btp_ip.value, - btp_password: input.btp_password.value, - btp_timezone: input.btp_timezone.value, - dm_style: input.dm_style.value, - displaysettings_general: input.displaysettings_general.value, - warmup: input.warmup.value, - warmup_ready: input.warmup_ready.value, - warmup_start: input.warmup_start.value, - ticker_enabled: input.ticker_enabled.checked, - ticker_url: input.ticker_url.value, - ticker_password: input.ticker_password.value, - upcoming_matches_animation_speed: input.upcoming_animation_speed.value, - upcoming_matches_max_count: input.upcoming_matches_max_count.value, - upcoming_matches_animation_pause: input.upcoming_animation_pause.value, - tabletoperator_enabled: input.tabletoperator_enabled.checked, - tabletoperator_with_umpire_enabled: input.tabletoperator_with_umpire_enabled.checked, - tabletoperator_winner_of_quaterfinals_enabled: input.tabletoperator_winner_of_quaterfinals_enabled.checked, - tabletoperator_split_doubles: input.tabletoperator_split_doubles.checked, - tabletoperator_with_state_enabled: input.tabletoperator_with_state_enabled.checked, - tabletoperator_with_state_from_match_enabled: input.tabletoperator_with_state_from_match_enabled.checked, - tabletoperator_set_break_after_tabletservice: input.tabletoperator_set_break_after_tabletservice.checked, - tabletoperator_use_manual_counting_boards_enabled: input.tabletoperator_use_manual_counting_boards_enabled.checked, - tabletoperator_break_seconds: input.tabletoperator_break_seconds.value, - annoncement_include_event: input.annoncement_include_event.checked, - annoncement_include_round: input.annoncement_include_round.checked, - annoncement_include_matchnumber: input.annoncement_include_matchnumber.checked, - announcement_speed: input.announcement_speed.value, - announcement_pause_time_ms: input.announcement_pause_time_ms.value, - preparation_meetingpoint_enabled: input.preparation_meetingpoint_enabled.checked, - preparation_tabletoperator_setup_enabled: input.preparation_tabletoperator_setup_enabled.checked, - call_preparation_matches_automatically_enabled: input.call_preparation_matches_automatically_enabled.checked, - call_next_possible_scheduled_match_in_preparation: input.call_next_possible_scheduled_match_in_preparation.checked, - } - - send({ - type: 'tournament_edit_props', - key: curt.key, - props: props, - }, (err) => { - return callback(err); - }); - } - - function update_scoring_formats() { + function update_scoring_formats() { if (!scoring_formats_main) { if (typeof debug !== "undefined" && debug?.log) { debug.log("update_scoring_formats: main container not initialized"); @@ -1416,6 +1604,25 @@ var ctournament = (function() { uiu.remove(dlg); } + function close_scoring_format_dialog_if_open(scoringFormatId, reason_i18n_key) { + const dlg = document.querySelector('.scoring_format_edit_dialog'); + if (!dlg) { + return false; + } + const open_id = dlg.getAttribute('data-scoring-format-id'); + if (typeof scoringFormatId !== 'undefined' && scoringFormatId !== null && Number(open_id) !== Number(scoringFormatId)) { + return false; + } + _cancel_ui_edit_scoring_format(); + if (reason_i18n_key) { + const reason_text = ci18n(reason_i18n_key); + if (!set_live_settings_status_message(reason_text, 'error')) { + cerror.silent(reason_text); + } + } + return true; + } + function create_scoring_format_field(parent, label, name, value, type = "text", attrs = {}) { const row = uiu.el(parent, 'label', 'scoring_format_edit_row'); uiu.el(row, 'span', {}, label); @@ -1537,21 +1744,10 @@ var ctournament = (function() { } function save_scoring_format(scoringFormatId, scoringFormat, callback) { - const scoringFormats = clone_scoring_formats(); - const formats = Array.isArray(scoringFormats.formats) ? scoringFormats.formats : []; - const index = formats.findIndex(f => Number(f.id) === Number(scoringFormatId)); - if (index === -1) { - return callback(new Error(`Unknown scoring format ${scoringFormatId}`)); - } - formats[index] = scoringFormat; - scoringFormats.formats = formats; - - send({ - type: 'tournament_edit_props', + send_with_live_status({ + type: 'tournament_edit_scoring_format', key: curt.key, - props: { - scoring_formats: scoringFormats, - }, + scoring_format: scoringFormat, }, callback); } @@ -1822,7 +2018,7 @@ var ctournament = (function() { new_normalization.replace = document.getElementById('normalizations_replace').value; new_normalization.language = document.getElementById('normalizations_language').value; - send({ + send_with_live_status({ type: 'normalization_add', tournament_key: curt.key, normalization: new_normalization, @@ -1845,10 +2041,10 @@ var ctournament = (function() { delete_btn.addEventListener('click', function (e) { const del_btn = e.target; const normalization_id = del_btn.getAttribute('data-normalization-id'); - send({ - type: 'normalization_remove', - tournament_key: curt.key, - normalization_id: normalization_id, + send_with_live_status({ + type: 'normalization_remove', + tournament_key: curt.key, + normalization_id: normalization_id, }, err => { if (err) { return cerror.net(err); @@ -1883,10 +2079,10 @@ var ctournament = (function() { new_advertisement.url = document.getElementById('advertisement_url').value; new_advertisement.type = document.getElementById('advertisement_type').value; new_advertisement.disabled = false; - send({ - type: 'advertisement_add', - tournament_key: curt.key, - advertisement: new_advertisement, + send_with_live_status({ + type: 'advertisement_add', + tournament_key: curt.key, + advertisement: new_advertisement, }, err => { if (err) { return cerror.net(err); @@ -1907,10 +2103,10 @@ var ctournament = (function() { delete_btn.addEventListener('click', function (e) { const del_btn = e.target; const advertisement_id = del_btn.getAttribute('data-advertisement-id'); - send({ - type: 'advertisement_remove', - tournament_key: curt.key, - advertisement_id: advertisement_id, + send_with_live_status({ + type: 'advertisement_remove', + tournament_key: curt.key, + advertisement_id: advertisement_id, }, err => { if (err) { return cerror.net(err); @@ -2009,7 +2205,7 @@ var ctournament = (function() { value: curt.logo_background_color || '#000000', }); logo_background_color_input.addEventListener('input', (e) => { - send({ + send_with_live_status({ type: 'tournament_edit_logo', key: curt.key, props: { @@ -2028,7 +2224,7 @@ var ctournament = (function() { value: curt.logo_foreground_color || '#aaaaaa', }); fg_col_input.addEventListener('input', (e) => { - send({ + send_with_live_status({ type: 'tournament_edit_logo', key: curt.key, props: { @@ -2108,7 +2304,7 @@ var ctournament = (function() { const del_btn = e.target; const setting_id = del_btn.getAttribute('data-display-setting-id'); - send({ + send_with_live_status({ type: 'delete_display_setting', tournament_key: curt.key, setting_id: setting_id, @@ -2171,7 +2367,7 @@ var ctournament = (function() { form_utils.onsubmit(form, function(d) { const displaysetting = create_displaysettings_object(d); - send({ + send_with_live_status({ type: 'edit_display_setting', tournament_key: curt.key, displaysetting: displaysetting, @@ -2570,7 +2766,7 @@ var ctournament = (function() { reset_btn.addEventListener('click', function (e) { const rst_btn = e.target; const display_client_id = rst_btn.getAttribute('data-display-client-id'); - send({ + send_with_live_status({ type: 'display_reset', tournament_key: curt.key, display_client_id: display_client_id, @@ -2591,7 +2787,7 @@ var ctournament = (function() { delete_btn.addEventListener('click', function (e) { const del_btn = e.target; const display_client_id = del_btn.getAttribute('data-display-client-id'); - send({ + send_with_live_status({ type: 'display_delete', tournament_key: curt.key, display_client_id: display_client_id, @@ -2730,7 +2926,7 @@ var ctournament = (function() { const reader = new FileReader(); reader.readAsDataURL(input.files[0]); reader.onload = () => { - send({ + send_with_live_status({ type: 'tournament_upload_location_logo', tournament_key: curt.key, data_url: reader.result, @@ -2767,7 +2963,7 @@ var ctournament = (function() { const preparation_addition = parent.querySelector("#preparation_addition").value; const meetingpoint_announcement = parent.querySelector("#meetingpoint_announcement").value; - send({ + send_with_live_status({ type: 'location_changed', tournament_key: curt.key, location_id, @@ -3109,7 +3305,7 @@ function render_officials_by_timestamp(main, { !!o.is_umpire ); umpire_cb.addEventListener('change', (e) => { - send({ + send_with_live_status({ type: 'official_edit', tournament_key: o.tournament_key, official_id: o._id, @@ -3125,7 +3321,7 @@ function render_officials_by_timestamp(main, { !!o.is_service_judge ); service_cb.addEventListener('change', (e) => { - send({ + send_with_live_status({ type: 'official_edit', tournament_key: o.tournament_key, official_id: o._id, @@ -3277,7 +3473,7 @@ function update_official_tables(officials_host) { const prev_btp_id = prev_id ? officialById.get(prev_id)?.btp_id : null; const next_btp_id = next_id ? officialById.get(next_id)?.btp_id : null; - send({ + send_with_live_status({ type: 'official_list_move', tournament_key: curt.key, official_id: row_id, @@ -3337,7 +3533,7 @@ function update_officials() { const active_cb = create_simple_checkbox(active_td, {'name' : 'active_cb', 'data-court-id': c._id,}, c.is_active); active_cb.addEventListener('change', (e) => { const court_id = e.target.getAttribute('data-court-id'); - send({ + send_with_live_status({ type: 'court_edit', tournament_key: curt.key, is_active: e.target.checked, @@ -3400,21 +3596,26 @@ function update_officials() { if (curt[filed_id]) { attrs.checked = 'checked'; } - const result = uiu.el(label, 'input', attrs); - uiu.el(label, 'span', {}, ci18n('tournament:edit:' + filed_id)); - return result; - } + const result = uiu.el(label, 'input', attrs); + uiu.el(label, 'span', {}, ci18n('tournament:edit:' + filed_id)); + bind_live_prop(result, filed_id); + return result; + } function create_input(curt, type, parent_el, filed_id) { const text_input = uiu.el(parent_el, 'label'); uiu.el(text_input, 'span', {}, ci18n('tournament:edit:' + filed_id)); - const result = uiu.el(text_input, 'input', { - type: type, - name: filed_id, - value: curt[filed_id] || '', - }); - return result; - } + const result = uiu.el(text_input, 'input', { + type: type, + name: filed_id, + value: curt[filed_id] || '', + }); + bind_live_prop(result, filed_id, { + event_name: (type === 'text') ? 'blur' : 'change', + get_value: type === 'number' ? input_el => Number(input_el.value) : undefined, + }); + return result; + } function create_undecorated_input(type, parent_el, filed_id) { return ( @@ -3441,15 +3642,19 @@ function update_officials() { function create_numeric_input(curt, parent_el, filed_id, min_value, max_value, default_value, step_value) { const text_input = uiu.el(parent_el, 'label'); uiu.el(text_input, 'span', {}, ci18n('tournament:edit:' + filed_id)); - return uiu.el(text_input, 'input', { - type: "number", - name: filed_id, - value: curt[filed_id] || default_value, - min: min_value, - max: max_value, - step: step_value - }); - } + const result = uiu.el(text_input, 'input', { + type: "number", + name: filed_id, + value: curt[filed_id] || default_value, + min: min_value, + max: max_value, + step: step_value + }); + bind_live_prop(result, filed_id, { + get_value: input_el => Number(input_el.value), + }); + return result; + } function createCourtSelectBox(parentEl, parent_id, court_id) { const court_select_box = uiu.el(parentEl, 'select', { @@ -3483,7 +3688,7 @@ function update_officials() { court_select_box.addEventListener('change', (e) => { const select_box = e.target; const display_setting_id = select_box.name.split("_")[1]; - send({ + send_with_live_status({ type: 'relocate_display', tournament_key: curt.key, new_court_id: e.srcElement.value, @@ -3506,7 +3711,7 @@ function update_officials() { displaysettings_select_box.addEventListener('change', (e) => { const select_box = e.target; const display_setting_id = select_box.name.split("_")[1]; - send({ + send_with_live_status({ type: 'change_display_mode', tournament_key: curt.key, new_displaysettings_id: e.srcElement.value, @@ -3823,9 +4028,15 @@ function update_officials() { add_normalization, remove_advertisement, add_advertisement, - update_general_displaysettings, - delete_display, - }; + update_general_displaysettings, + update_metadata_settings, + update_edit_dependencies, + update_btp_settings_ui, + update_show_tabletoperators, + close_scoring_format_dialog_if_open, + refresh_current_view, + delete_display, + }; })(); From f562fd88351f19640e0c72096568947349c35afb Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 4 Apr 2026 03:13:26 +0200 Subject: [PATCH 214/224] Add dropdown navigation for public match views --- static/css/toprow.css | 64 ++++++++++++++++++++++++++ static/js/ci18n_de.js | 3 ++ static/js/ci18n_en.js | 3 ++ static/js/ctournament.js | 99 +++++++++++++++++++++++++++++++--------- static/js/toprow.js | 28 ++++++++++++ 5 files changed, 175 insertions(+), 22 deletions(-) diff --git a/static/css/toprow.css b/static/css/toprow.css index bf6095c..e593c5c 100644 --- a/static/css/toprow.css +++ b/static/css/toprow.css @@ -26,3 +26,67 @@ .toprow_right > * + * { margin-left: 1.2em; } + +.toprow_menu { + position: relative; + display: inline-block; + padding-bottom: 0.35em; + margin-bottom: -0.35em; +} + +.toprow_menu_label { + display: inline-block; +} + +.toprow_menu_button { + font-size: 1.3em; + line-height: 1; +} + +.toprow_menu_dropdown { + position: absolute; + right: 0; + top: 100%; + display: none; + min-width: 220px; + background: #fff; + border: 1px solid #bbb; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18); + padding: 0.35em 0; + z-index: 20; +} + +.toprow_menu:hover::after, +.toprow_menu:focus-within::after { + content: ''; + position: absolute; + right: 0; + top: 100%; + width: 240px; + height: 1.4em; + transform: translateY(-1.4em); + z-index: 19; +} + +.toprow_menu:hover > .toprow_menu_dropdown, +.toprow_menu:focus-within > .toprow_menu_dropdown { + display: block; +} + +.toprow_menu_item { + display: block; + padding: 0.45em 0.9em; + color: black; + white-space: nowrap; +} + +.toprow_menu_separator { + height: 0; + margin: 0; + border-top: 1px solid #000; +} + +.toprow_menu_item:hover { + background: #f0f0f0; + text-decoration: none; +} diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 2a43b25..ba3525c 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -9,6 +9,9 @@ var ci18n_de = { 'Matchoverview': 'Spielübersicht', 'Scoreboard': 'Anzeigetafel', 'Umpire Panel': 'Schiedsrichter-Panel', + 'Menu': 'Menü', + 'All Locations': 'Alle Locations', + 'only location': 'nur', 'edit tournament': 'bearbeiten', 'Court': 'Court', 'Match': 'Spiel', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index e777d0b..f0f375f 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -9,6 +9,9 @@ var ci18n_en = { 'Matchoverview': 'Matchoverview', 'Scoreboard': 'Scoreboard', 'Umpire Panel': 'Umpire Panel', + 'Menu': 'Menu', + 'All Locations': 'All Locations', + 'only location': 'only', 'edit tournament': 'edit', 'Court': 'Court', 'Match': 'Match', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 6f580bd..15db4d6 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -220,6 +220,7 @@ var ctournament = (function() { m.btp_winner = cval.match.btp_winner; const new_section = cmatch.calc_section(m); cmatch.update_match(m, old_section, new_section); + rerender_public_match_views(old_section, new_section); if (old_section != new_section || new_section == 'unassigned') { uiu.qsEach('.upcoming_container', (upcoming_container) => { @@ -818,11 +819,56 @@ var ctournament = (function() { cmatch.update_tables(loc._id, checkbox.checked); }); } + + function build_location_view_menu_items() { + const base_path = '/admin/t/' + encodeURIComponent(curt.key); + const bup_lang = ((curt.language && curt.language !== 'auto') ? '&lang=' + encodeURIComponent(curt.language) : ''); + const bup_dm_style = '&dm_style=' + encodeURIComponent(curt.dm_style || 'international'); + const locations = curt.locations || []; + + function section_items(label, path_suffix) { + const items = [{ + label, + href: base_path + path_suffix, + }]; + + locations.forEach((loc) => { + const params = new URLSearchParams({ location: loc.name }); + items.push({ + label: label + ' (' + ci18n('only location') + ' ' + loc.name + ')', + href: base_path + path_suffix + '?' + params.toString(), + }); + }); + + items.push({ class: 'toprow_menu_separator' }); + return items; + } + + const view_items = [ + ...section_items(ci18n('Matchoverview'), '/upcoming'), + ...section_items(ci18n('Current Matches'), '/current_matches'), + ...section_items(ci18n('Next Matches'), '/next_matches'), + ]; + if (view_items.length > 0 && view_items[view_items.length - 1].class === 'toprow_menu_separator') { + view_items.pop(); + } + + return [{ + label: ci18n('Scoreboard'), + href: '/bup/#btsh_e=' + encodeURIComponent(curt.key) + '&display' + bup_dm_style + bup_lang, + }, { + class: 'toprow_menu_separator', + }, { + label: ci18n('Umpire Panel'), + href: '/bup/#btsh_e=' + encodeURIComponent(curt.key) + bup_lang, + }, { + class: 'toprow_menu_separator', + }, + ...view_items]; + } function ui_show() { current_view = 'show' crouting.set('t/:key/', { key: curt.key }); - const bup_lang = ((curt.language && curt.language !== 'auto') ? '&lang=' + encodeURIComponent(curt.language) : ''); - const bup_dm_style = '&dm_style=' + encodeURIComponent(curt.dm_style || 'international'); toprow.set([{ label: ci18n('Tournaments'), func: ui_list, @@ -831,20 +877,9 @@ var ctournament = (function() { func: ui_show, 'class': 'ct_name', }], [{ - label: ci18n('Scoreboard'), - href: '/bup/#btsh_e=' + encodeURIComponent(curt.key) + '&display' + bup_dm_style + bup_lang, - }, { - label: ci18n('Umpire Panel'), - href: '/bup/#btsh_e=' + encodeURIComponent(curt.key) + bup_lang, - }, { - label: ci18n('Matchoverview'), - href: '/admin/t/' + encodeURIComponent(curt.key) + '/upcoming', - }, { - label: ci18n('Current Matches'), - href: '/admin/t/' + encodeURIComponent(curt.key) + '/current_matches', - }, { - label: ci18n('Next Matches'), - href: '/admin/t/' + encodeURIComponent(curt.key) + '/next_matches', + label: '\u2630', + class: 'toprow_menu_button', + items: build_location_view_menu_items(), }]); const main = uiu.qs('.main'); @@ -3765,6 +3800,29 @@ function update_officials() { cmatch.render_upcoming_matches(upcoming_container); } + function rerender_public_match_views(old_section, new_section) { + const affects_courts = ( + old_section.startsWith('court_') || + new_section.startsWith('court_') + ); + const affects_upcoming = ( + old_section === 'unassigned' || + new_section === 'unassigned' + ); + + if ((current_view === 'upcoming' || current_view === 'current_matches') && affects_courts) { + uiu.qsEach('.courts_container', (courts_container) => { + cmatch.render_courts(courts_container, 'public'); + }); + } + + if ((current_view === 'upcoming' || current_view === 'next_matches') && affects_upcoming) { + uiu.qsEach('.upcoming_container', (upcoming_container) => { + cmatch.render_upcoming_matches(upcoming_container); + }); + } + } + function ui_upcoming() { current_view = 'upcoming'; const main = ui_match_screens('t/:key/upcoming'); @@ -3784,15 +3842,12 @@ function update_officials() { } function ui_match_screens(route) { - current_view = 'match_screen'; crouting.set(route, { key: curt.key }); toprow.hide(); const main = uiu.qs('.main'); uiu.empty(main); main.classList.add('main_upcoming'); - main.addEventListener('click', () => { - fullscreen.toggle(); - }); + main.onclick = () => fullscreen.toggle(); return main; } @@ -3803,13 +3858,13 @@ function update_officials() { update_player_status: update_player_status, })); - _route_single(/t\/([a-z0-9]+)\/current_matches/, ui_current_matches, change.default_handler(_update_all_ui_elements_upcoming, { + _route_single(/t\/([a-z0-9]+)\/current_matches/, ui_current_matches, change.default_handler(_update_all_ui_elements_current_matches, { score: update_score, court_current_match: update_upcoming_current_match, match_edit: update_upcoming_match, update_player_status: update_player_status, })); - _route_single(/t\/([a-z0-9]+)\/next_matches/, ui_next_matches, change.default_handler(_update_all_ui_elements_upcoming, { + _route_single(/t\/([a-z0-9]+)\/next_matches/, ui_next_matches, change.default_handler(_update_all_ui_elements_next_matches, { score: update_score, court_current_match: update_upcoming_current_match, match_edit: update_upcoming_match, diff --git a/static/js/toprow.js b/static/js/toprow.js index e1c9cbf..80bb5a6 100644 --- a/static/js/toprow.js +++ b/static/js/toprow.js @@ -10,6 +10,34 @@ function update_container(container, elems, with_sep) { uiu.el(container, 'span', 'toprow_sep', '>'); } + if (el.class === 'toprow_menu_separator') { + uiu.el(container, 'div', 'toprow_menu_separator'); + return; + } + + if (el.items && Array.isArray(el.items)) { + const menu = uiu.el(container, 'div', 'toprow_menu'); + uiu.el(menu, 'span', 'toprow_link vlink toprow_menu_label', el.label); + const dropdown = uiu.el(menu, 'div', 'toprow_menu_dropdown'); + el.items.forEach(function(item) { + if (item.class === 'toprow_menu_separator') { + uiu.el(dropdown, 'div', 'toprow_menu_separator'); + return; + } + const item_attrs = { + 'class': 'toprow_menu_item' + ((item.func || item.href) ? ' vlink' : '') + (item.class ? (' ' + item.class) : ''), + }; + if (item.href) { + item_attrs.href = item.href; + } + const item_el = uiu.el(dropdown, (item.href ? 'a' : 'span'), item_attrs, item.label); + if (item.func) { + item_el.addEventListener('click', item.func); + } + }); + return; + } + const css_class = 'toprow_link' + ((el.func || el.href) ? ' vlink' : '') + (el.class ? (' ' + el.class) : ''); const attrs = { 'class': css_class, From 8292e398eb28d9fcf161e4be0d516a4aab020dbb Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 4 Apr 2026 03:20:51 +0200 Subject: [PATCH 215/224] Add self-check-in view and participant check-in --- bts/admin.js | 71 +++- static/css/cmatch.css | 230 +++++++++++ static/js/ci18n_de.js | 8 +- static/js/ci18n_en.js | 6 + static/js/cmatch.js | 102 +++-- static/js/ctournament.js | 800 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 1139 insertions(+), 78 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index cbad72b..3d270fa 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -961,6 +961,50 @@ function handle_match_player_check_in (app, ws, msg) { }); } +function handle_match_participant_check_in(app, ws, msg) { + const match_utils = require('./match_utils'); + + if (!_require_msg(ws, msg, ['tournament_key', 'match_id', 'role', 'checked_in'])) { + return; + } + + app.db.matches.findOne({ tournament_key: msg.tournament_key, _id: msg.match_id }, async (err, match) => { + if (err) { + return ws.respond(msg, err); + } + if (!match || !match.setup) { + return ws.respond(msg, new Error('Match not found')); + } + + let participant_found = false; + const checked_in = !!msg.checked_in; + + if (msg.role === 'umpire' || msg.role === 'service_judge') { + const participant = match.setup[msg.role]; + if (participant && participant.btp_id == msg.participant_id) { + participant.checked_in = checked_in; + participant_found = true; + } + } else if (msg.role === 'tabletoperator' && Array.isArray(match.setup.tabletoperators)) { + match.setup.tabletoperators.forEach((participant) => { + if (participant.btp_id == msg.participant_id) { + participant.checked_in = checked_in; + participant_found = true; + } + }); + } + + if (!participant_found) { + return ws.respond(msg, new Error('Participant not found in match')); + } + + match_utils.match_update(app, match, undefined, (update_err) => { + ws.respond(msg, update_err); + return; + }); + }); +} + function handle_begin_to_play_call(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'setup'])) { @@ -1206,17 +1250,18 @@ function handle_official_list_move(app, ws, msg) { // Spezifikation: // - currentUmpire[from_list] = null // - currentUmpire[to_list] = newTS - const setObj = {}; - - setObj[inactive_list] = null; - setObj[service_judge_pause] = null; - setObj[umpire_pause] = null; - setObj[service_judge_wait] = null; - setObj[umpire_wait] = null; - setObj[service_judge_on_court] = null; - setObj[umpire_on_court] = null; - setObj[is_planed_as_service_judge] = false; - setObj[is_planed_as_umpire] = false; + const setObj = {}; + + setObj[from_list] = null; + setObj['inactive_list'] = null; + setObj['service_judge_pause'] = null; + setObj['umpire_pause'] = null; + setObj['service_judge_wait'] = null; + setObj['umpire_wait'] = null; + setObj['service_judge_on_court'] = null; + setObj['umpire_on_court'] = null; + setObj['is_planed_as_service_judge'] = false; + setObj['is_planed_as_umpire'] = false; setObj[to_list] = newTS; app.db.umpires.update( @@ -1341,7 +1386,8 @@ function handle_add_officials_to_match(app, ws, msg) { name: u.name, firstname: u.firstname, surname: u.surname, - country: u.country + country: u.country, + checked_in: false }; } @@ -1834,6 +1880,7 @@ module.exports = { handle_match_call_on_court, handle_match_preparation_call, handle_match_player_check_in, + handle_match_participant_check_in, handle_ticker_pushall, handle_ticker_reset, handle_free_announce, diff --git a/static/css/cmatch.css b/static/css/cmatch.css index aea208d..aad4f5a 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -398,6 +398,236 @@ match_manual_call_button, background-color: unset; } +.main_self_check_in { + background: #000; + color: #fff; + height: 100vh; + overflow: hidden; + padding: 20px; + box-sizing: border-box; +} + +.self_check_in_container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + position: relative; +} + +.self_check_in_title { + margin: 0 0 1rem; + text-align: center; + flex: 0 0 auto; + font-size: clamp(2rem, 4vw, 3.8rem); +} + +.self_check_in_empty { + margin: auto 0; + text-align: center; + font-size: clamp(1.4rem, 3vw, 2rem); + color: #ddd; +} + +.self_check_in_list { + flex: 1 1 auto; + display: grid; + gap: 0.9rem; + min-height: 0; + align-items: stretch; +} + +.self_check_in_match { + --self-check-in-header-height: 96px; + --self-check-in-header-gap: 8px; + --self-check-in-number-font-size: 22px; + --self-check-in-event-font-size: 34px; + --self-check-in-meta-font-size: 14px; + --self-check-in-status-font-size: 14px; + border-radius: 1rem; + padding: calc(1rem * var(--self-check-in-scale, 1)); + border: 3px solid #444; + background: #111; + min-height: 0; + display: grid; + grid-template-rows: minmax(0, var(--self-check-in-header-height)) minmax(0, 1fr); + overflow: hidden; +} + +.self_check_in_match_ready { + border-color: #4caf50; + box-shadow: 0 0 0 1px rgba(76, 175, 80, 0.35); +} + +.self_check_in_match_waiting { + border-color: #9a6a00; + box-shadow: 0 0 0 1px rgba(255, 193, 7, 0.25); +} + +.self_check_in_match_heading { + display: block; + margin-bottom: var(--self-check-in-header-gap); + min-height: 0; + overflow: hidden; +} + +.self_check_in_match_top_row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; +} + +.self_check_in_match_heading_left { + min-width: 0; +} + +.self_check_in_match_number { + font-size: var(--self-check-in-number-font-size); + font-weight: bold; + line-height: 1; +} + +.self_check_in_match_event { + font-size: var(--self-check-in-event-font-size); + font-weight: bold; + line-height: 1; + min-width: 0; +} + +.self_check_in_match_event_row { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; +} + +.self_check_in_match_court { + flex: 0 0 auto; + color: #bbb; + font-size: var(--self-check-in-event-font-size); + line-height: 1.1; + text-align: right; + white-space: nowrap; +} + +.self_check_in_match_meta { + margin-top: 0.2rem; + color: #bbb; + font-size: var(--self-check-in-meta-font-size); + line-height: 1.1; + min-width: 0; +} + +.self_check_in_match_status { + flex: 0 0 auto; + font-weight: bold; + text-align: right; + font-size: var(--self-check-in-status-font-size); + line-height: 1.1; + white-space: nowrap; +} + +.self_check_in_chips { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-auto-rows: 1fr; + gap: calc(0.45rem * var(--self-check-in-scale, 1)); + align-content: stretch; + min-height: 0; + align-self: stretch; + height: 100%; + --self-check-in-chip-name-scale: 1; + --self-check-in-chip-box-scale: 1; + --self-check-in-chip-name-scale-row: 1; + --self-check-in-chip-box-scale-row: 1; +} + +.self_check_in_chips[data-columns="1"] { + grid-template-columns: minmax(0, 1fr); +} + +.self_check_in_chips[data-has-officials="1"][data-rows="2"] { + grid-template-rows: 0.9fr 1.1fr; +} + +.self_check_in_chips[data-has-officials="1"][data-rows="3"] { + grid-template-rows: 0.9fr 0.9fr 1.2fr; +} + +.self_check_in_chip { + appearance: none; + border: 0; + border-radius: min(1.8rem, calc(0.9rem * var(--self-check-in-scale, 1) * var(--self-check-in-chip-box-scale, 1))); + padding: calc(0.2rem * var(--self-check-in-scale, 1) * var(--self-check-in-chip-box-scale, 1) * var(--self-check-in-chip-box-scale-row, 1)) calc(0.5rem * var(--self-check-in-scale, 1) * var(--self-check-in-chip-box-scale, 1) * var(--self-check-in-chip-box-scale-row, 1)); + font: inherit; + color: #111; + display: inline-flex; + align-items: center; + gap: calc(0.25rem * var(--self-check-in-scale, 1) * var(--self-check-in-chip-box-scale, 1) * var(--self-check-in-chip-box-scale-row, 1)); + cursor: pointer; + width: 100%; + max-width: 100%; + height: 100%; + min-width: 0; + overflow: hidden; +} + +.self_check_in_chip_with_role { + flex-direction: column; + align-items: stretch; + justify-content: center; +} + +.self_check_in_chip_waiting { + background: #ff8a80; +} + +.self_check_in_chip_ready { + background: #a5d6a7; +} + +.self_check_in_chip_role { + font-size: calc(0.58rem * var(--self-check-in-scale, 1) * var(--self-check-in-chip-box-scale, 1) * var(--self-check-in-chip-box-scale-row, 1)); + font-weight: bold; + text-transform: uppercase; + opacity: 0.8; + display: block; + line-height: 1; +} + +.self_check_in_chip_name { + font-size: calc(clamp(0.95rem, calc(2vw * var(--self-check-in-scale, 1)), 1.8rem) * var(--self-check-in-chip-name-scale, 1) * var(--self-check-in-chip-name-scale-row, 1)); + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + display: block; + width: 100%; + line-height: 1.2; +} + +.self_check_in_container[data-match-count="1"] .self_check_in_title { + margin-bottom: 1.5rem; +} + +.self_check_in_container[data-match-count="1"] .self_check_in_list { + --self-check-in-scale: 1; +} + +.self_check_in_container[data-match-count="1"] .self_check_in_match { + --self-check-in-header-height: 120px; +} + +.self_check_in_container[data-match-count="1"] .self_check_in_match_event { + font-size: clamp(1.8rem, 4vw, 3rem); +} + +.self_check_in_container[data-match-count="1"] .self_check_in_chip_name { + font-size: clamp(1.4rem, 3vw, 2.4rem); +} + @font-face { font-family: "SEGMENT"; src: url("./seg7.ttf"); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index ba3525c..8c518df 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -7,13 +7,19 @@ var ci18n_de = { 'Next Matches': 'Nächste Spiele', 'Current Matches': 'Laufende Spiele', 'Matchoverview': 'Spielübersicht', + 'Self-Check-In': 'Self-Check-In', + 'Self-Check-In: ready': 'Bereit zum Aufruf', + 'Self-Check-In: waiting': 'Warten auf Check-In', + 'Self-Check-In: empty': 'Aktuell sind keine Paarungen in Vorbereitung.', 'Scoreboard': 'Anzeigetafel', 'Umpire Panel': 'Schiedsrichter-Panel', + 'Service judge': 'Aufschlagrichter', + 'Tablet operator': 'Tabletbedienung', 'Menu': 'Menü', 'All Locations': 'Alle Locations', 'only location': 'nur', 'edit tournament': 'bearbeiten', - 'Court': 'Court', + 'Court': 'Feld', 'Match': 'Spiel', 'Players': 'Spieler', 'Umpire': 'Schiedsrichter', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index f0f375f..b02494c 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -7,8 +7,14 @@ var ci18n_en = { 'Next Matches': 'Next Matches', 'Current Matches': 'Current Matches', 'Matchoverview': 'Matchoverview', + 'Self-Check-In': 'Self-Check-In', + 'Self-Check-In: ready': 'Ready for call', + 'Self-Check-In: waiting': 'Waiting for check-in', + 'Self-Check-In: empty': 'There are currently no matches in preparation.', 'Scoreboard': 'Scoreboard', 'Umpire Panel': 'Umpire Panel', + 'Service judge': 'Service judge', + 'Tablet operator': 'Tablet operator', 'Menu': 'Menu', 'All Locations': 'All Locations', 'only location': 'only', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index 440b89d..b6165a6 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -110,8 +110,6 @@ function resize_table(resizable_rows, table_width_factor) { }); } - - function resizable_auto_size(parrent_el, factor, fixed_size, table_width_factor) { parrent_el.classList.add("auto_size_parrent"); parrent_el.style.width = (table_width_factor * window.innerWidth - fixed_size) * factor + 'px'; @@ -372,50 +370,41 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ resizable_elements.variable_width_factor.push(1); } - if(style != 'public') { - const to_td = uiu.el(tr, 'td', 'umpire_and_tablet'); - if (style === 'default' || style === 'plain' || style === 'unasigned') { - if (setup.umpire && setup.umpire.name) { - const umpire_span = uiu.el(to_td, 'span', 'person'); - uiu.el(umpire_span, 'div', 'umpire', ''); - uiu.el(umpire_span, 'span', 'name', setup.umpire.name); + if(style != 'public') { + const to_td = uiu.el(tr, 'td', 'umpire_and_tablet'); + const show_participant_check_in_status = !setup.now_on_court; + if (style === 'default' || style === 'plain' || style === 'unasigned') { + if (setup.umpire && setup.umpire.name) { + const umpire_span = render_match_participant_el(to_td, setup.umpire, match._id, 'umpire', 'umpire', show_participant_check_in_status); - if(style === 'unasigned' && match.setup.highlight >= 1){ - create_match_button(umpire_span, 'vlink match_second_preparation_call_button', 'match:secondcallumpire', on_second_preparation_call_umpire_button_click, match._id); - } + if(style === 'unasigned' && match.setup.highlight >= 1){ + create_match_button(umpire_span, 'vlink match_second_preparation_call_button', 'match:secondcallumpire', on_second_preparation_call_umpire_button_click, match._id); + } if (style === 'default' || style === 'plain' || style === 'unasigned') { create_match_button(umpire_span, 'vlink match_second_call_button', 'match:secondcallumpire', on_second_call_umpire_button_click, match._id); - } - if (setup.service_judge && setup.service_judge.name) { - - const service_judge_span = uiu.el(to_td, 'span', 'person'); - uiu.el(service_judge_span, 'div', 'service_judge', ''); - uiu.el(service_judge_span, 'span', 'name', setup.service_judge.name); - if(style === 'unasigned' && match.setup.highlight >= 1){ - create_match_button(service_judge_span, 'vlink match_second_preparation_call_button', 'match:secondcallservicejudge', on_second_preparation_call_servicejudge_button_click, match._id); } + if (setup.service_judge && setup.service_judge.name) { + + const service_judge_span = render_match_participant_el(to_td, setup.service_judge, match._id, 'service_judge', 'service_judge', show_participant_check_in_status); + if(style === 'unasigned' && match.setup.highlight >= 1){ + create_match_button(service_judge_span, 'vlink match_second_preparation_call_button', 'match:secondcallservicejudge', on_second_preparation_call_servicejudge_button_click, match._id); + } if (style === 'default' || style === 'plain' || style === 'unasigned') { create_match_button(service_judge_span, 'vlink match_second_call_button', 'match:secondcallservicejudge', on_second_call_servicejudge_button_click, match._id); } } - } - if (setup.tabletoperators && setup.tabletoperators.length > 0) { - const tablet_div = uiu.el(to_td, 'div', 'tablet_operator', ''); - uiu.el(tablet_div, 'div', 'tablet', ''); - - const operators_div = uiu.el(tablet_div, 'div', 'operators'); - const person_div = uiu.el(operators_div, 'div', 'person'); - uiu.el(person_div, 'span', 'match_no_umpire', setup.tabletoperators[0].name ); - - if (setup.tabletoperators.length > 1) { - uiu.el(person_div, 'span', 'match_no_umpire', ' \u200B/ '); - const person2_div = uiu.el(operators_div, 'div', 'person'); - uiu.el(person2_div, 'span', 'match_no_umpire', setup.tabletoperators[1].name ); } + if (setup.tabletoperators && setup.tabletoperators.length > 0) { + const tablet_div = uiu.el(to_td, 'div', 'tablet_operator', ''); + + const operators_div = uiu.el(tablet_div, 'div', 'operators'); + setup.tabletoperators.forEach((operator) => { + render_match_participant_el(operators_div, operator, match._id, 'tabletoperator', 'tablet', show_participant_check_in_status); + }); - if(style === 'unasigned' && match.setup.highlight >= 1){ - create_match_button(tablet_div, 'vlink match_second_preparation_call_button', 'match:secondcaltabletoperator', on_second_preparation_call_tabletoperator_button_click, match._id); - } + if(style === 'unasigned' && match.setup.highlight >= 1){ + create_match_button(tablet_div, 'vlink match_second_preparation_call_button', 'match:secondcaltabletoperator', on_second_preparation_call_tabletoperator_button_click, match._id); + } if (style === 'default' || style === 'plain' || style === 'unasigned') { create_match_button(tablet_div, 'vlink match_second_call_button', 'match:secondcaltabletoperator', on_second_call_tabletoperator_button_click, match._id); @@ -806,6 +795,45 @@ function render_players_el(parentNode, setup, team_id, match, show_player_status } } +function render_match_participant_el(parentNode, participant, match_id, role, icon_class, show_check_in_status = true) { + const participant_status = show_check_in_status && participant && participant.checked_in ? 'checked_in' : (show_check_in_status ? 'not_checked_in' : 'no_status'); + const participant_el = uiu.el(parentNode, 'span', { + 'class': 'person ' + participant_status, + 'data-btp_id': participant.btp_id, + 'data-match_id': match_id, + }, participant.name || short_name(participant.firstname, participant.lastname || participant.surname, participant.name)); + + participant_el.innerHTML = ''; + uiu.el(participant_el, 'div', icon_class, ''); + uiu.el(participant_el, 'span', 'name', participant.name || short_name(participant.firstname, participant.lastname || participant.surname, participant.name)); + + if (show_check_in_status && participant.btp_id != null && participant.btp_id >= 0) { + if (participant_status === 'checked_in') { + participant_el.classList.add('can_check_out'); + } else { + participant_el.classList.add('can_check_in'); + } + + participant_el.addEventListener('click', function(ev) { + send({ + type: 'match_participant_check_in', + match_id, + role, + participant_id: participant.btp_id, + checked_in: participant_status === 'not_checked_in', + tournament_key: curt.key + }, function (err) { + if (err) { + return cerror.net(err); + } + }); + ev.stopPropagation(); + }, false); + } + + return participant_el; +} + function render_player_el(parentNode, player, match_id, now_on_court, show_player_status, style, is_doubles) { let player_status = get_player_status(player, now_on_court, show_player_status); const player_name = (style === 'public' || style === 'upcoming' && is_doubles) ? short_name(player.firstname, player.lastname) : player.name; @@ -1817,9 +1845,7 @@ function render_upcoming_matches(container) { }); resizable_rows.push(render_match_row(tr, match, null, 'upcoming')); } - resize_table(resizable_rows, 0.98); - const qr = uiu.el(container, 'img', { type: 'img', id: 'main_q_code_upcoming', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 15db4d6..29a3891 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -5,6 +5,12 @@ let current_view = null; let scoring_formats_main = null; let live_settings_status_el = null; let live_settings_pending_requests = 0; +let self_check_in_chip_fit_cache = Object.create(null); +let self_check_in_layout_fit_cache = new Map(); +let self_check_in_resize_frame = null; +let self_check_in_measure_probe = null; +let self_check_in_fit_scheduled = false; +let self_check_in_fit_roots = new Set(); var ctournament = (function() { function _route_single(rex, func, handler) { @@ -199,6 +205,29 @@ var ctournament = (function() { return old_section; } + function rerender_public_match_views(old_section, new_section) { + const affects_courts = ( + old_section.startsWith('court_') || + new_section.startsWith('court_') + ); + const affects_upcoming = ( + old_section === 'unassigned' || + new_section === 'unassigned' + ); + + if ((current_view === 'upcoming' || current_view === 'current_matches') && affects_courts) { + uiu.qsEach('.courts_container', (courts_container) => { + cmatch.render_courts(courts_container, 'public'); + }); + } + + if ((current_view === 'upcoming' || current_view === 'next_matches') && affects_upcoming) { + uiu.qsEach('.upcoming_container', (upcoming_container) => { + cmatch.render_upcoming_matches(upcoming_container); + }); + } + } + function update_upcoming_match(c) { const cval = c.val; const match_id = cval.match__id; @@ -332,6 +361,9 @@ var ctournament = (function() { case 'next_matches': ui_next_matches(); break; + case 'self_check_in': + ui_self_check_in(); + break; default: break; } @@ -833,14 +865,19 @@ var ctournament = (function() { }]; locations.forEach((loc) => { - const params = new URLSearchParams({ location: loc.name }); + const params = new URLSearchParams({ + location: loc.name, + }); items.push({ label: label + ' (' + ci18n('only location') + ' ' + loc.name + ')', href: base_path + path_suffix + '?' + params.toString(), }); }); - items.push({ class: 'toprow_menu_separator' }); + items.push({ + class: 'toprow_menu_separator', + }); + return items; } @@ -848,6 +885,7 @@ var ctournament = (function() { ...section_items(ci18n('Matchoverview'), '/upcoming'), ...section_items(ci18n('Current Matches'), '/current_matches'), ...section_items(ci18n('Next Matches'), '/next_matches'), + ...section_items(ci18n('Self-Check-In'), '/self_check_in'), ]; if (view_items.length > 0 && view_items[view_items.length - 1].class === 'toprow_menu_separator') { view_items.pop(); @@ -3300,22 +3338,18 @@ for (const tbody of tbodies) { * DEINE TABELLE (pro Feld) - gibt TBODY zurück * ============================================================ */ -function render_officials_by_timestamp(main, { +function render_officials_table(main, { title = null, - officials, - timestamp_field, + rows, + table_id, min_height_px = 240 }) { if (title) { uiu.el(main, 'h2', 'edit', title); } - const rows = officials - .filter(o => o[timestamp_field] !== null) - .sort((a, b) => a[timestamp_field] - b[timestamp_field]); - const table = uiu.el(main, 'table', 'officials_table'); - const tbody = uiu.el(table, 'tbody', { 'data-table-id': timestamp_field }); + const tbody = uiu.el(table, 'tbody', { 'data-table-id': table_id }); /* ---------- Header ---------- */ const trHead = uiu.el(tbody, 'tr'); @@ -3400,6 +3434,24 @@ function render_officials_by_timestamp(main, { return { table, tbody }; } +function render_officials_by_timestamp(main, { + title = null, + officials, + timestamp_field, + min_height_px = 240 +}) { + const rows = officials + .filter(o => o[timestamp_field] !== null) + .sort((a, b) => a[timestamp_field] - b[timestamp_field]); + + return render_officials_table(main, { + title, + rows, + table_id: timestamp_field, + min_height_px + }); +} + function enable_min_height_resize_recalc(tables) { window._officialMinHeightTables = tables; @@ -3800,27 +3852,613 @@ function update_officials() { cmatch.render_upcoming_matches(upcoming_container); } - function rerender_public_match_views(old_section, new_section) { - const affects_courts = ( - old_section.startsWith('court_') || - new_section.startsWith('court_') - ); - const affects_upcoming = ( - old_section === 'unassigned' || - new_section === 'unassigned' + function get_location_name_filter() { + const params = new URLSearchParams(window.location.search); + return params.get('location'); + } + + function match_matches_selected_location(match) { + const param_location = get_location_name_filter(); + if (!param_location) { + return true; + } + + const loc = utils.find(curt.locations, l => l._id === match.setup.location_id); + if (!loc) { + return true; + } + return loc.name === param_location; + } + + function person_display_name(person) { + if (!person) { + return ''; + } + if (person.name) { + return person.name; + } + const surname = person.lastname || person.surname || ''; + return [person.firstname, surname].filter(Boolean).join(' '); + } + + function split_person_name_parts(person) { + const firstname = (person && person.firstname ? person.firstname : '').trim(); + const surname = ((person && (person.lastname || person.surname)) ? (person.lastname || person.surname) : '').trim(); + if (firstname || surname) { + return { + first_parts: firstname ? firstname.split(/\s+/).filter(Boolean) : [], + surname, + }; + } + + const fallback = person_display_name(person).trim(); + if (!fallback) { + return { first_parts: [], surname: '' }; + } + const parts = fallback.split(/\s+/).filter(Boolean); + if (parts.length <= 1) { + return { first_parts: parts, surname: '' }; + } + return { + first_parts: parts.slice(0, -1), + surname: parts[parts.length - 1], + }; + } + + function build_person_name_variants(person) { + const { first_parts, surname } = split_person_name_parts(person); + if (first_parts.length === 0) { + return [person_display_name(person)]; + } + + const variants = []; + const push_variant = (first_name_parts) => { + const label = [...first_name_parts, surname].filter(Boolean).join(' ').trim(); + if (label && !variants.includes(label)) { + variants.push(label); + } + }; + + push_variant(first_parts); + + if (first_parts.length > 1) { + push_variant([ + first_parts[0], + ...first_parts.slice(1).map(name => name[0] + '.'), + ]); + push_variant([first_parts[0]]); + } + + push_variant([first_parts[0][0] + '.']); + return variants; + } + + function get_self_check_in_matches() { + return curt.matches + .filter(m => m.setup && m.setup.state === 'preparation' && m.setup.is_match && match_matches_selected_location(m)) + .sort((a, b) => (a.setup.preparation_call_timestamp || 0) - (b.setup.preparation_call_timestamp || 0)); + } + + function self_check_in_match_structure_signature(match) { + if (!match || !match.setup || !match.setup.is_match || match.setup.state !== 'preparation' || !match_matches_selected_location(match)) { + return null; + } + + return JSON.stringify({ + match_id: match._id, + match_num: match.setup.match_num, + event_name: match.setup.event_name, + scheduled_time_str: match.setup.scheduled_time_str, + location_id: match.setup.location_id, + participants: self_check_in_participants(match).map(participant => ({ + key: participant.key, + label: participant.label, + role_label: participant.role_label || '', + })), + }); + } + + function self_check_in_match_status_signature(match) { + if (!match || !match.setup || !match.setup.is_match || match.setup.state !== 'preparation' || !match_matches_selected_location(match)) { + return null; + } + return JSON.stringify({ + match_id: match._id, + participants: self_check_in_participants(match).map((participant) => ({ + key: participant.key, + checked_in: participant.checked_in, + })), + }); + } + + function update_self_check_in_match_status(match_id) { + const list = document.querySelector('.self_check_in_list'); + const match = utils.find(curt.matches, m => m._id === match_id); + if (!list || !match) { + _update_all_ui_elements_self_check_in(); + return; + } + const card = list.querySelector('.self_check_in_match[data-match-id="' + match_id + '"]'); + if (!card) { + _update_all_ui_elements_self_check_in(); + return; + } + + const participants = self_check_in_participants(match); + const chips = Array.from(card.querySelectorAll('.self_check_in_chip')); + if (chips.length !== participants.length) { + update_self_check_in_match_card(match_id); + return; + } + + const chips_by_key = new Map( + chips.map((chip) => [chip.getAttribute('data-participant-key'), chip]) ); + for (const participant of participants) { + const chip = chips_by_key.get(participant.key); + if (!chip) { + update_self_check_in_match_card(match_id); + return; + } + chip.classList.toggle('self_check_in_chip_ready', !!participant.checked_in); + chip.classList.toggle('self_check_in_chip_waiting', !participant.checked_in); + } - if ((current_view === 'upcoming' || current_view === 'current_matches') && affects_courts) { - uiu.qsEach('.courts_container', (courts_container) => { - cmatch.render_courts(courts_container, 'public'); + const all_ready = participants.length > 0 && participants.every((participant) => participant.checked_in); + card.classList.toggle('self_check_in_match_ready', all_ready); + card.classList.toggle('self_check_in_match_waiting', !all_ready); + const status_el = card.querySelector('.self_check_in_match_status'); + if (status_el) { + status_el.textContent = ci18n(all_ready ? 'Self-Check-In: ready' : 'Self-Check-In: waiting'); + } + } + + function rerender_self_check_in_if_needed(before_structure_signature, before_status_signature, match_id) { + const container = document.querySelector('.self_check_in_container'); + const match = utils.find(curt.matches, m => m._id === match_id); + const after_structure_signature = self_check_in_match_structure_signature(match); + const after_status_signature = self_check_in_match_status_signature(match); + if (before_structure_signature !== after_structure_signature) { + if (!container || !after_structure_signature) { + _update_all_ui_elements_self_check_in(); + return; + } + const visible_before_ids = Array.from(container.querySelectorAll('.self_check_in_match')).map((card) => String(card.getAttribute('data-match-id'))); + const visible_after_ids = get_self_check_in_matches().map((m) => String(m._id)); + if ( + visible_before_ids.length !== visible_after_ids.length || + visible_before_ids.some((id, index) => id !== visible_after_ids[index]) + ) { + _update_all_ui_elements_self_check_in(); + return; + } + update_self_check_in_match_card(match_id); + return; + } + + if (before_status_signature !== after_status_signature) { + update_self_check_in_match_status(match_id); + } + } + + function self_check_in_participants(match) { + const participants = []; + match.setup.teams.forEach((team, team_index) => { + team.players.forEach((player, player_index) => { + participants.push({ + key: 'player_' + team_index + '_' + player_index + '_' + player.btp_id, + role: 'player', + match_id: match._id, + participant_id: player.btp_id, + label: person_display_name(player), + label_variants: build_person_name_variants(player), + checked_in: !!player.checked_in, + }); + }); + }); + + if (match.setup.umpire && match.setup.umpire.btp_id != null) { + participants.push({ + key: 'umpire_' + match.setup.umpire.btp_id, + role: 'umpire', + match_id: match._id, + participant_id: match.setup.umpire.btp_id, + label: person_display_name(match.setup.umpire), + label_variants: build_person_name_variants(match.setup.umpire), + checked_in: !!match.setup.umpire.checked_in, + role_label: ci18n('Umpire'), }); } - if ((current_view === 'upcoming' || current_view === 'next_matches') && affects_upcoming) { - uiu.qsEach('.upcoming_container', (upcoming_container) => { - cmatch.render_upcoming_matches(upcoming_container); + if (match.setup.service_judge && match.setup.service_judge.btp_id != null) { + participants.push({ + key: 'service_judge_' + match.setup.service_judge.btp_id, + role: 'service_judge', + match_id: match._id, + participant_id: match.setup.service_judge.btp_id, + label: person_display_name(match.setup.service_judge), + label_variants: build_person_name_variants(match.setup.service_judge), + checked_in: !!match.setup.service_judge.checked_in, + role_label: ci18n('Service judge'), + }); + } + + if (Array.isArray(match.setup.tabletoperators)) { + match.setup.tabletoperators.forEach((operator, index) => { + participants.push({ + key: 'tabletoperator_' + index + '_' + operator.btp_id, + role: 'tabletoperator', + match_id: match._id, + participant_id: operator.btp_id, + label: person_display_name(operator), + label_variants: build_person_name_variants(operator), + checked_in: !!operator.checked_in, + role_label: ci18n('Tablet operator'), + }); + }); + } + + return participants; + } + + function resolve_self_check_in_court_label(match) { + const current_match = utils.find(curt.matches, (m) => m._id === match._id) || match; + const court_id = (match.setup && match.setup.court_id) || (current_match.setup && current_match.setup.court_id); + let court = null; + if (court_id && curt.courts_by_id && curt.courts_by_id[court_id]) { + court = curt.courts_by_id[court_id]; + } + if (!court && Array.isArray(curt.courts)) { + court = curt.courts.find((c) => c._id === court_id) + || curt.courts.find((c) => c.match_id === match._id) + || curt.courts.find((c) => current_match && c.match_id === current_match._id) + || curt.courts.find((c) => match.btp_id && c.match_id === ('btp_' + match.btp_id)) + || curt.courts.find((c) => current_match && current_match.btp_id && c.match_id === ('btp_' + current_match.btp_id)); + } + if (!court || !court.num) { + return ''; + } + return ci18n('Court') + ' ' + court.num; + } + + function render_self_check_in_chip(container, participant) { + const attrs = { + type: 'button', + 'class': 'self_check_in_chip self_check_in_chip_' + (participant.checked_in ? 'ready' : 'waiting') + (participant.role_label ? ' self_check_in_chip_with_role' : ''), + 'data-role': participant.role, + 'data-match_id': participant.match_id, + 'data-participant_id': participant.participant_id, + 'data-participant-key': participant.key, + }; + const chip = uiu.el(container, 'button', attrs); + if (participant.role_label) { + uiu.el(chip, 'span', 'self_check_in_chip_role', participant.role_label); + } + const cached_fit = self_check_in_chip_fit_cache[participant.key]; + const initial_label = cached_fit ? cached_fit.label : participant.label; + const name_el = uiu.el(chip, 'span', 'self_check_in_chip_name', initial_label); + chip._name_el = name_el; + chip._label_variants = participant.label_variants || [participant.label]; + chip._participant_key = participant.key; + if (cached_fit && cached_fit.font_size) { + name_el.style.fontSize = cached_fit.font_size; + } + chip.addEventListener('click', function(ev) { + ev.stopPropagation(); + const checked_in = !chip.classList.contains('self_check_in_chip_ready'); + const payload = { + tournament_key: curt.key, + match_id: participant.match_id, + checked_in, + }; + + if (participant.role === 'player') { + payload.type = 'match_player_check_in'; + payload.player_id = participant.participant_id; + } else { + payload.type = 'match_participant_check_in'; + payload.role = participant.role; + payload.participant_id = participant.participant_id; + } + + send(payload, function(err) { + if (err) { + return cerror.net(err); + } }); + }); + } + + function fit_self_check_in_chip(chip) { + const name_el = chip._name_el; + const variants = chip._label_variants || [name_el.textContent]; + if (!name_el.dataset.baseFontSize) { + const previous_font_size = name_el.style.fontSize; + name_el.style.fontSize = ''; + name_el.dataset.baseFontSize = String(parseFloat(window.getComputedStyle(name_el).fontSize)); + name_el.style.fontSize = previous_font_size; + } + const chip_style = window.getComputedStyle(chip); + const css_base_font_size = Number(name_el.dataset.baseFontSize) || 16; + const role_el = chip.querySelector('.self_check_in_chip_role'); + const available_height = chip.clientHeight + - parseFloat(chip_style.paddingTop || 0) + - parseFloat(chip_style.paddingBottom || 0) + - (role_el ? role_el.offsetHeight + parseFloat(chip_style.gap || 0) : 0); + const height_based_font_size = Math.max( + css_base_font_size, + available_height * (role_el ? 0.58 : 0.7) + ); + const base_font_size = height_based_font_size; + const available_width = chip.clientWidth + - parseFloat(chip_style.paddingLeft || 0) + - parseFloat(chip_style.paddingRight || 0); + const measure_text_width = (text, font_size_px) => { + if (!self_check_in_measure_probe) { + self_check_in_measure_probe = document.createElement('span'); + self_check_in_measure_probe.style.position = 'absolute'; + self_check_in_measure_probe.style.visibility = 'hidden'; + self_check_in_measure_probe.style.pointerEvents = 'none'; + self_check_in_measure_probe.style.whiteSpace = 'nowrap'; + self_check_in_measure_probe.style.left = '-99999px'; + self_check_in_measure_probe.style.top = '0'; + document.body.appendChild(self_check_in_measure_probe); + } + const probe = self_check_in_measure_probe; + probe.textContent = text; + const name_style = window.getComputedStyle(name_el); + probe.style.fontFamily = name_style.fontFamily; + probe.style.fontWeight = name_style.fontWeight; + probe.style.fontStyle = name_style.fontStyle; + probe.style.fontStretch = name_style.fontStretch; + probe.style.fontVariant = name_style.fontVariant; + probe.style.fontSize = font_size_px + 'px'; + probe.style.lineHeight = name_style.lineHeight; + probe.style.letterSpacing = window.getComputedStyle(name_el).letterSpacing; + const width = probe.getBoundingClientRect().width; + return width; + }; + const min_font_size = Math.max(base_font_size * 0.18, 8); + const line_height_factor = 1.2; + const max_font_by_height = Math.max(min_font_size, (available_height / line_height_factor) * 0.98); + const layout_fit_cache_key = JSON.stringify({ + variants, + width: Math.round(available_width), + height: Math.round(available_height), + base_font_size: Math.round(base_font_size * 10) / 10, + min_font_size: Math.round(min_font_size * 10) / 10, + max_font_by_height: Math.round(max_font_by_height * 10) / 10, + }); + const cached_layout_fit = self_check_in_layout_fit_cache.get(layout_fit_cache_key); + if (cached_layout_fit) { + name_el.textContent = cached_layout_fit.label; + name_el.style.fontSize = cached_layout_fit.font_size; + self_check_in_chip_fit_cache[chip._participant_key] = cached_layout_fit; + return; + } + let best = null; + + for (const variant of variants) { + const width_at_base = measure_text_width(variant, base_font_size); + const width_ratio = available_width / Math.max(width_at_base, 1); + const width_limited_font_size = base_font_size * width_ratio * 0.98; + const fitted_font_size = Math.max( + min_font_size, + Math.min(base_font_size, max_font_by_height, width_limited_font_size) + ); + + if (!best || fitted_font_size > best.font_size + 0.05) { + best = { + label: variant, + font_size: fitted_font_size, + }; + } + } + + const chosen = best || { + label: variants[variants.length - 1], + font_size: min_font_size, + }; + name_el.textContent = chosen.label; + name_el.style.fontSize = chosen.font_size + 'px'; + const chosen_fit = { + label: chosen.label, + font_size: name_el.style.fontSize, + }; + self_check_in_layout_fit_cache.set(layout_fit_cache_key, chosen_fit); + self_check_in_chip_fit_cache[chip._participant_key] = chosen_fit; + } + + function fit_self_check_in_card(card) { + const card_height = card.clientHeight || 0; + if (card_height > 0) { + const header_height = Math.max(56, Math.min(card_height * 0.26, 140)); + card.style.setProperty('--self-check-in-header-height', header_height + 'px'); + card.style.setProperty('--self-check-in-header-gap', Math.max(4, Math.min(header_height * 0.06, 12)) + 'px'); + card.style.setProperty('--self-check-in-number-font-size', Math.max(18, Math.min(header_height * 0.18, 34)) + 'px'); + card.style.setProperty('--self-check-in-event-font-size', Math.max(26, Math.min(header_height * 0.3, 54)) + 'px'); + card.style.setProperty('--self-check-in-meta-font-size', Math.max(18, Math.min(header_height * 0.18, 34)) + 'px'); + card.style.setProperty('--self-check-in-status-font-size', Math.max(18, Math.min(header_height * 0.18, 34)) + 'px'); + const heading = card.querySelector('.self_check_in_match_heading'); + const header_gap = Math.max(4, Math.min(header_height * 0.06, 12)); + if (heading) { + const available_header_height = Math.max(24, header_height - header_gap); + if (heading.scrollHeight > available_header_height + 1) { + const ratio = available_header_height / Math.max(heading.scrollHeight, 1); + card.style.setProperty('--self-check-in-number-font-size', Math.max(14, Math.min(header_height * 0.18 * ratio * 0.98, 34)) + 'px'); + card.style.setProperty('--self-check-in-event-font-size', Math.max(18, Math.min(header_height * 0.3 * ratio * 0.98, 54)) + 'px'); + card.style.setProperty('--self-check-in-meta-font-size', Math.max(14, Math.min(header_height * 0.18 * ratio * 0.98, 34)) + 'px'); + card.style.setProperty('--self-check-in-status-font-size', Math.max(14, Math.min(header_height * 0.18 * ratio * 0.98, 34)) + 'px'); + } + } + } + card.querySelectorAll('.self_check_in_chip').forEach(fit_self_check_in_chip); + card.style.visibility = 'visible'; + } + + function update_self_check_in_match_card(match_id) { + const list = document.querySelector('.self_check_in_list'); + const match = utils.find(curt.matches, m => m._id === match_id); + if (!list || !match) { + _update_all_ui_elements_self_check_in(); + return; + } + const current_card = list.querySelector('.self_check_in_match[data-match-id="' + match_id + '"]'); + if (!current_card) { + _update_all_ui_elements_self_check_in(); + return; + } + const temp = document.createElement('div'); + render_self_check_in_match_card(temp, match, false); + const new_card = temp.firstElementChild; + if (!new_card) { + _update_all_ui_elements_self_check_in(); + return; } + [ + '--self-check-in-header-height', + '--self-check-in-header-gap', + '--self-check-in-number-font-size', + '--self-check-in-event-font-size', + '--self-check-in-meta-font-size', + '--self-check-in-status-font-size', + ].forEach((prop) => { + const value = current_card.style.getPropertyValue(prop); + if (value) { + new_card.style.setProperty(prop, value); + } + }); + current_card.replaceWith(new_card); + schedule_fit_self_check_in_cards(new_card); + } + + function render_self_check_in_match_card(container, match, do_fit) { + if (do_fit === undefined) { + do_fit = true; + } + const participants = self_check_in_participants(match); + const all_ready = participants.length > 0 && participants.every(p => p.checked_in); + const columns = participants.length <= 3 ? 1 : 2; + const rows = Math.ceil(participants.length / columns); + const has_officials = participants.some((participant) => !!participant.role_label); + const card = uiu.el(container, 'section', { + 'class': 'self_check_in_match ' + (all_ready ? 'self_check_in_match_ready' : 'self_check_in_match_waiting'), + }); + card.setAttribute('data-match-id', String(match._id)); + card.setAttribute('data-rows', String(rows)); + card.style.visibility = 'hidden'; + + const heading = uiu.el(card, 'div', 'self_check_in_match_heading'); + const left = uiu.el(heading, 'div', 'self_check_in_match_heading_left'); + const top_row = uiu.el(left, 'div', 'self_check_in_match_top_row'); + uiu.el(top_row, 'div', 'self_check_in_match_number', '#' + match.setup.match_num); + uiu.el(top_row, 'div', 'self_check_in_match_status', ci18n(all_ready ? 'Self-Check-In: ready' : 'Self-Check-In: waiting')); + const event_row = uiu.el(left, 'div', 'self_check_in_match_event_row'); + uiu.el(event_row, 'div', 'self_check_in_match_event', match.setup.event_name || ''); + uiu.el(event_row, 'div', 'self_check_in_match_court', resolve_self_check_in_court_label(match)); + + const meta = []; + if (match.setup.scheduled_time_str) { + meta.push(match.setup.scheduled_time_str); + } + if (match.setup.location_id) { + const loc = utils.find(curt.locations, l => l._id === match.setup.location_id); + if (loc && loc.name) { + meta.push(loc.name); + } + } + if (meta.length > 0) { + uiu.el(left, 'div', 'self_check_in_match_meta', meta.join(' • ')); + } + + const chips = uiu.el(card, 'div', 'self_check_in_chips'); + chips.setAttribute('data-columns', String(columns)); + chips.setAttribute('data-rows', String(rows)); + chips.setAttribute('data-has-officials', has_officials ? '1' : '0'); + if (rows === 3) { + chips.style.setProperty('--self-check-in-chip-name-scale-row', '1.12'); + chips.style.setProperty('--self-check-in-chip-box-scale-row', '1.04'); + } else if (rows === 2) { + chips.style.setProperty('--self-check-in-chip-name-scale-row', '0.80'); + chips.style.setProperty('--self-check-in-chip-box-scale-row', '0.98'); + } else { + chips.style.setProperty('--self-check-in-chip-name-scale-row', '1'); + chips.style.setProperty('--self-check-in-chip-box-scale-row', '1'); + } + participants.forEach((participant) => render_self_check_in_chip(chips, participant)); + if (do_fit) { + schedule_fit_self_check_in_cards(card); + } + } + + function calc_self_check_in_grid(match_count) { + if (match_count <= 1) { + return { cols: 1, rows: 1 }; + } + + let best = null; + for (let cols = 1; cols <= match_count; cols++) { + for (let rows = 1; rows <= cols; rows++) { + const area = cols * rows; + if (area < match_count) { + continue; + } + + const candidate = { + cols, + rows, + area, + diff: cols - rows, + }; + + if ( + !best || + candidate.cols < best.cols || + (candidate.cols === best.cols && candidate.area < best.area) || + (candidate.cols === best.cols && candidate.area === best.area && candidate.diff < best.diff) + ) { + best = candidate; + } + } + if (best && best.cols === cols) { + break; + } + } + + return { cols: best.cols, rows: best.rows }; + } + + function render_self_check_in(container) { + uiu.empty(container); + + const matches = get_self_check_in_matches(); + container.setAttribute('data-match-count', String(matches.length)); + if (matches.length === 0) { + uiu.el(container, 'div', 'self_check_in_empty', ci18n('Self-Check-In: empty')); + return; + } + + const list = uiu.el(container, 'div', 'self_check_in_list'); + const grid = calc_self_check_in_grid(matches.length); + const display_cols = grid.rows; + const display_rows = grid.cols; + const density = Math.max(display_cols, display_rows); + let scale = Math.max(0.42, Math.min(1, 1.75 / density)); + if (display_cols === 2) { + scale *= 1.5; + } else if (display_cols === 1) { + scale *= 2; + } + scale = Math.min(scale, 2); + const chip_name_scale = Math.max(0.9, Math.min(1.75, 3 / display_rows)); + const chip_box_scale = Math.max(0.72, Math.min(1.6, 2.4 / display_rows)); + list.style.gridTemplateColumns = 'repeat(' + display_cols + ', minmax(0, 1fr))'; + list.style.gridTemplateRows = 'repeat(' + display_rows + ', minmax(0, 1fr))'; + list.style.setProperty('--self-check-in-scale', String(scale)); + list.style.setProperty('--self-check-in-chip-name-scale', String(chip_name_scale)); + list.style.setProperty('--self-check-in-chip-box-scale', String(chip_box_scale)); + matches.forEach((match) => render_self_check_in_match_card(list, match, false)); + schedule_fit_self_check_in_cards(list); } function ui_upcoming() { @@ -3841,13 +4479,28 @@ function update_officials() { render_next_matches(main); } - function ui_match_screens(route) { + function ui_self_check_in() { + current_view = 'self_check_in'; + const main = ui_match_screens('t/:key/self_check_in', { + enable_fullscreen_toggle: false, + main_class: 'main_self_check_in', + }); + const container = uiu.el(main, 'div', 'self_check_in_container'); + render_self_check_in(container); + } + + function ui_match_screens(route, options) { + options = options || {}; crouting.set(route, { key: curt.key }); toprow.hide(); const main = uiu.qs('.main'); uiu.empty(main); - main.classList.add('main_upcoming'); - main.onclick = () => fullscreen.toggle(); + main.classList.remove('main_upcoming', 'main_self_check_in'); + main.classList.add(options.main_class || 'main_upcoming'); + main.onclick = null; + if (options.enable_fullscreen_toggle !== false) { + main.onclick = () => fullscreen.toggle(); + } return main; } @@ -3871,6 +4524,99 @@ function update_officials() { update_player_status: update_player_status, })); + function _update_all_ui_elements_self_check_in() { + render_self_check_in(uiu.qs('.self_check_in_container')); + } + + function schedule_fit_self_check_in_cards(scope) { + self_check_in_fit_roots.add(scope || document); + if (self_check_in_fit_scheduled) { + return; + } + self_check_in_fit_scheduled = true; + requestAnimationFrame(() => { + requestAnimationFrame(() => { + const cards = new Set(); + self_check_in_fit_roots.forEach((root) => { + if (!root) { + return; + } + if (root.classList && root.classList.contains('self_check_in_match')) { + cards.add(root); + } + root.querySelectorAll?.('.self_check_in_match').forEach((card) => cards.add(card)); + }); + self_check_in_fit_roots.clear(); + self_check_in_fit_scheduled = false; + cards.forEach((card) => { + if (card && card.isConnected) { + fit_self_check_in_card(card); + } + }); + }); + }); + } + + function schedule_self_check_in_resize_recalc() { + if (current_view !== 'self_check_in') { + return; + } + if (self_check_in_resize_frame) { + cancelAnimationFrame(self_check_in_resize_frame); + } + self_check_in_resize_frame = requestAnimationFrame(() => { + self_check_in_resize_frame = null; + self_check_in_chip_fit_cache = Object.create(null); + self_check_in_layout_fit_cache = new Map(); + const container = document.querySelector('.self_check_in_container'); + if (container) { + render_self_check_in(container); + } + }); + } + + window.addEventListener('resize', schedule_self_check_in_resize_recalc); + + _route_single(/t\/([a-z0-9]+)\/self_check_in/, ui_self_check_in, change.default_handler(_update_all_ui_elements_self_check_in, { + score: function(c) { + const before_structure_signature = self_check_in_match_structure_signature(utils.find(curt.matches, m => m._id === c.val.match_id)); + const before_status_signature = self_check_in_match_status_signature(utils.find(curt.matches, m => m._id === c.val.match_id)); + update_score(c); + rerender_self_check_in_if_needed(before_structure_signature, before_status_signature, c.val.match_id); + }, + court_current_match: function(c) { + const before_structure_signature = self_check_in_match_structure_signature(utils.find(curt.matches, m => m._id === c.val.match__id)); + const before_status_signature = self_check_in_match_status_signature(utils.find(curt.matches, m => m._id === c.val.match__id)); + update_upcoming_current_match(c); + rerender_self_check_in_if_needed(before_structure_signature, before_status_signature, c.val.match__id); + }, + match_edit: function(c) { + const before_structure_signature = self_check_in_match_structure_signature(utils.find(curt.matches, m => m._id === c.val.match__id)); + const before_status_signature = self_check_in_match_status_signature(utils.find(curt.matches, m => m._id === c.val.match__id)); + update_match(c); + rerender_self_check_in_if_needed(before_structure_signature, before_status_signature, c.val.match__id); + }, + update_player_status: function(c) { + const before_structure_signature = self_check_in_match_structure_signature(utils.find(curt.matches, m => m._id === c.val.match__id)); + const before_status_signature = self_check_in_match_status_signature(utils.find(curt.matches, m => m._id === c.val.match__id)); + update_player_status(c); + rerender_self_check_in_if_needed(before_structure_signature, before_status_signature, c.val.match__id); + }, + match_preparation_call: function(c) { + const before_structure_signature = self_check_in_match_structure_signature(utils.find(curt.matches, m => m._id === c.val.match__id)); + const before_status_signature = self_check_in_match_status_signature(utils.find(curt.matches, m => m._id === c.val.match__id)); + const changed_match = c.val.match; + const cur_match = utils.find(curt.matches, m => m._id === c.val.match__id); + if (cur_match && changed_match) { + cur_match.setup = changed_match.setup; + cur_match.btp_winner = changed_match.btp_winner; + cur_match.team1_won = changed_match.team1_won; + cur_match.network_score = changed_match.network_score; + } + rerender_self_check_in_if_needed(before_structure_signature, before_status_signature, c.val.match__id); + }, + })); + function init() { send({ From dfb9d04a4d7b0fdbe8c3d38ba422346ffc193258 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 4 Apr 2026 03:34:01 +0200 Subject: [PATCH 216/224] Add self-check-in called-match overlay and settings --- bts/admin.js | 2 ++ static/css/cmatch.css | 30 +++++++++++++++++ static/js/change.js | 40 ++++++++++++++--------- static/js/ci18n_de.js | 4 ++- static/js/ci18n_en.js | 4 ++- static/js/ctournament.js | 70 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 17 deletions(-) diff --git a/bts/admin.js b/bts/admin.js index 3d270fa..01961cd 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -69,6 +69,7 @@ function handle_tournament_edit_props(app, ws, msg) { 'is_team', 'is_nation_competition', 'warmup', 'warmup_ready', 'warmup_start', 'upcoming_matches_animation_speed', 'upcoming_matches_max_count','upcoming_matches_animation_pause', + 'self_check_in_called_overlay_duration_ms', 'ticker_enabled', 'ticker_url', 'ticker_password', 'language', 'dm_style', 'displaysettings_general', 'tabletoperator_enabled', 'tabletoperator_break_seconds', @@ -129,6 +130,7 @@ function handle_tournament_edit_prop(app, ws, msg) { 'is_team', 'is_nation_competition', 'warmup', 'warmup_ready', 'warmup_start', 'upcoming_matches_animation_speed', 'upcoming_matches_max_count', 'upcoming_matches_animation_pause', + 'self_check_in_called_overlay_duration_ms', 'ticker_enabled', 'ticker_url', 'ticker_password', 'language', 'dm_style', 'displaysettings_general', 'tabletoperator_enabled', 'tabletoperator_break_seconds', diff --git a/static/css/cmatch.css b/static/css/cmatch.css index aad4f5a..68d9880 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -415,6 +415,36 @@ match_manual_call_button, position: relative; } +.self_check_in_called_overlay { + position: fixed; + inset: 20px; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; +} + +.self_check_in_called_overlay_backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.55); + pointer-events: auto; +} + +.self_check_in_called_overlay_host { + position: relative; + z-index: 1; + width: min(1100px, calc(100vw - 80px)); + height: min(72vh, 700px); + pointer-events: auto; +} + +.self_check_in_called_overlay_card { + height: 100%; + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55); +} + .self_check_in_title { margin: 0 0 1rem; text-align: center; diff --git a/static/js/change.js b/static/js/change.js index da4cee6..79168de 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -6,6 +6,16 @@ function default_handler(rerender, special_funcs) { }; } + function _handle_announcement_event(kind, payload, announce_fn) { + if (ctournament && typeof ctournament.handle_view_announcement === 'function') { + const handled = ctournament.handle_view_announcement(kind, payload); + if (handled) { + return; + } + } + announce_fn(); + } + function _apply_tournament_field_change(field, value) { curt[field] = value; @@ -77,11 +87,11 @@ function default_handler(rerender, special_funcs) { switch (c.ctype) { case 'free_announce': - announce([c.val.text]); + _handle_announcement_event('free_announce', c.val, () => announce([c.val.text])); break; case 'emergency_announce': curt.enable_emergency = c.val; - emergency_announce(c.val); + _handle_announcement_event('emergency_announce', c.val, () => emergency_announce(c.val)); if (current_view === 'show'){ ctournament.update_emergency_btn() } @@ -213,7 +223,7 @@ function default_handler(rerender, special_funcs) { ctournament.update_location_logo(c.val.location_id, loc.logo_id, loc.logo_name); break; case 'match_preparation_call': - announcePreparationMatch(c.val.match.setup); + _handle_announcement_event('match_preparation_call', c.val, () => announcePreparationMatch(c.val.match.setup)); curt.matches.forEach((match) => { match.setup.location_id = c.val.location_id; }); @@ -221,40 +231,40 @@ function default_handler(rerender, special_funcs) { ctournament.update_upcoming_match(c); break; case 'match_called_on_court': - announceNewMatch(c.val.setup); + _handle_announcement_event('match_called_on_court', c.val, () => announceNewMatch(c.val.setup)); break; case 'begin_to_play_call': - announceBeginnToPlay(c.val.setup); + _handle_announcement_event('begin_to_play_call', c.val, () => announceBeginnToPlay(c.val.setup)); break; case 'second_call_tabletoperator': - announceSecondCallTabletoperator(c.val.setup); + _handle_announcement_event('second_call_tabletoperator', c.val, () => announceSecondCallTabletoperator(c.val.setup)); break; case 'second_call_umpire': - announceSecondCallUmpire(c.val.setup); + _handle_announcement_event('second_call_umpire', c.val, () => announceSecondCallUmpire(c.val.setup)); break; case 'second_call_servicejudge': - announceSecondCallServiceJudge(c.val.setup); + _handle_announcement_event('second_call_servicejudge', c.val, () => announceSecondCallServiceJudge(c.val.setup)); break; case 'second_call_team_one': - announceSecondCallTeamOne(c.val.setup); + _handle_announcement_event('second_call_team_one', c.val, () => announceSecondCallTeamOne(c.val.setup)); break; case 'second_call_team_two': - announceSecondCallTeamTwo(c.val.setup); + _handle_announcement_event('second_call_team_two', c.val, () => announceSecondCallTeamTwo(c.val.setup)); break; case 'second_preparation_call_tabletoperator': - announceSecondPreparationCallTabletoperator(c.val.setup); + _handle_announcement_event('second_preparation_call_tabletoperator', c.val, () => announceSecondPreparationCallTabletoperator(c.val.setup)); break; case 'second_preparation_call_umpire': - announceSecondPreparationCallUmpire(c.val.setup); + _handle_announcement_event('second_preparation_call_umpire', c.val, () => announceSecondPreparationCallUmpire(c.val.setup)); break; case 'second_preparation_call_servicejudge': - announceSecondPreparationCallServiceJudge(c.val.setup); + _handle_announcement_event('second_preparation_call_servicejudge', c.val, () => announceSecondPreparationCallServiceJudge(c.val.setup)); break; case 'second_preparation_call_team_one': - announceSecondPreparationCallTeamOne(c.val.setup); + _handle_announcement_event('second_preparation_call_team_one', c.val, () => announceSecondPreparationCallTeamOne(c.val.setup)); break; case 'second_preparation_call_team_two': - announceSecondPreparationCallTeamTwo(c.val.setup); + _handle_announcement_event('second_preparation_call_team_two', c.val, () => announceSecondPreparationCallTeamTwo(c.val.setup)); break; case 'btp_status': ctournament.btp_status_changed(c); diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 8c518df..f51a2de 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -10,6 +10,7 @@ var ci18n_de = { 'Self-Check-In': 'Self-Check-In', 'Self-Check-In: ready': 'Bereit zum Aufruf', 'Self-Check-In: waiting': 'Warten auf Check-In', + 'Self-Check-In: called': 'Aufgerufen', 'Self-Check-In: empty': 'Aktuell sind keine Paarungen in Vorbereitung.', 'Scoreboard': 'Anzeigetafel', 'Umpire Panel': 'Schiedsrichter-Panel', @@ -225,9 +226,10 @@ var ci18n_de = { 'tournament:edit:btp': 'Badminton Turnier Planer Einstellungen:', 'tournament:edit:bts': 'Badminton Turnier Server Einstellungen:', 'tournament:edit:upcoming_matches_settings': 'Spielübersichts Einstellungen', - 'tournament:edit:upcoming_matches_animation_speed': 'Animationsgeschwindigkeit beim Scrollen der Spielübersichten', + 'tournament:edit:upcoming_matches_animation_speed': 'Animationsgeschwindigkeit beim Scrollen der Spielübersichten', 'tournament:edit:upcoming_matches_animation_pause': 'Animationsunterbrechung am Anfang und Ende der Seite (sec)', 'tournament:edit:upcoming_matches_max_count': 'Maximale Anzahl von Spielen in der Spielübersicht', + 'tournament:edit:self_check_in_called_overlay_duration_ms': 'Einblenddauer der aufgerufenen Self-Check-In-Kachel (sek)', 'tournament:edit:call_preparation_matches_automatically_enabled': 'Spiele in Vorbereitung automatisch auf freien Felden aufrufen', 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Nächst mögliches Spiel automatisch in Vorbereitung aufrufen', 'tournament:edit:scoring_formats': 'Punktsysteme', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index b02494c..1b9f617 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -10,6 +10,7 @@ var ci18n_en = { 'Self-Check-In': 'Self-Check-In', 'Self-Check-In: ready': 'Ready for call', 'Self-Check-In: waiting': 'Waiting for check-in', + 'Self-Check-In: called': 'Called', 'Self-Check-In: empty': 'There are currently no matches in preparation.', 'Scoreboard': 'Scoreboard', 'Umpire Panel': 'Umpire Panel', @@ -223,9 +224,10 @@ var ci18n_en = { 'tournament:edit:btp': 'Badminton Tournament Planer Settings:', 'tournament:edit:bts': 'Badminton Tournament Server Settings:', 'tournament:edit:upcoming_matches_settings': 'Game Overview Settings', - 'tournament:edit:upcoming_matches_animation_speed': 'Animationspeed for scroll on game overviews', + 'tournament:edit:upcoming_matches_animation_speed': 'Animationspeed for scroll on game overviews', 'tournament:edit:upcoming_matches_animation_pause': 'Animation interruption at the beginning and end of the page (sec)', 'tournament:edit:upcoming_matches_max_count': 'Maximum number of games in the game overview', + 'tournament:edit:self_check_in_called_overlay_duration_ms': 'Display duration of the called self check-in card (sec)', 'tournament:edit:call_preparation_matches_automatically_enabled': 'Call games in preparation on free fields automatically', 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Automatically call up next possible game in preparation', 'tournament:edit:scoring_formats': 'Scoring formats', diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 29a3891..467ece6 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -11,6 +11,7 @@ let self_check_in_resize_frame = null; let self_check_in_measure_probe = null; let self_check_in_fit_scheduled = false; let self_check_in_fit_roots = new Set(); +let self_check_in_called_overlay_timeout = null; var ctournament = (function() { function _route_single(rex, func, handler) { @@ -1459,6 +1460,7 @@ var ctournament = (function() { input.upcoming_animation_speed = create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_animation_speed', 0, 10, 2, 1); input.upcoming_animation_pause = create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_animation_pause', 1, 20, 4, 1); input.upcoming_matches_max_count = create_numeric_input(curt, upcoming_fieldset, 'upcoming_matches_max_count', 10, 50, 15, 1); + input.self_check_in_called_overlay_duration_ms = create_duration_seconds_input(curt, upcoming_fieldset, 'self_check_in_called_overlay_duration_ms', 1, 60, 12, 0.5); } // officials_host ###################################################################################################### @@ -3743,6 +3745,25 @@ function update_officials() { return result; } + function create_duration_seconds_input(curt, parent_el, filed_id, min_seconds, max_seconds, default_seconds, step_seconds) { + const text_input = uiu.el(parent_el, 'label'); + uiu.el(text_input, 'span', {}, ci18n('tournament:edit:' + filed_id)); + const current_ms = Number(curt[filed_id]); + const value_seconds = Number.isFinite(current_ms) && current_ms > 0 ? (current_ms / 1000) : default_seconds; + const result = uiu.el(text_input, 'input', { + type: "number", + name: filed_id, + value: value_seconds, + min: min_seconds, + max: max_seconds, + step: step_seconds + }); + bind_live_prop(result, filed_id, { + get_value: input_el => Number(input_el.value) * 1000, + }); + return result; + } + function createCourtSelectBox(parentEl, parent_id, court_id) { const court_select_box = uiu.el(parentEl, 'select', { name: 'court_' + parent_id, @@ -4461,6 +4482,44 @@ function update_officials() { schedule_fit_self_check_in_cards(list); } + function show_self_check_in_called_match(match) { + const container = document.querySelector('.self_check_in_container'); + if (!container || !match || !match.setup) { + return; + } + + const existing = container.querySelector('.self_check_in_called_overlay'); + if (existing) { + existing.remove(); + } + + const overlay = uiu.el(container, 'div', 'self_check_in_called_overlay'); + const backdrop = uiu.el(overlay, 'div', 'self_check_in_called_overlay_backdrop'); + backdrop.addEventListener('click', () => overlay.remove()); + const card_host = uiu.el(overlay, 'div', 'self_check_in_called_overlay_host'); + render_self_check_in_match_card(card_host, match, false); + const card = card_host.querySelector('.self_check_in_match'); + if (card) { + card.classList.add('self_check_in_called_overlay_card'); + const status_el = card.querySelector('.self_check_in_match_status'); + if (status_el) { + status_el.textContent = ci18n('Self-Check-In: called'); + } + schedule_fit_self_check_in_cards(card); + } + + if (self_check_in_called_overlay_timeout) { + clearTimeout(self_check_in_called_overlay_timeout); + } + const overlay_duration_ms = Math.max(1000, Number(curt.self_check_in_called_overlay_duration_ms || 12000)); + self_check_in_called_overlay_timeout = setTimeout(() => { + if (overlay.isConnected) { + overlay.remove(); + } + self_check_in_called_overlay_timeout = null; + }, overlay_duration_ms); + } + function ui_upcoming() { current_view = 'upcoming'; const main = ui_match_screens('t/:key/upcoming'); @@ -4504,6 +4563,16 @@ function update_officials() { return main; } + function handle_view_announcement(kind, payload) { + if (current_view === 'self_check_in') { + if (kind === 'match_called_on_court') { + show_self_check_in_called_match(payload); + } + return true; + } + return false; + } + _route_single(/t\/([a-z0-9]+)\/upcoming/, ui_upcoming, change.default_handler(_update_all_ui_elements_upcoming, { score: update_score, court_current_match: update_upcoming_current_match, @@ -4836,6 +4905,7 @@ function update_officials() { update_show_tabletoperators, close_scoring_format_dialog_if_open, refresh_current_view, + handle_view_announcement, delete_display, }; From d95dbe480d37251ec723f3786986352d07e676cd Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Sat, 4 Apr 2026 03:00:53 +0200 Subject: [PATCH 217/224] Preserve officials during BTP sync --- bts/btp_sync.js | 204 ++++++++++++++++++++++++++++++++++++++++++--- bts/match_utils.js | 8 ++ 2 files changed, 201 insertions(+), 11 deletions(-) diff --git a/bts/btp_sync.js b/bts/btp_sync.js index b79369a..cf8df61 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -220,13 +220,13 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, st if (bm.Official1ID) { const o = get_umpire(app, tkey, officials, bm.Official1ID[0]); assert(o); - setup.umpire = o; + setup.umpire = { ...o, checked_in: false }; } if (bm.Official2ID) { const o = get_umpire(app, tkey, officials, bm.Official2ID[0]); assert(o); - setup.service_judge = o; + setup.service_judge = { ...o, checked_in: false }; } const btp_match_ids = [{ @@ -471,17 +471,36 @@ async function cleanup_entities(app, tkey, btp_state, callback) { } umpires.forEach(function (umpire) { if (btpUmpires[umpire.btp_id] === true) { - //TODO invert query return; - } else { - const mumpire_q = { _id: umpire._id }; - app.db.umpires.remove(mumpire_q, {}, (err) => { - const admin = require('./admin'); - admin.notify_change(app, tkey, 'umpire_removed', { umpire }); - return; - }); - } + const allListsNull = + !umpire.is_planed_as_umpire && + !umpire.is_planed_as_service_judge && + umpire.umpire_on_court == null && + umpire.service_judge_on_court == null && + umpire.umpire_wait == null && + umpire.service_judge_wait == null && + umpire.umpire_pause == null && + umpire.service_judge_pause == null; + + const next_inactive_list = allListsNull ? (umpire.inactive_list || Date.now()) : null; + if (umpire.inactive_list === next_inactive_list) { + return; + } + + app.db.umpires.update( + { _id: umpire._id }, + { $set: { inactive_list: next_inactive_list } }, + { returnUpdatedDocs: true }, + (err, numAffected, changed_umpire) => { + if (err) { + console.error(err); + return; + } + const admin = require('./admin'); + admin.notify_change(app, tkey, 'umpire_updated', changed_umpire); + } + ); }); }); @@ -626,6 +645,26 @@ async function integrate_matches(app, tkey, btp_state, scoring_formats, location match.setup.state = 'preparation'; } + if (cur_match.setup.umpire && !match.setup.umpire) { + match.setup.umpire = cur_match.setup.umpire; + } + + if (cur_match.setup.umpire && match.setup.umpire && + cur_match.setup.umpire.btp_id == match.setup.umpire.btp_id && + ('checked_in' in cur_match.setup.umpire)) { + match.setup.umpire.checked_in = cur_match.setup.umpire.checked_in; + } + + if (cur_match.setup.service_judge && !match.setup.service_judge) { + match.setup.service_judge = cur_match.setup.service_judge; + } + + if (cur_match.setup.service_judge && match.setup.service_judge && + cur_match.setup.service_judge.btp_id == match.setup.service_judge.btp_id && + ('checked_in' in cur_match.setup.service_judge)) { + match.setup.service_judge.checked_in = cur_match.setup.service_judge.checked_in; + } + if (cur_match.setup.tabletoperators) { // tabletoperators is not from btp so we have to coppy it to the match generated by btp. match.setup.tabletoperators = cur_match.setup.tabletoperators; @@ -835,6 +874,148 @@ async function integrate_matches(app, tkey, btp_state, scoring_formats, location }); } +async function reconcile_match_officials(app, tkey, callback) { + const admin = require('./admin'); + const stournament = require('./stournament'); + + app.db.matches.find({ tournament_key: tkey }, (err, matches) => { + if (err) { + return callback(err); + } + + app.db.umpires.find({ tournament_key: tkey }, (err2, umpires) => { + if (err2) { + return callback(err2); + } + + const byId = new Map(); + const byBtpId = new Map(); + umpires.forEach((umpire) => { + byId.set(umpire._id, umpire); + if (umpire.btp_id != null) { + byBtpId.set(String(umpire.btp_id), umpire); + } + }); + + const refs = []; + matches.forEach((match) => { + const setup = match.setup || {}; + if (setup.umpire) { + refs.push({ official: setup.umpire, role: 'umpire', match }); + } + if (setup.service_judge) { + refs.push({ official: setup.service_judge, role: 'service_judge', match }); + } + }); + + let changed = false; + + async.eachSeries(refs, ({ official, role, match }, cb) => { + const existing = + (official._id && byId.get(official._id)) || + (official.btp_id != null && byBtpId.get(String(official.btp_id))) || + null; + + const is_on_court = match.setup && match.setup.now_on_court === true; + const is_finished = typeof match.team1_won === 'boolean' || match.btp_winner || match.btp_needsync; + const planned_key = role === 'umpire' ? 'is_planed_as_umpire' : 'is_planed_as_service_judge'; + const on_court_key = role === 'umpire' ? 'umpire_on_court' : 'service_judge_on_court'; + const safe_name = official.name || [official.firstname, official.surname].filter(Boolean).join(' ').trim(); + + if (!existing) { + const new_official = { + _id: official._id || (official.btp_id != null ? `${tkey}_btp_${official.btp_id}` : `${tkey}_${role}_${match._id}`), + tournament_key: tkey, + btp_id: official.btp_id, + firstname: official.firstname || '', + surname: official.surname || '', + name: safe_name, + country: official.country || '', + status: 'ready', + is_umpire: role === 'umpire', + is_service_judge: role === 'service_judge', + is_planed_as_umpire: role === 'umpire' && !is_on_court && !is_finished, + is_planed_as_service_judge: role === 'service_judge' && !is_on_court && !is_finished, + umpire_on_court: role === 'umpire' && is_on_court ? (match.setup.court_id || true) : null, + service_judge_on_court: role === 'service_judge' && is_on_court ? (match.setup.court_id || true) : null, + umpire_wait: null, + service_judge_wait: null, + umpire_pause: null, + service_judge_pause: null, + inactive_list: null + }; + changed = true; + app.db.umpires.insert(new_official, (insertErr, inserted) => { + if (insertErr) { + return cb(insertErr); + } + byId.set(inserted._id, inserted); + if (inserted.btp_id != null) { + byBtpId.set(String(inserted.btp_id), inserted); + } + admin.notify_change(app, tkey, 'umpire_add', { umpire: inserted }); + return cb(); + }); + return; + } + + const setObj = {}; + if ((existing.name || '') !== safe_name) setObj.name = safe_name; + if ((existing.firstname || '') !== (official.firstname || '')) setObj.firstname = official.firstname || ''; + if ((existing.surname || '') !== (official.surname || '')) setObj.surname = official.surname || ''; + if ((existing.country || '') !== (official.country || '')) setObj.country = official.country || ''; + if (official.btp_id != null && existing.btp_id !== official.btp_id) setObj.btp_id = official.btp_id; + if (role === 'umpire' && existing.is_umpire !== true) setObj.is_umpire = true; + if (role === 'service_judge' && existing.is_service_judge !== true) setObj.is_service_judge = true; + + if (!is_finished) { + if (!is_on_court && existing[planned_key] !== true) { + setObj[planned_key] = true; + } + if (is_on_court && existing[on_court_key] == null) { + setObj[on_court_key] = match.setup.court_id || true; + } + if (existing.inactive_list != null) { + setObj.inactive_list = null; + } + } + + if (Object.keys(setObj).length === 0) { + return cb(); + } + + changed = true; + app.db.umpires.update( + { _id: existing._id }, + { $set: setObj }, + { returnUpdatedDocs: true }, + (updateErr, numAffected, updated) => { + if (updateErr) { + return cb(updateErr); + } + byId.set(updated._id, updated); + if (updated.btp_id != null) { + byBtpId.set(String(updated.btp_id), updated); + } + admin.notify_change(app, tkey, 'umpire_updated', updated); + return cb(); + } + ); + }, (seriesErr) => { + if (seriesErr || !changed) { + return callback(seriesErr); + } + stournament.get_umpires(app.db, tkey, (allErr, all_umpires) => { + if (!allErr) { + admin.notify_change(app, tkey, 'umpires_changed', { all_umpires }); + } + return callback(allErr); + }); + }); + }); + }); +} + function generateHallAbbreviation(name) { const wordRegex = /([A-Za-zÄÖÜäöüß0-9]+)([\s\-]*)/g; let match; @@ -1918,6 +2099,7 @@ async function sync_btp_data(app, tkey, response) { (scoring_formats, cb) => integrate_locations(app, tkey, btp_state, scoring_formats, cb), (scoring_formats, location_map, cb) => integrate_courts(app, tkey, btp_state, scoring_formats, location_map, cb), (scoring_formats, location_map, court_map, cb) => integrate_matches(app, tkey, btp_state, scoring_formats, location_map, court_map, cb), + cb => reconcile_match_officials(app, tkey, cb), cb => integrate_now_on_court(app, tkey, cb), cb => cleanup_entities(app, tkey, btp_state, cb), ], (err) => { diff --git a/bts/match_utils.js b/bts/match_utils.js index 461b178..89dcaed 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -895,8 +895,10 @@ async function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, if (cur_match.setup.umpire) { const umpire = cur_match.setup.umpire; umpire.umpire_on_court = null; + umpire.service_judge_on_court = null; umpire.is_planed_as_umpire = false; umpire.last_time_on_court_ts = end_ts; + umpire.checked_in = false; umpire.status = 'ready'; umpire.court_id = null; @@ -914,8 +916,10 @@ async function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, if (cur_match.setup.service_judge) { const service_judge = cur_match.setup.service_judge; service_judge.umpire_on_court = null; + service_judge.service_judge_on_court = null; service_judge.is_planed_as_umpire = false; service_judge.last_time_on_court_ts = end_ts; + service_judge.checked_in = false; service_judge.status = 'ready'; service_judge.court_id = null; @@ -937,8 +941,10 @@ function set_umpire_to_standby(app, tournament_key, setup) { if (setup.umpire) { const umpire = setup.umpire; umpire.umpire_on_court = null; + umpire.service_judge_on_court = null; umpire.is_planed_as_umpire = false; umpire.last_time_on_court_ts = null; + umpire.checked_in = false; umpire.status = 'standby'; umpire.court_id = null; update_umpire(app, tournament_key, umpire); @@ -947,8 +953,10 @@ function set_umpire_to_standby(app, tournament_key, setup) { if (setup.service_judge) { const service_judge = setup.service_judge; service_judge.umpire_on_court = null; + service_judge.service_judge_on_court = null; service_judge.is_planed_as_umpire = false; service_judge.last_time_on_court_ts = null; + service_judge.checked_in = false; service_judge.status = 'standby'; service_judge.court_id = null; update_umpire(app, tournament_key, service_judge); From 599cfced0b60650d59382438a0aeaa4b389a2575 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 7 Apr 2026 02:07:38 +0200 Subject: [PATCH 218/224] Harden official sync between BTS and BTP --- bts/admin.js | 1431 +++++++++++++++++++++++++++++++++++------ bts/btp_conn.js | 57 +- bts/btp_sync.js | 738 +++++++++++++++------ bts/match_utils.js | 7 +- test/test_admin.js | 75 +++ test/test_btp_sync.js | 334 ++++++++++ 6 files changed, 2235 insertions(+), 407 deletions(-) create mode 100644 test/test_admin.js diff --git a/bts/admin.js b/bts/admin.js index 01961cd..e7e37cd 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -872,33 +872,270 @@ function handle_match_edit(app, ws, msg) { }); } else { - // TODO get old setup, make sure no key has been removed - app.db.matches.update({_id: msg.id, tournament_key}, {$set: {setup}}, {returnUpdatedDocs: true}, function(err, numAffected, changed_match) { - if (err) { - ws.respond(msg, err); + app.db.matches.findOne({_id: msg.id, tournament_key}, function(old_err, old_match) { + if (old_err) { + ws.respond(msg, old_err); return; } - if (numAffected !== 1) { + if (!old_match) { ws.respond(msg, new Error('Cannot find match ' + msg.id + ' of tournament ' + tournament_key + ' in database')); return; } - if (changed_match._id !== msg.id) { - const errmsg = 'Match ' + changed_match._id + ' changed by accident, intended to change ' + msg.id + ' (old nedb version?)'; - serror.silent(errmsg); - ws.respond(msg, new Error(errmsg)); - return; - } - notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, match: changed_match}); - if (msg.btp_update) { - btp_manager.update_score(app, changed_match); + const dependent_releases = _collect_dependent_official_releases(setup); + const official_sync_meta = _build_match_edit_official_sync_meta(old_match.setup || {}, setup || {}); + const update_set = { setup }; + if (official_sync_meta.has_official_change) { + update_set.btp_needsync = true; } - ws.respond(msg, err); + app.db.matches.update({_id: msg.id, tournament_key}, {$set: update_set}, {returnUpdatedDocs: true}, function(err, numAffected, changed_match) { + if (err) { + ws.respond(msg, err); + return; + } + if (numAffected !== 1) { + ws.respond(msg, new Error('Cannot find match ' + msg.id + ' of tournament ' + tournament_key + ' in database')); + return; + } + if (changed_match._id !== msg.id) { + const errmsg = 'Match ' + changed_match._id + ' changed by accident, intended to change ' + msg.id + ' (old nedb version?)'; + serror.silent(errmsg); + ws.respond(msg, new Error(errmsg)); + return; + } + + _apply_match_edit_official_state_changes(app, tournament_key, old_match.setup || {}, changed_match.setup || {}, function(official_err) { + if (official_err) { + ws.respond(msg, official_err); + return; + } + _apply_wait_releases(app, tournament_key, dependent_releases, Date.now() / 10, function(release_err) { + if (release_err) { + ws.respond(msg, release_err); + return; + } + notify_change(app, tournament_key, 'match_edit', {match__id: msg.id, match: changed_match}); + if (msg.btp_update) { + btp_manager.update_score(app, changed_match); + } + app.db.umpires.find({ tournament_key }, function (umpire_err, all_umpires) { + if (umpire_err) { + ws.respond(msg, umpire_err); + return; + } + notify_change(app, tournament_key, 'umpires_changed', { all_umpires }); + ws.respond(msg, err); + }); + }); + }); + }); }); } }); } +function _roles_by_official_id_from_setup(setup) { + const map = new Map(); + ['umpire', 'service_judge'].forEach((role) => { + const official = setup && setup[role]; + if (official && official._id) { + if (!map.has(official._id)) { + map.set(official._id, { official, roles: new Set() }); + } + map.get(official._id).roles.add(role); + } + }); + return map; +} + +function _build_match_edit_official_sync_meta(old_setup, new_setup) { + const result = { + has_official_change: false + }; + ['umpire', 'service_judge'].forEach((role) => { + const suppressed_key = role === 'umpire' ? 'suppressed_umpire_btp_id' : 'suppressed_service_judge_btp_id'; + const old_official = old_setup && old_setup[role]; + const new_official = new_setup && new_setup[role]; + const old_btp_id = old_official && old_official.btp_id != null ? String(old_official.btp_id) : null; + const new_btp_id = new_official && new_official.btp_id != null ? String(new_official.btp_id) : null; + if (old_btp_id !== new_btp_id) { + result.has_official_change = true; + if (old_official && old_official.btp_id != null) { + new_setup[suppressed_key] = old_official.btp_id; + } + } + if (new_official && new_official.btp_id != null) { + delete new_setup[suppressed_key]; + } + }); + return result; +} + +function _collect_dependent_official_releases(setup) { + const releases = []; + if (!setup.umpire && setup.service_judge) { + const dependent = setup.service_judge; + if (dependent && dependent.btp_id != null) { + setup.suppressed_service_judge_btp_id = dependent.btp_id; + } + delete setup.service_judge; + if (dependent && dependent._id) { + releases.push({ + official_id: dependent._id, + wait_field: 'service_judge_wait', + target_position: 'front' + }); + } + } + return releases; +} + +function _remove_official_from_setup(setup, role) { + const current_btp_id = setup[role] && setup[role].btp_id != null ? setup[role].btp_id : null; + delete setup[role]; + if (current_btp_id != null) { + setup[role === 'umpire' ? 'suppressed_umpire_btp_id' : 'suppressed_service_judge_btp_id'] = current_btp_id; + } + return _collect_dependent_official_releases(setup); +} + +function _official_wait_set_obj(wait_field, ts) { + const setObj = { + inactive_list: null, + service_judge_pause: null, + umpire_pause: null, + service_judge_wait: null, + umpire_wait: null, + service_judge_on_court: null, + umpire_on_court: null, + is_planed_as_service_judge: false, + is_planed_as_umpire: false + }; + setObj[wait_field] = ts; + return setObj; +} + +function _apply_wait_releases(app, tournament_key, releases, start_ts, cb) { + if (!releases.length) { + cb(null, []); + return; + } + let index = 0; + const updated_ids = []; + const next = () => { + if (index >= releases.length) { + cb(null, updated_ids); + return; + } + const release = releases[index++]; + updated_ids.push(release.official_id); + const release_ts = release.target_position === 'front' + ? index + : (start_ts + index - 1); + app.db.umpires.update( + { _id: release.official_id, tournament_key }, + { $set: _official_wait_set_obj(release.wait_field, release_ts) }, + {}, + function (err) { + if (err) { + cb(err); + return; + } + next(); + } + ); + }; + next(); +} + +function _preferred_wait_field_from_roles(official_doc, old_roles) { + if (old_roles.has('service_judge') && !old_roles.has('umpire')) { + return 'service_judge_wait'; + } + if (old_roles.has('umpire') && !old_roles.has('service_judge')) { + return 'umpire_wait'; + } + if (official_doc && official_doc.is_umpire && !official_doc.is_service_judge) { + return 'umpire_wait'; + } + if (official_doc && official_doc.is_service_judge && !official_doc.is_umpire) { + return 'service_judge_wait'; + } + return 'umpire_wait'; +} + +function _apply_match_edit_official_state_changes(app, tournament_key, old_setup, new_setup, cb) { + const old_roles_by_id = _roles_by_official_id_from_setup(old_setup); + const new_roles_by_id = _roles_by_official_id_from_setup(new_setup); + const affected_ids = [...new Set([...old_roles_by_id.keys(), ...new_roles_by_id.keys()])]; + if (!affected_ids.length) { + cb(); + return; + } + + app.db.umpires.find({ tournament_key, _id: { $in: affected_ids } }, function (err, officials) { + if (err) { + cb(err); + return; + } + const official_by_id = new Map((officials || []).map((official) => [official._id, official])); + const updates = affected_ids.map((official_id) => { + const official_doc = official_by_id.get(official_id); + if (!official_doc) { + return null; + } + const old_roles = old_roles_by_id.get(official_id)?.roles || new Set(); + const new_roles = new_roles_by_id.get(official_id)?.roles || new Set(); + const same_roles = old_roles.size === new_roles.size && [...old_roles].every((role) => new_roles.has(role)); + if (same_roles) { + return null; + } + const setObj = { + inactive_list: null, + service_judge_pause: null, + umpire_pause: null, + service_judge_wait: null, + umpire_wait: null, + service_judge_on_court: null, + umpire_on_court: null, + is_planed_as_service_judge: new_roles.has('service_judge'), + is_planed_as_umpire: new_roles.has('umpire') + }; + if (new_roles.size === 0) { + setObj[_preferred_wait_field_from_roles(official_doc, old_roles)] = Date.now() / 10; + } + return { official_id, setObj }; + }).filter(Boolean); + + if (!updates.length) { + cb(); + return; + } + + let index = 0; + const next = () => { + if (index >= updates.length) { + cb(); + return; + } + const update = updates[index++]; + app.db.umpires.update( + { _id: update.official_id, tournament_key }, + { $set: update.setObj }, + {}, + function (update_err) { + if (update_err) { + cb(update_err); + return; + } + next(); + } + ); + }; + next(); + }); +} + @@ -933,34 +1170,43 @@ function handle_match_player_check_in (app, ws, msg) { return; } - app.db.tournaments.findOne({ key: msg.tournament_key }, async (err, tournament) => { - if (err) { - return ws.respond(msg, err); - } - - - app.db.matches.findOne({tournament_key: msg.tournament_key, _id: msg.match_id}, async (err, match) => { + update_queue.instance().execute(update_queue.named('handle_match_player_check_in', () => new Promise((resolve, reject) => { + app.db.tournaments.findOne({ key: msg.tournament_key }, async (err, tournament) => { if (err) { - return ws.respond(msg, err); + return reject(err); } - - for(const team of match.setup.teams) { - for(const player of team.players) { - if(player.btp_id == msg.player_id) { - player.checked_in = msg.checked_in; - } + app.db.matches.findOne({tournament_key: msg.tournament_key, _id: msg.match_id}, async (err, match) => { + if (err) { + return reject(err); + } + if (!match || !match.setup) { + return reject(new Error('Match not found')); } - } + let player_found = false; + for (const team of match.setup.teams) { + for (const player of team.players) { + if (player.btp_id == msg.player_id) { + player.checked_in = msg.checked_in; + player_found = true; + } + } + } + if (!player_found) { + return reject(new Error('Player not found in match')); + } - match_utils.match_update(app, match, undefined, (err) => { - ws.respond(msg, err); - return; + match_utils.match_update(app, match, undefined, (err) => { + if (err) { + return reject(err); + } + resolve(); + }); }); }); - }); + }))).then(() => ws.respond(msg)).catch((err) => ws.respond(msg, err)); } function handle_match_participant_check_in(app, ws, msg) { @@ -970,41 +1216,45 @@ function handle_match_participant_check_in(app, ws, msg) { return; } - app.db.matches.findOne({ tournament_key: msg.tournament_key, _id: msg.match_id }, async (err, match) => { - if (err) { - return ws.respond(msg, err); - } - if (!match || !match.setup) { - return ws.respond(msg, new Error('Match not found')); - } + update_queue.instance().execute(update_queue.named('handle_match_participant_check_in', () => new Promise((resolve, reject) => { + app.db.matches.findOne({ tournament_key: msg.tournament_key, _id: msg.match_id }, async (err, match) => { + if (err) { + return reject(err); + } + if (!match || !match.setup) { + return reject(new Error('Match not found')); + } - let participant_found = false; - const checked_in = !!msg.checked_in; + let participant_found = false; + const checked_in = !!msg.checked_in; - if (msg.role === 'umpire' || msg.role === 'service_judge') { - const participant = match.setup[msg.role]; - if (participant && participant.btp_id == msg.participant_id) { - participant.checked_in = checked_in; - participant_found = true; - } - } else if (msg.role === 'tabletoperator' && Array.isArray(match.setup.tabletoperators)) { - match.setup.tabletoperators.forEach((participant) => { - if (participant.btp_id == msg.participant_id) { + if (msg.role === 'umpire' || msg.role === 'service_judge') { + const participant = match.setup[msg.role]; + if (participant && participant.btp_id == msg.participant_id) { participant.checked_in = checked_in; participant_found = true; } - }); - } + } else if (msg.role === 'tabletoperator' && Array.isArray(match.setup.tabletoperators)) { + match.setup.tabletoperators.forEach((participant) => { + if (participant.btp_id == msg.participant_id) { + participant.checked_in = checked_in; + participant_found = true; + } + }); + } - if (!participant_found) { - return ws.respond(msg, new Error('Participant not found in match')); - } + if (!participant_found) { + return reject(new Error('Participant not found in match')); + } - match_utils.match_update(app, match, undefined, (update_err) => { - ws.respond(msg, update_err); - return; + match_utils.match_update(app, match, undefined, (update_err) => { + if (update_err) { + return reject(update_err); + } + resolve(); + }); }); - }); + }))).then(() => ws.respond(msg)).catch((err) => ws.respond(msg, err)); } @@ -1166,9 +1416,7 @@ function handle_official_list_move(app, ws, msg) { 'tournament_key', 'official_id', 'from_list', - 'to_list', - 'prev_btp_id', - 'next_btp_id' + 'to_list' ])) { return; } @@ -1178,23 +1426,100 @@ function handle_official_list_move(app, ws, msg) { official_id, prev_btp_id, next_btp_id, + prev_official_id, + next_official_id, + ordered_official_ids, from_list, to_list } = msg; + if (Array.isArray(ordered_official_ids) && ordered_official_ids.length > 0) { + const unique_ordered_ids = [...new Set(ordered_official_ids.filter(Boolean))]; + if (!unique_ordered_ids.includes(official_id)) { + unique_ordered_ids.push(official_id); + } + return app.db.umpires.find({ + tournament_key, + _id: { $in: unique_ordered_ids } + }, function(err, docs) { + if (err) return cerror.ws(ws, err); + const currentUmpire = docs.find((u) => u._id === official_id); + if (!currentUmpire) { + return cerror.ws(ws, new Error('current umpire not found')); + } + + const now = Date.now(); + const updates = unique_ordered_ids.map((id, index) => { + const setObj = {}; + if (id === official_id) { + setObj[from_list] = null; + setObj['inactive_list'] = null; + setObj['service_judge_pause'] = null; + setObj['umpire_pause'] = null; + setObj['service_judge_wait'] = null; + setObj['umpire_wait'] = null; + setObj['service_judge_on_court'] = null; + setObj['umpire_on_court'] = null; + setObj['is_planed_as_service_judge'] = false; + setObj['is_planed_as_umpire'] = false; + } + setObj[to_list] = now + index; + return { _id: id, setObj }; + }); + + return async.eachSeries(updates, function(entry, next) { + app.db.umpires.update( + { _id: entry._id, tournament_key }, + { $set: entry.setObj }, + {}, + next + ); + }, function(err2) { + if (err2) return cerror.ws(ws, err2); + app.db.umpires.find( + { tournament_key, _id: { $in: unique_ordered_ids } }, + function(err3, updatedOfficials) { + if (err3) return cerror.ws(ws, err3); + app.db.umpires.find({ tournament_key }, function(err4, all_umpires) { + if (err4) return cerror.ws(ws, err4); + updatedOfficials.forEach((updated) => { + notify_change(app, tournament_key, 'umpire_updated', updated); + }); + notify_change(app, tournament_key, 'umpires_changed', { all_umpires }); + notify_change(app, tournament_key, 'official_list_move', { + official_id, + from_list, + to_list, + new_ts: now + unique_ordered_ids.indexOf(official_id), + }); + ws.respond(msg); + }); + } + ); + }); + }); + } + // btp_id sicher normalisieren const prevId = (prev_btp_id == null) ? null : Number(prev_btp_id); const nextId = (next_btp_id == null) ? null : Number(next_btp_id); + const neighborOfficialIds = []; + if (prev_official_id) neighborOfficialIds.push(prev_official_id); + if (next_official_id) neighborOfficialIds.push(next_official_id); + const neighborBtpIds = []; if (Number.isFinite(prevId)) neighborBtpIds.push(prevId); if (Number.isFinite(nextId)) neighborBtpIds.push(nextId); - // Query: current über _id, prev/next über btp_id + // Query: current über _id, prev/next primär über _id, fallback über btp_id const query = { tournament_key, $or: [{ _id: official_id }] }; + if (neighborOfficialIds.length > 0) { + query.$or.push({ _id: { $in: neighborOfficialIds } }); + } if (neighborBtpIds.length > 0) { query.$or.push({ btp_id: { $in: neighborBtpIds } }); } @@ -1211,6 +1536,14 @@ function handle_official_list_move(app, ws, msg) { currentUmpire = u; continue; } + if (prev_official_id && u._id === prev_official_id) { + prevUmpire = u; + continue; + } + if (next_official_id && u._id === next_official_id) { + nextUmpire = u; + continue; + } if (Number.isFinite(prevId) && Number(u.btp_id) === prevId) { prevUmpire = u; continue; @@ -1373,6 +1706,60 @@ function handle_official_edit(app, ws, msg) { ); } +function handle_official_roles_edit(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'official_id', 'is_umpire', 'is_service_judge'])) { + return; + } + + const { tournament_key, official_id } = msg; + const setObj = { + is_umpire: !!msg.is_umpire, + is_service_judge: !!msg.is_service_judge, + updated_at: Date.now() + }; + + app.db.umpires.findOne({ _id: official_id, tournament_key }, function (err, umpire) { + if (err) { + return ws.respond(msg, err); + } + if (!umpire) { + return ws.respond( + msg, + new Error( + 'Cannot find official ' + + official_id + + ' of tournament ' + + tournament_key + + ' in database' + ) + ); + } + + app.db.umpires.update( + { _id: official_id, tournament_key }, + { $set: setObj }, + {}, + function (err2) { + if (err2) { + return ws.respond(msg, err2); + } + + app.db.umpires.findOne( + { _id: official_id, tournament_key }, + function (err3, updated) { + if (err3) { + return ws.respond(msg, err3); + } + + notify_change(app, tournament_key, 'umpire_updated', updated); + ws.respond(msg); + } + ); + } + ); + }); +} + function handle_add_officials_to_match(app, ws, msg) { // 1) Pflichtfelder prüfen if (!_require_msg(ws, msg, ['tournament_key', 'match_id'])) { @@ -1389,152 +1776,775 @@ function handle_add_officials_to_match(app, ws, msg) { firstname: u.firstname, surname: u.surname, country: u.country, + is_umpire: !!u.is_umpire, + is_service_judge: !!u.is_service_judge, checked_in: false }; } - // 2) Match laden und prüfen, ob schon Officials gesetzt sind - app.db.matches.findOne({ _id: match_id, tournament_key }, function (err, match) { - if (err) return ws.respond(msg, err); - if (!match) { - return ws.respond( - msg, - new Error('Cannot find match ' + match_id + ' of tournament ' + tournament_key + ' in database') - ); - } + update_queue.instance().execute(update_queue.named('handle_add_officials_to_match', () => new Promise((resolve, reject) => { + // 2) Match laden und prüfen, ob schon ein Umpire gesetzt ist + app.db.matches.findOne({ _id: match_id, tournament_key }, function (err, match) { + if (err) return reject(err); + if (!match) { + return reject( + new Error('Cannot find match ' + match_id + ' of tournament ' + tournament_key + ' in database') + ); + } - if (match.setup?.umpire || match.setup?.service_judge) { - return ws.respond( - msg, - new Error('Match already has assigned officials') - ); - } + if (match.setup?.umpire) { + return reject( + new Error('Match already has assigned umpire') + ); + } + + const setup = match.setup; + + // 3) Ältesten Umpire suchen + app.db.umpires + .find({ tournament_key, umpire_wait: { $ne: null } }) + .sort({ umpire_wait: 1 }) + .limit(1) + .exec(function (err2, umps) { + if (err2) return reject(err2); + if (!umps || umps.length === 0) { + return reject(new Error('No umpire available')); + } + + const umpire = umps[0]; + + // 4) Atomar reservieren (Race-Condition-sicher) + app.db.umpires.update( + { _id: umpire._id, tournament_key, umpire_wait: { $ne: null } }, + { $set: { umpire_wait: null, + is_planed_as_umpire: true } }, + {}, + function (err3, affected1) { + if (err3) return reject(err3); + if (affected1 === 0) { + return reject(new Error('Umpire was already taken by another assignment')); + } + + // 5) Match.setup updaten + setup.umpire = pack_official(umpire); + + app.db.matches.update( + { _id: match_id, tournament_key, 'setup.umpire': { $exists: false } }, + { $set: { setup, btp_needsync: true } }, + {}, + function (err4, affectedMatch) { + if (err4 || affectedMatch === 0) { + app.db.umpires.update( + { _id: umpire._id, tournament_key }, + { $set: { umpire_wait: Date.now()/10, + is_planed_as_umpire: false + } } + ); + return reject(err4 || new Error('Match changed during official assignment')); + } + + // 6) Broadcast + app.db.matches.findOne( + { _id: match_id, tournament_key }, + function (err5, updatedMatch) { + if (err5) return reject(err5); + + notify_change(app, tournament_key, 'match_edit', {match__id: msg.match_id, match: updatedMatch}); + btp_manager.update_score(app, updatedMatch); + + app.db.umpires.find( + { tournament_key, _id: umpire._id }, + function (err6, updatedOfficials) { + if (err6) { + return reject(err6); + } + if (updatedOfficials) { + for (const u of updatedOfficials) { + notify_change(app, tournament_key, 'umpire_updated', u); + } + } + app.db.umpires.find({ tournament_key }, function (err7, all_umpires) { + if (err7) { + return reject(err7); + } + notify_change(app, tournament_key, 'umpires_changed', { all_umpires }); + resolve(); + }); + } + ); + } + ); + } + ); + } + ); + }); + }); + }))).then(() => ws.respond(msg)).catch((err) => ws.respond(msg, err)); +} + +function handle_add_service_judge_to_match(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'match_id'])) { + return; + } + + const { tournament_key, match_id } = msg; + + function pack_official(u) { + return { + _id: u._id, + btp_id: u.btp_id, + name: u.name, + firstname: u.firstname, + surname: u.surname, + country: u.country, + is_umpire: !!u.is_umpire, + is_service_judge: !!u.is_service_judge, + checked_in: false + }; + } - const setup = match.setup; - - // 3) Ältesten Umpire suchen - app.db.umpires - .find({ tournament_key, umpire_wait: { $ne: null } }) - .sort({ umpire_wait: 1 }) - .limit(1) - .exec(function (err2, umps) { - if (err2) return ws.respond(msg, err2); - if (!umps || umps.length === 0) { - return ws.respond(msg, new Error('No umpire available')); + update_queue.instance().execute(update_queue.named('handle_add_service_judge_to_match', () => new Promise((resolve, reject) => { + app.db.matches.findOne({ _id: match_id, tournament_key }, function (err, match) { + if (err) return reject(err); + if (!match) { + return reject( + new Error('Cannot find match ' + match_id + ' of tournament ' + tournament_key + ' in database') + ); + } + + if (!match.setup?.umpire) { + return reject(new Error('Match has no assigned umpire')); + } + if (match.setup?.service_judge) { + return reject(new Error('Match already has assigned service judge')); + } + + const setup = match.setup; + + app.db.umpires + .find({ tournament_key, service_judge_wait: { $ne: null } }) + .sort({ service_judge_wait: 1 }) + .limit(1) + .exec(function (err2, sjs) { + if (err2) return reject(err2); + if (!sjs || sjs.length === 0) { + return reject(new Error('No service judge available')); + } + + const service_judge = sjs[0]; + + app.db.umpires.update( + { _id: service_judge._id, tournament_key, service_judge_wait: { $ne: null } }, + { $set: { service_judge_wait: null, is_planed_as_service_judge: true } }, + {}, + function (err3, affected) { + if (err3) return reject(err3); + if (affected === 0) { + return reject(new Error('Service judge was already taken')); + } + + setup.service_judge = pack_official(service_judge); + + app.db.matches.update( + { _id: match_id, tournament_key, 'setup.umpire': { $exists: true }, 'setup.service_judge': { $exists: false } }, + { $set: { setup, btp_needsync: true } }, + {}, + function (err4, affectedMatch) { + if (err4 || affectedMatch === 0) { + app.db.umpires.update( + { _id: service_judge._id, tournament_key }, + { $set: { service_judge_wait: Date.now() / 10, is_planed_as_service_judge: false } } + ); + return reject(err4 || new Error('Match changed during service judge assignment')); + } + + app.db.matches.findOne( + { _id: match_id, tournament_key }, + function (err5, updatedMatch) { + if (err5) return reject(err5); + + notify_change(app, tournament_key, 'match_edit', { match__id: msg.match_id, match: updatedMatch }); + btp_manager.update_score(app, updatedMatch); + + app.db.umpires.findOne( + { tournament_key, _id: service_judge._id }, + function (err6, updatedOfficial) { + if (err6) { + return reject(err6); + } + if (updatedOfficial) { + notify_change(app, tournament_key, 'umpire_updated', updatedOfficial); + } + app.db.umpires.find({ tournament_key }, function (err7, all_umpires) { + if (err7) { + return reject(err7); + } + notify_change(app, tournament_key, 'umpires_changed', { all_umpires }); + resolve(); + }); + } + ); + } + ); + } + ); + } + ); + }); + }); + }))).then(() => ws.respond(msg)).catch((err) => ws.respond(msg, err)); +} + +function _pack_official_for_match(u) { + return { + _id: u._id, + btp_id: u.btp_id, + name: u.name, + firstname: u.firstname, + surname: u.surname, + country: u.country, + is_umpire: !!u.is_umpire, + is_service_judge: !!u.is_service_judge, + checked_in: false + }; +} + +function handle_assign_official_to_preparation_match(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'official_id', 'match_id', 'role'])) { + return; + } + + const { tournament_key, official_id, match_id, role, source_match_id, source_type, source_role } = msg; + if (role !== 'umpire' && role !== 'service_judge') { + return cerror.ws(ws, new Error('Invalid role for assign_official_to_preparation_match: ' + role)); + } + if (source_type != null && source_type !== 'preparation' && source_type !== 'assigned') { + return cerror.ws(ws, new Error('Invalid source_type for assign_official_to_preparation_match: ' + source_type)); + } + if (source_role != null && source_role !== 'umpire' && source_role !== 'service_judge') { + return cerror.ws(ws, new Error('Invalid source_role for assign_official_to_preparation_match: ' + source_role)); + } + + const role_flag = role === 'umpire' ? 'is_planed_as_umpire' : 'is_planed_as_service_judge'; + + update_queue.instance().execute(update_queue.named('handle_assign_official_to_preparation_match', () => new Promise((resolve, reject) => { + app.db.matches.find({ tournament_key, _id: { $in: [...new Set([match_id, source_match_id].filter(Boolean))] } }, function (err, matches) { + if (err) return reject(err); + const match = matches.find((m) => m._id === match_id); + if (!match) return reject(new Error('Cannot find match ' + match_id)); + if ((match.setup || {}).state !== 'preparation') { + return reject(new Error('Match is not in preparation')); + } + const source_match = source_match_id ? matches.find((m) => m._id === source_match_id) : null; + if (source_match_id && !source_match) { + return reject(new Error('Cannot find source match ' + source_match_id)); + } + const same_match_move = !!source_match && source_match._id === match_id; + if (match.setup && match.setup[role] && (!same_match_move || source_role !== role || match.setup[role]._id !== official_id)) { + return reject(new Error('Match already has assigned ' + role)); + } + if (source_match) { + const source_setup = source_match.setup || {}; + const source_official = source_setup[source_role]; + if (!source_official || source_official._id !== official_id) { + return reject(new Error('Official is not assigned to the source match/role')); } + } - const umpire = umps[0]; + app.db.umpires.findOne({ _id: official_id, tournament_key }, function (err2, official) { + if (err2) return reject(err2); + if (!official) return reject(new Error('Cannot find official ' + official_id)); + + const target_setup = structuredClone(match.setup || {}); + const source_setup = source_match ? structuredClone(source_match.setup || {}) : null; + if (source_setup && source_role) { + const current_btp_id = source_setup[source_role] && source_setup[source_role].btp_id != null ? source_setup[source_role].btp_id : null; + delete source_setup[source_role]; + if (current_btp_id != null) { + source_setup[source_role === 'umpire' ? 'suppressed_umpire_btp_id' : 'suppressed_service_judge_btp_id'] = current_btp_id; + } + if (same_match_move) { + delete target_setup[source_role]; + if (current_btp_id != null) { + target_setup[source_role === 'umpire' ? 'suppressed_umpire_btp_id' : 'suppressed_service_judge_btp_id'] = current_btp_id; + } + } + } + target_setup[role] = _pack_official_for_match(official); + delete target_setup[role === 'umpire' ? 'suppressed_umpire_btp_id' : 'suppressed_service_judge_btp_id']; + + const officialSetObj = { + inactive_list: null, + service_judge_pause: null, + umpire_pause: null, + service_judge_wait: null, + umpire_wait: null, + service_judge_on_court: null, + umpire_on_court: null, + is_planed_as_service_judge: false, + is_planed_as_umpire: false + }; + officialSetObj[role_flag] = true; + + app.db.umpires.update( + { _id: official_id, tournament_key }, + { $set: officialSetObj }, + {}, + function (err3) { + if (err3) return reject(err3); + + const finish = () => { + app.db.umpires.findOne({ _id: official_id, tournament_key }, function (err6, updatedOfficial) { + if (err6) return reject(err6); + const match_ids = [...new Set([match_id, source_match_id].filter(Boolean))]; + app.db.matches.find({ tournament_key, _id: { $in: match_ids } }, function (err7, updatedMatches) { + if (err7) return reject(err7); + updatedMatches.forEach((updatedMatch) => { + notify_change(app, tournament_key, 'match_edit', { match__id: updatedMatch._id, match: updatedMatch }); + btp_manager.update_score(app, updatedMatch); + }); + notify_change(app, tournament_key, 'umpire_updated', updatedOfficial); + resolve(); + }); + }); + }; + + if (same_match_move) { + const same_guard = { _id: match_id, tournament_key, [`setup.${source_role}._id`]: official_id }; + if (source_role !== role) { + same_guard[`setup.${role}`] = { $exists: false }; + } + app.db.matches.update( + same_guard, + { $set: { setup: target_setup, btp_needsync: true } }, + {}, + function (err4, affected) { + if (err4) return reject(err4); + if (!affected) return reject(new Error('Match changed during official reassignment')); + finish(); + } + ); + return; + } + + const update_source = (cb) => { + if (!source_match) return cb(); + const source_guard = { _id: source_match_id, tournament_key, [`setup.${source_role}._id`]: official_id }; + app.db.matches.update( + source_guard, + { $set: { setup: source_setup, btp_needsync: true } }, + {}, + function (err4, affected) { + if (err4) return cb(err4); + if (!affected) return cb(new Error('Source match changed during official move')); + cb(); + } + ); + }; + + update_source(function (err4) { + if (err4) return reject(err4); + const guard = { _id: match_id, tournament_key }; + guard[`setup.${role}`] = { $exists: false }; + app.db.matches.update( + guard, + { $set: { setup: target_setup, btp_needsync: true } }, + {}, + function (err5, affected) { + if (err5) return reject(err5); + if (!affected) return reject(new Error('Match changed during official assignment')); + finish(); + } + ); + }); + } + ); + }); + }); + }))).then(() => ws.respond(msg)).catch((err) => cerror.ws(ws, err)); +} + +function handle_assign_official_to_match(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'official_id', 'match_id', 'role'])) { + return; + } + + const { tournament_key, official_id, match_id, role, source_match_id, source_type, source_role } = msg; + if (role !== 'umpire' && role !== 'service_judge') { + return cerror.ws(ws, new Error('Invalid role for assign_official_to_match: ' + role)); + } + if (source_type != null && source_type !== 'preparation' && source_type !== 'assigned') { + return cerror.ws(ws, new Error('Invalid source_type for assign_official_to_match: ' + source_type)); + } + if (source_role != null && source_role !== 'umpire' && source_role !== 'service_judge') { + return cerror.ws(ws, new Error('Invalid source_role for assign_official_to_match: ' + source_role)); + } + + const role_flag = role === 'umpire' ? 'is_planed_as_umpire' : 'is_planed_as_service_judge'; + + update_queue.instance().execute(update_queue.named('handle_assign_official_to_match', () => new Promise((resolve, reject) => { + app.db.matches.find({ tournament_key, _id: { $in: [...new Set([match_id, source_match_id].filter(Boolean))] } }, function (err, matches) { + if (err) return reject(err); + const match = matches.find((m) => m._id === match_id); + if (!match) return reject(new Error('Cannot find match ' + match_id)); + const state = (match.setup || {}).state; + if (state === 'preparation' || ['oncourt', 'blocked', 'finished'].includes(state)) { + return reject(new Error('Match cannot be assigned in state ' + state)); + } + const source_match = source_match_id ? matches.find((m) => m._id === source_match_id) : null; + if (source_match_id && !source_match) { + return reject(new Error('Cannot find source match ' + source_match_id)); + } + const same_match_move = !!source_match && source_match._id === match_id; + if (match.setup && match.setup[role] && (!same_match_move || source_role !== role || match.setup[role]._id !== official_id)) { + return reject(new Error('Match already has assigned ' + role)); + } + if (source_match) { + const source_setup = source_match.setup || {}; + const source_official = source_setup[source_role]; + if (!source_official || source_official._id !== official_id) { + return reject(new Error('Official is not assigned to the source match/role')); + } + } + + app.db.umpires.findOne({ _id: official_id, tournament_key }, function (err2, official) { + if (err2) return reject(err2); + if (!official) return reject(new Error('Cannot find official ' + official_id)); + + const target_setup = structuredClone(match.setup || {}); + const source_setup = source_match ? structuredClone(source_match.setup || {}) : null; + if (source_setup && source_role) { + const current_btp_id = source_setup[source_role] && source_setup[source_role].btp_id != null ? source_setup[source_role].btp_id : null; + delete source_setup[source_role]; + if (current_btp_id != null) { + source_setup[source_role === 'umpire' ? 'suppressed_umpire_btp_id' : 'suppressed_service_judge_btp_id'] = current_btp_id; + } + if (same_match_move) { + delete target_setup[source_role]; + if (current_btp_id != null) { + target_setup[source_role === 'umpire' ? 'suppressed_umpire_btp_id' : 'suppressed_service_judge_btp_id'] = current_btp_id; + } + } + } + target_setup[role] = _pack_official_for_match(official); + delete target_setup[role === 'umpire' ? 'suppressed_umpire_btp_id' : 'suppressed_service_judge_btp_id']; + + const officialSetObj = { + inactive_list: null, + service_judge_pause: null, + umpire_pause: null, + service_judge_wait: null, + umpire_wait: null, + service_judge_on_court: null, + umpire_on_court: null, + is_planed_as_service_judge: false, + is_planed_as_umpire: false + }; + officialSetObj[role_flag] = true; - // 4) Atomar reservieren (Race-Condition-sicher) app.db.umpires.update( - { _id: umpire._id, tournament_key, umpire_wait: { $ne: null } }, - { $set: { umpire_wait: null, - is_planed_as_umpire: true } }, + { _id: official_id, tournament_key }, + { $set: officialSetObj }, {}, - function (err3, affected1) { - if (err3) return ws.respond(msg, err3); - if (affected1 === 0) { - return ws.respond(msg, new Error('Umpire was already taken by another assignment')); + function (err3) { + if (err3) return reject(err3); + + const finish = () => { + app.db.umpires.findOne({ _id: official_id, tournament_key }, function (err6, updatedOfficial) { + if (err6) return reject(err6); + const match_ids = [...new Set([match_id, source_match_id].filter(Boolean))]; + app.db.matches.find({ tournament_key, _id: { $in: match_ids } }, function (err7, updatedMatches) { + if (err7) return reject(err7); + updatedMatches.forEach((updatedMatch) => { + notify_change(app, tournament_key, 'match_edit', { match__id: updatedMatch._id, match: updatedMatch }); + btp_manager.update_score(app, updatedMatch); + }); + notify_change(app, tournament_key, 'umpire_updated', updatedOfficial); + resolve(); + }); + }); + }; + + if (same_match_move) { + const same_guard = { _id: match_id, tournament_key, [`setup.${source_role}._id`]: official_id }; + if (source_role !== role) { + same_guard[`setup.${role}`] = { $exists: false }; + } + app.db.matches.update( + same_guard, + { $set: { setup: target_setup, btp_needsync: true } }, + {}, + function (err4, affected) { + if (err4) return reject(err4); + if (!affected) return reject(new Error('Match changed during official reassignment')); + finish(); + } + ); + return; } - // 5) Ältesten Service Judge suchen - app.db.umpires - .find({ tournament_key, service_judge_wait: { $ne: null } }) - .sort({ service_judge_wait: 1 }) - .limit(1) - .exec(function (err4, sjs) { - if (err4) return ws.respond(msg, err4); - if (!sjs || sjs.length === 0) { - // Rollback Umpire + const update_source = (cb) => { + if (!source_match) return cb(); + const source_guard = { _id: source_match_id, tournament_key, [`setup.${source_role}._id`]: official_id }; + app.db.matches.update( + source_guard, + { $set: { setup: source_setup, btp_needsync: true } }, + {}, + function (err4, affected) { + if (err4) return cb(err4); + if (!affected) return cb(new Error('Source match changed during official move')); + cb(); + } + ); + }; + + update_source(function (err4) { + if (err4) return reject(err4); + const guard = { _id: match_id, tournament_key }; + guard[`setup.${role}`] = { $exists: false }; + app.db.matches.update( + guard, + { $set: { setup: target_setup, btp_needsync: true } }, + {}, + function (err5, affected) { + if (err5) return reject(err5); + if (!affected) return reject(new Error('Match changed during official assignment')); + finish(); + } + ); + }); + } + ); + }); + }); + }))).then(() => ws.respond(msg)).catch((err) => cerror.ws(ws, err)); +} + +function handle_remove_official_from_preparation_match(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'official_id', 'match_id', 'role', 'to_list'])) { + return; + } + + const { tournament_key, official_id, match_id, role, to_list, ordered_official_ids } = msg; + if (role !== 'umpire' && role !== 'service_judge') { + return cerror.ws(ws, new Error('Invalid role for remove_official_from_preparation_match: ' + role)); + } + + update_queue.instance().execute(update_queue.named('handle_remove_official_from_preparation_match', () => new Promise((resolve, reject) => { + app.db.matches.findOne({ _id: match_id, tournament_key }, function (err, match) { + if (err) return reject(err); + if (!match) return reject(new Error('Cannot find match ' + match_id)); + const currentOfficial = match.setup && match.setup[role]; + if (!currentOfficial || currentOfficial._id !== official_id) { + return reject(new Error('Official is not assigned to this preparation role')); + } + + app.db.umpires.findOne({ _id: official_id, tournament_key }, function (err2, official) { + if (err2) return reject(err2); + if (!official) return reject(new Error('Cannot find official ' + official_id)); + + const setup = structuredClone(match.setup || {}); + const dependent_releases = _remove_official_from_setup(setup, role); + + const baseSetObj = { + inactive_list: null, + service_judge_pause: null, + umpire_pause: null, + service_judge_wait: null, + umpire_wait: null, + service_judge_on_court: null, + umpire_on_court: null, + is_planed_as_service_judge: false, + is_planed_as_umpire: false + }; + + app.db.matches.update( + { _id: match_id, tournament_key, [`setup.${role}._id`]: official_id }, + { $set: { setup, btp_needsync: true } }, + {}, + function (err3, affected) { + if (err3) return reject(err3); + if (!affected) return reject(new Error('Match changed during official removal')); + + const applyOfficialUpdates = (cb) => { + if (Array.isArray(ordered_official_ids) && ordered_official_ids.length > 0) { + const unique_ordered_ids = [...new Set(ordered_official_ids.filter(Boolean))]; + if (!unique_ordered_ids.includes(official_id)) { + unique_ordered_ids.push(official_id); + } + const now = Date.now(); + return async.eachSeries(unique_ordered_ids, (id, next) => { + const setObj = (id === official_id) ? { ...baseSetObj } : {}; + setObj[to_list] = now + unique_ordered_ids.indexOf(id); app.db.umpires.update( - { _id: umpire._id, tournament_key }, - { $set: { umpire_wait: Date.now(), - is_planed_as_umpire: false } } + { _id: id, tournament_key }, + { $set: setObj }, + {}, + next ); - return ws.respond(msg, new Error('No service judge available')); + }, function(series_err) { + if (series_err) return cb(series_err); + _apply_wait_releases(app, tournament_key, dependent_releases, now + unique_ordered_ids.length, cb); + }); + } + const setObj = { ...baseSetObj }; + const now = Date.now(); + setObj[to_list] = now; + app.db.umpires.update( + { _id: official_id, tournament_key }, + { $set: setObj }, + {}, + function(update_err) { + if (update_err) return cb(update_err); + _apply_wait_releases(app, tournament_key, dependent_releases, now + 1, cb); } + ); + }; + + applyOfficialUpdates(function (err4) { + if (err4) return reject(err4); + + app.db.matches.findOne({ _id: match_id, tournament_key }, function (err5, updatedMatch) { + if (err5) return reject(err5); + const affected_official_ids = [...new Set( + (Array.isArray(ordered_official_ids) ? ordered_official_ids.filter(Boolean) : []) + .concat([official_id], dependent_releases.map((release) => release.official_id)) + )]; + const officialQuery = { tournament_key, _id: { $in: affected_official_ids } }; + app.db.umpires.find(officialQuery, function (err6, updatedOfficials) { + if (err6) return reject(err6); + app.db.umpires.find({ tournament_key }, function (err7, all_umpires) { + if (err7) return reject(err7); + notify_change(app, tournament_key, 'match_edit', { match__id: match_id, match: updatedMatch }); + btp_manager.update_score(app, updatedMatch); + (updatedOfficials || []).forEach((updatedOfficial) => { + notify_change(app, tournament_key, 'umpire_updated', updatedOfficial); + }); + notify_change(app, tournament_key, 'umpires_changed', { all_umpires }); + resolve(); + }); + }); + }); + }); + } + ); + }); + }); + }))).then(() => ws.respond(msg)).catch((err) => cerror.ws(ws, err)); +} - const service_judge = sjs[0]; - - // 6) Atomar reservieren - app.db.umpires.update( - { _id: service_judge._id, tournament_key, service_judge_wait: { $ne: null } }, - { $set: { service_judge_wait: null, - is_planed_as_service_judge: true - } }, - {}, - function (err5, affected2) { - if (err5) return ws.respond(msg, err5); - if (affected2 === 0) { - // Rollback Umpire - app.db.umpires.update( - { _id: umpire._id, tournament_key }, - { $set: { umpire_wait: Date.now(), - is_planed_as_service_judge: true } } - ); - return ws.respond(msg, new Error('Service judge was already taken')); - } +function handle_remove_official_from_match(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'official_id', 'match_id', 'role', 'to_list'])) { + return; + } - // 7) Match.setup updaten - setup.umpire = pack_official(umpire); - setup.service_judge = pack_official(service_judge); - - app.db.matches.update( - { _id: match_id, tournament_key }, - { $set: {setup} }, - {}, - function (err6) { - if (err6) { - // Rollback beider Officials - app.db.umpires.update( - { _id: umpire._id, tournament_key }, - { $set: { umpire_wait: Date.now()/10, - is_planed_as_umpire: false - } } - ); - app.db.umpires.update( - { _id: service_judge._id, tournament_key }, - { $set: { service_judge_wait: Date.now()/10, - is_planed_as_service_judge: false - } } - ); - return ws.respond(msg, err6); - } + const { tournament_key, official_id, match_id, role, to_list, ordered_official_ids } = msg; + if (role !== 'umpire' && role !== 'service_judge') { + return cerror.ws(ws, new Error('Invalid role for remove_official_from_match: ' + role)); + } - // 8) Broadcast - app.db.matches.findOne( - { _id: match_id, tournament_key }, - function (err7, updatedMatch) { - if (err7) return ws.respond(msg, err7); - - notify_change(app, tournament_key, 'match_edit', {match__id: msg.match_id, match: updatedMatch}); - btp_manager.update_score(app, updatedMatch); - - // Officials ebenfalls broadcasten - app.db.umpires.find( - { tournament_key, _id: { $in: [umpire._id, service_judge._id] } }, - function (err8, updatedOfficials) { - if (!err8 && updatedOfficials) { - for (const u of updatedOfficials) { - notify_change(app, tournament_key, 'umpire_updated', u); - } - } - - // 9) Erfolg - return ws.respond(msg); - } - ); - } - ); - } - ); - } - ); + update_queue.instance().execute(update_queue.named('handle_remove_official_from_match', () => new Promise((resolve, reject) => { + app.db.matches.findOne({ _id: match_id, tournament_key }, function (err, match) { + if (err) return reject(err); + if (!match) return reject(new Error('Cannot find match ' + match_id)); + const currentOfficial = match.setup && match.setup[role]; + if (!currentOfficial || currentOfficial._id !== official_id) { + return reject(new Error('Official is not assigned to this role')); + } + + app.db.umpires.findOne({ _id: official_id, tournament_key }, function (err2, official) { + if (err2) return reject(err2); + if (!official) return reject(new Error('Cannot find official ' + official_id)); + + const setup = structuredClone(match.setup || {}); + const dependent_releases = _remove_official_from_setup(setup, role); + + const baseSetObj = { + inactive_list: null, + service_judge_pause: null, + umpire_pause: null, + service_judge_wait: null, + umpire_wait: null, + service_judge_on_court: null, + umpire_on_court: null, + is_planed_as_service_judge: false, + is_planed_as_umpire: false + }; + + app.db.matches.update( + { _id: match_id, tournament_key, [`setup.${role}._id`]: official_id }, + { $set: { setup, btp_needsync: true } }, + {}, + function (err3, affected) { + if (err3) return reject(err3); + if (!affected) return reject(new Error('Match changed during official removal')); + + const applyOfficialUpdates = (cb) => { + if (Array.isArray(ordered_official_ids) && ordered_official_ids.length > 0) { + const unique_ordered_ids = [...new Set(ordered_official_ids.filter(Boolean))]; + if (!unique_ordered_ids.includes(official_id)) { + unique_ordered_ids.push(official_id); + } + const now = Date.now(); + return async.eachSeries(unique_ordered_ids, (id, next) => { + const setObj = (id === official_id) ? { ...baseSetObj } : {}; + setObj[to_list] = now + unique_ordered_ids.indexOf(id); + app.db.umpires.update( + { _id: id, tournament_key }, + { $set: setObj }, + {}, + next + ); + }, function(series_err) { + if (series_err) return cb(series_err); + _apply_wait_releases(app, tournament_key, dependent_releases, now + unique_ordered_ids.length, cb); + }); + } + const setObj = { ...baseSetObj }; + const now = Date.now(); + setObj[to_list] = now; + app.db.umpires.update( + { _id: official_id, tournament_key }, + { $set: setObj }, + {}, + function(update_err) { + if (update_err) return cb(update_err); + _apply_wait_releases(app, tournament_key, dependent_releases, now + 1, cb); + } + ); + }; + + applyOfficialUpdates(function (err4) { + if (err4) return reject(err4); + + app.db.matches.findOne({ _id: match_id, tournament_key }, function (err5, updatedMatch) { + if (err5) return reject(err5); + const affected_official_ids = [...new Set( + (Array.isArray(ordered_official_ids) ? ordered_official_ids.filter(Boolean) : []) + .concat([official_id], dependent_releases.map((release) => release.official_id)) + )]; + const officialQuery = { tournament_key, _id: { $in: affected_official_ids } }; + app.db.umpires.find(officialQuery, function (err6, updatedOfficials) { + if (err6) return reject(err6); + app.db.umpires.find({ tournament_key }, function (err7, all_umpires) { + if (err7) return reject(err7); + notify_change(app, tournament_key, 'match_edit', { match__id: match_id, match: updatedMatch }); + btp_manager.update_score(app, updatedMatch); + (updatedOfficials || []).forEach((updatedOfficial) => { + notify_change(app, tournament_key, 'umpire_updated', updatedOfficial); + }); + notify_change(app, tournament_key, 'umpires_changed', { all_umpires }); + resolve(); + }); + }); }); + }); } ); }); - }); + }); + }))).then(() => ws.respond(msg)).catch((err) => cerror.ws(ws, err)); } @@ -1715,13 +2725,46 @@ function handle_ticker_reset(app, ws, msg) { } const all_admins = []; +function _notify_queue_hang(payload) { + for (const admin_ws of all_admins) { + admin_ws.sendmsg({ + type: 'change', + tournament_key: admin_ws.last_tournament_key || 'default', + ctype: 'queue_hang_warning', + val: payload, + }); + } +} function notify_change(app, tournament_key, ctype, val) { + let payload = val; + const announcement_change_types = new Set([ + 'match_preparation_call', + 'match_called_on_court', + 'begin_to_play_call', + 'second_call_tabletoperator', + 'second_preparation_call_tabletoperator', + 'second_call_umpire', + 'second_preparation_call_umpire', + 'second_call_servicejudge', + 'second_preparation_call_servicejudge', + 'second_call_team_one', + 'second_preparation_call_team_one', + 'second_call_team_two', + 'second_preparation_call_team_two', + 'free_announce', + ]); + if (payload && typeof payload === 'object' && announcement_change_types.has(ctype) && payload._announcement_ts == null) { + payload = { + ...payload, + _announcement_ts: Date.now(), + }; + } for (const admin_ws of all_admins) { admin_ws.sendmsg({ type: 'change', tournament_key, ctype, - val, + val: payload, }); } } @@ -1750,6 +2793,7 @@ function handle_fetch_allscoresheets_data(app, ws, msg) { function on_connect(app, ws) { all_admins.push(ws); + update_queue.instance().set_hang_reporter(_notify_queue_hang); } function on_close(app, ws) { @@ -1889,7 +2933,13 @@ module.exports = { handle_emergency_announce, handle_official_list_move, handle_official_edit, + handle_official_roles_edit, handle_add_officials_to_match, + handle_add_service_judge_to_match, + handle_assign_official_to_match, + handle_assign_official_to_preparation_match, + handle_remove_official_from_match, + handle_remove_official_from_preparation_match, handle_second_call_umpire, handle_second_preparation_call_umpire, handle_second_call_servicejudge, @@ -1914,4 +2964,7 @@ module.exports = { generate_tournament_web_url, on_close, on_connect, + _build_match_edit_official_sync_meta, + _collect_dependent_official_releases, + _remove_official_from_setup, }; diff --git a/bts/btp_conn.js b/bts/btp_conn.js index 8201c0e..ad65f3b 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -229,38 +229,63 @@ class BTPConn { async.waterfall([ (cb) => { - if (!match.setup || !match.setup.umpire || !match.setup.umpire.name) { + if (!match.setup) { return cb(null, null, null); } - this.app.db.umpires.findOne({ - name: match.setup.umpire.name, - tournament_key: this.tkey, - }, (err, umpire) => { - if (err) { - return cb(err); - } + const packed_umpire = match.setup.umpire; + const packed_service_judge = match.setup.service_judge; + const packed_umpire_btp_id = packed_umpire && packed_umpire.btp_id != null ? packed_umpire.btp_id : null; + const packed_service_judge_btp_id = packed_service_judge && packed_service_judge.btp_id != null ? packed_service_judge.btp_id : null; - const umpire_btp_id = umpire ? umpire.btp_id : null; - if (!match.setup.service_judge || !match.setup.service_judge.name) { - return cb(null, umpire_btp_id, null); + if (packed_umpire_btp_id != null && (packed_service_judge_btp_id != null || !packed_service_judge || !packed_service_judge.name)) { + return cb(null, packed_umpire_btp_id, packed_service_judge_btp_id); + } + + if (!packed_umpire || !packed_umpire.name) { + if (packed_service_judge_btp_id != null) { + return cb(null, null, packed_service_judge_btp_id); } + if (!packed_service_judge || !packed_service_judge.name) { + return cb(null, null, null); + } + } + const resolveOfficialBtpId = (packed_official, done) => { + if (!packed_official) { + return done(null, null); + } + if (packed_official.btp_id != null) { + return done(null, packed_official.btp_id); + } + if (!packed_official.name) { + return done(null, null); + } this.app.db.umpires.findOne({ - name: match.setup.service_judge.name, + name: packed_official.name, tournament_key: this.tkey, - }, (err, service_judge) => { + }, (err, official) => { if (err) { - return cb(err); + return done(err); } + return done(null, official ? official.btp_id : null); + }); + }; - const service_judge_btp_id = service_judge ? service_judge.btp_id : null; + resolveOfficialBtpId(packed_umpire, (err, umpire_btp_id) => { + if (err) { + return cb(err); + } + resolveOfficialBtpId(packed_service_judge, (err2, service_judge_btp_id) => { + if (err2) { + return cb(err2); + } return cb(null, umpire_btp_id, service_judge_btp_id); }); }); }, (umpire_btp_id, service_judge_btp_id, cb) => { - if (!match.setup || !match.setup.court_id || match.setup.now_on_court !== true) { + if (!match.setup || !match.setup.court_id) { return cb(null, umpire_btp_id, service_judge_btp_id, null); } diff --git a/bts/btp_sync.js b/bts/btp_sync.js index cf8df61..20139ee 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -218,15 +218,18 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, st }); } if (bm.Official1ID) { - const o = get_umpire(app, tkey, officials, bm.Official1ID[0]); - assert(o); - setup.umpire = { ...o, checked_in: false }; - + const official_id = bm.Official1ID[0]; + const o = get_umpire(app, tkey, officials, official_id) || build_fallback_official(official_id, tkey); + if (o) { + setup.umpire = { ...o, checked_in: false }; + } } if (bm.Official2ID) { - const o = get_umpire(app, tkey, officials, bm.Official2ID[0]); - assert(o); - setup.service_judge = { ...o, checked_in: false }; + const official_id = bm.Official2ID[0]; + const o = get_umpire(app, tkey, officials, official_id) || build_fallback_official(official_id, tkey); + if (o) { + setup.service_judge = { ...o, checked_in: false }; + } } const btp_match_ids = [{ @@ -275,6 +278,107 @@ function findDefaultScoringFormat(scoringFormatMap) { return null; } +function mergeLocalMatchIntoBtpMatch(current_match, match) { + if (current_match.team1_won === null) { + current_match.team1_won = undefined; + } + + if (current_match.btp_winner) { + match.setup.state = 'finished'; + } + if (typeof current_match.team1_won === 'boolean' || current_match.btp_winner || current_match.btp_needsync) { + match.setup.now_on_court = false; + match.setup.state = 'finished'; + } else if (current_match.setup.now_on_court === true) { + // Keep the local on-court state until the result is explicitly confirmed. + match.setup.now_on_court = true; + if (current_match.setup.state === 'blocked') { + match.setup.state = 'blocked'; + } else if (current_match.setup.called_timestamp) { + match.setup.state = 'oncourt'; + } + } + + if (!match.setup.court_id && current_match.setup && current_match.setup.court_id) { + match.setup.court_id = current_match.setup.court_id; + } + + if (!match.network_score && current_match.network_score) { + match.network_score = current_match.network_score; + } + + if (current_match.setup.called_timestamp) { + match.setup.called_timestamp = current_match.setup.called_timestamp; + } + + if (current_match.setup.preparation_call_timestamp) { + match.setup.preparation_call_timestamp = current_match.setup.preparation_call_timestamp; + match.setup.state = 'preparation'; + } + + const suppression_active = current_match.btp_needsync === true; + const suppressed_umpire_btp_id = suppression_active ? current_match.setup.suppressed_umpire_btp_id : null; + const suppressed_service_judge_btp_id = suppression_active ? current_match.setup.suppressed_service_judge_btp_id : null; + if (suppressed_umpire_btp_id != null) { + if (match.setup.umpire && String(match.setup.umpire.btp_id) === String(suppressed_umpire_btp_id)) { + delete match.setup.umpire; + match.setup.suppressed_umpire_btp_id = suppressed_umpire_btp_id; + } + } + if (suppressed_service_judge_btp_id != null) { + if (match.setup.service_judge && String(match.setup.service_judge.btp_id) === String(suppressed_service_judge_btp_id)) { + delete match.setup.service_judge; + match.setup.suppressed_service_judge_btp_id = suppressed_service_judge_btp_id; + } + } + + if (current_match.btp_needsync === true && current_match.setup.umpire && !match.setup.umpire && suppressed_umpire_btp_id == null) { + match.setup.umpire = current_match.setup.umpire; + } + + if (current_match.setup.umpire && match.setup.umpire && + current_match.setup.umpire.btp_id == match.setup.umpire.btp_id && + ('checked_in' in current_match.setup.umpire)) { + match.setup.umpire.checked_in = current_match.setup.umpire.checked_in; + } + + if (current_match.btp_needsync === true && current_match.setup.service_judge && !match.setup.service_judge && suppressed_service_judge_btp_id == null) { + match.setup.service_judge = current_match.setup.service_judge; + } + + if (current_match.setup.service_judge && match.setup.service_judge && + current_match.setup.service_judge.btp_id == match.setup.service_judge.btp_id && + ('checked_in' in current_match.setup.service_judge)) { + match.setup.service_judge.checked_in = current_match.setup.service_judge.checked_in; + } + + if (current_match.setup.tabletoperators) { + match.setup.tabletoperators = current_match.setup.tabletoperators; + } + + for (let team_index = 0; team_index < Math.min(current_match.setup.teams.length, match.setup.teams.length); team_index++) { + for (let player_index = 0; player_index < Math.min(current_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { + if (current_match.setup.teams[team_index].players[player_index].now_playing_on_court != undefined) { + match.setup.teams[team_index].players[player_index].now_playing_on_court = current_match.setup.teams[team_index].players[player_index].now_playing_on_court; + } + + if (current_match.setup.teams[team_index].players[player_index].now_tablet_on_court != undefined) { + match.setup.teams[team_index].players[player_index].now_tablet_on_court = current_match.setup.teams[team_index].players[player_index].now_tablet_on_court; + } + + if (current_match.setup.teams[team_index].players[player_index].tablet_break_active != undefined) { + match.setup.teams[team_index].players[player_index].tablet_break_active = current_match.setup.teams[team_index].players[player_index].tablet_break_active; + } + + if (current_match.setup.teams[team_index].players[player_index].checked_in != undefined) { + match.setup.teams[team_index].players[player_index].checked_in = current_match.setup.teams[team_index].players[player_index].checked_in; + } + } + } + + return match; +} + function _craft_team(par) { if (!par) { return { players: [] }; @@ -516,6 +620,83 @@ function calculate_btp_match_id(tkey, bm, draws, events) { return tkey + '_' + discipline_name + '_' + bm.ID[0]; } +function build_match_update_fields(match) { + return { + btp_match_ids: match.btp_match_ids, + btp_player_ids: match.btp_player_ids, + setup: match.setup, + team1_won: match.team1_won, + btp_winner: match.btp_winner, + btp_needsync: match.btp_needsync, + network_score: match.network_score, + network_team1_left: match.network_team1_left, + network_team1_serving: match.network_team1_serving, + network_teams_player1_even: match.network_teams_player1_even, + presses: match.presses, + duration_ms: match.duration_ms, + end_ts: match.end_ts, + shuttle_count: match.shuttle_count, + match_order: match.match_order, + }; +} + +function build_match_update_operations(current_match, next_match) { + const current_fields = build_match_update_fields(current_match); + const next_fields = build_match_update_fields(next_match); + const setObj = {}; + const unsetObj = {}; + + function append_update_ops(current_value, next_value, path) { + if (utils.deep_equal(current_value, next_value)) { + return; + } + + if (next_value === undefined) { + unsetObj[path] = true; + return; + } + + if (current_value === undefined) { + setObj[path] = next_value; + return; + } + + const current_is_array = Array.isArray(current_value); + const next_is_array = Array.isArray(next_value); + if (current_is_array || next_is_array) { + if (!current_is_array || !next_is_array || current_value.length !== next_value.length) { + setObj[path] = next_value; + return; + } + for (let i = 0; i < next_value.length; i++) { + append_update_ops(current_value[i], next_value[i], `${path}.${i}`); + } + return; + } + + const current_is_object = current_value && typeof current_value === 'object'; + const next_is_object = next_value && typeof next_value === 'object'; + if (current_is_object && next_is_object) { + const keys = new Set([...Object.keys(current_value), ...Object.keys(next_value)]); + keys.forEach((key) => append_update_ops(current_value[key], next_value[key], `${path}.${key}`)); + return; + } + + setObj[path] = next_value; + } + + Object.keys(next_fields).forEach((key) => append_update_ops(current_fields[key], next_fields[key], key)); + + const update = {}; + if (Object.keys(setObj).length > 0) { + update.$set = setObj; + } + if (Object.keys(unsetObj).length > 0) { + update.$unset = unsetObj; + } + return update; +} + function get_umpires(app, tkey) { return new Promise((resolve, reject) => { @@ -532,13 +713,26 @@ function get_umpires(app, tkey) { function get_umpire(app, tkey, umpires , btp_id) { var returnValue = null; umpires.forEach((umpire) => { - if (umpire.btp_id === btp_id) { + if (umpire.btp_id != null && String(umpire.btp_id) === String(btp_id)) { returnValue = umpire; } }); return returnValue; } +function build_fallback_official(official_id, tkey) { + return { + _id: `${tkey}_btp_${official_id}`, + tournament_key: tkey, + btp_id: official_id, + firstname: '', + surname: '', + name: `BTP Official ${official_id}`, + country: '', + status: 'ready' + }; +} + async function integrate_matches(app, tkey, btp_state, scoring_formats, location_map, court_map, callback) { const admin = require('./admin'); // avoid dependency cycle const match_utils = require('./match_utils'); @@ -605,205 +799,137 @@ async function integrate_matches(app, tkey, btp_state, scoring_formats, location } if (cur_match) { - if (cur_match.team1_won === null) { - cur_match.team1_won = undefined; - } - - if (cur_match.btp_winner) { - match.setup.state = 'finished'; - } - if (typeof cur_match.team1_won === 'boolean' || cur_match.btp_winner || cur_match.btp_needsync) { - match.setup.now_on_court = false; - match.setup.state = 'finished'; - } else if (cur_match.setup.now_on_court === true) { - // Keep the local on-court state until the result is explicitly confirmed. - match.setup.now_on_court = true; - if (cur_match.setup.state === 'blocked') { - match.setup.state = 'blocked'; - } else if (cur_match.setup.called_timestamp) { - match.setup.state = 'oncourt'; + app.db.matches.findOne({ _id: cur_match._id, tournament_key: tkey }, (err, latest_match) => { + if (err) { + cb(err); + return; } - } - - if (!match.network_score && cur_match.network_score) { - match.network_score = cur_match.network_score; - } - - if (cur_match.setup.called_timestamp) { - // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. - match.setup.called_timestamp = cur_match.setup.called_timestamp; - } - if (cur_match.setup.called_timestamp) { - // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. - match.setup.called_timestamp = cur_match.setup.called_timestamp; - } - + const current_match = latest_match || cur_match; - if (cur_match.setup.preparation_call_timestamp) { - // The called_timestamp is not from btp so we have to coppy it to the match generated by btp. - match.setup.preparation_call_timestamp = cur_match.setup.preparation_call_timestamp; - match.setup.state = 'preparation'; - } - - if (cur_match.setup.umpire && !match.setup.umpire) { - match.setup.umpire = cur_match.setup.umpire; - } + match = mergeLocalMatchIntoBtpMatch(current_match, match); - if (cur_match.setup.umpire && match.setup.umpire && - cur_match.setup.umpire.btp_id == match.setup.umpire.btp_id && - ('checked_in' in cur_match.setup.umpire)) { - match.setup.umpire.checked_in = cur_match.setup.umpire.checked_in; - } + for (let team_index = 0; team_index < Math.min(current_match.setup.teams.length, match.setup.teams.length); team_index++) { + for (let player_index = 0; player_index < Math.min(current_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { - if (cur_match.setup.service_judge && !match.setup.service_judge) { - match.setup.service_judge = cur_match.setup.service_judge; - } - - if (cur_match.setup.service_judge && match.setup.service_judge && - cur_match.setup.service_judge.btp_id == match.setup.service_judge.btp_id && - ('checked_in' in cur_match.setup.service_judge)) { - match.setup.service_judge.checked_in = cur_match.setup.service_judge.checked_in; - } - - if (cur_match.setup.tabletoperators) { - // tabletoperators is not from btp so we have to coppy it to the match generated by btp. - match.setup.tabletoperators = cur_match.setup.tabletoperators; - } + if (current_match.setup.teams[team_index].players[player_index].last_time_on_court_ts || match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { + const current_ts = current_match.setup.teams[team_index].players[player_index].last_time_on_court_ts || 0; + const next_ts = match.setup.teams[team_index].players[player_index].last_time_on_court_ts || 0; + const max_ts = Math.max(current_ts, next_ts); + current_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; + match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; + } + } + } - for (let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { - for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { + match.btp_needsync = current_match.btp_needsync; + match.network_team1_left = current_match.network_team1_left; + match.network_team1_serving = current_match.network_team1_serving; + match.network_teams_player1_even = current_match.network_teams_player1_even; + match.presses = current_match.presses; + match.duration_ms = current_match.duration_ms; + match.end_ts = current_match.end_ts; + + if (match.setup.now_on_court === false) { + if (current_match.setup.warmup) { + match.setup.warmup = current_match.setup.warmup; + } - if (cur_match.setup.teams[team_index].players[player_index].now_playing_on_court != undefined) { - match.setup.teams[team_index].players[player_index].now_playing_on_court = cur_match.setup.teams[team_index].players[player_index].now_playing_on_court; + if (current_match.setup.warmup_ready) { + match.setup.warmup_ready = current_match.setup.warmup_ready; } - if (cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court != undefined) { - match.setup.teams[team_index].players[player_index].now_tablet_on_court = cur_match.setup.teams[team_index].players[player_index].now_tablet_on_court; + if (current_match.setup.warmup_start) { + match.setup.warmup_start = current_match.setup.warmup_start; } + } - if (cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts || match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { - if (!cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { - cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; + for (let team_index = 0; team_index < Math.min(current_match.setup.teams.length, match.setup.teams.length); team_index++) { + for (let player_index = 0; player_index < Math.min(current_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { + if ('tablet_break_active' in current_match.setup.teams[team_index].players[player_index]) { + match.setup.teams[team_index].players[player_index].tablet_break_active = current_match.setup.teams[team_index].players[player_index].tablet_break_active; } + } + } - if (!match.setup.teams[team_index].players[player_index].last_time_on_court_ts) { - match.setup.teams[team_index].players[player_index].last_time_on_court_ts = 0; - } + if (utils.plucked_deep_equal(match, current_match, Object.keys(match), true)) { + cb(null); + return; + } - let max_ts = Math.max(cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts, - match.setup.teams[team_index].players[player_index].last_time_on_court_ts); + let only_change_check_in = false; + let result_enterd_in_btp = false; + let match_player_changed = false; - cur_match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; - match.setup.teams[team_index].players[player_index].last_time_on_court_ts = max_ts; + for (let team_index = 0; team_index < Math.min(current_match.setup.teams.length, match.setup.teams.length); team_index++) { + if(current_match.setup.teams[team_index].players.length < match.setup.teams[team_index].players.length){ + for (let player_index = 0; player_index < match.setup.teams[team_index].players.length; player_index++) { + match_player_changed = true; + } + } + for (let player_index = 0; player_index < Math.min(current_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { + current_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; + if(match.setup.teams[team_index].players[player_index].btp_id != current_match.setup.teams[team_index].players[player_index].btp_id) { + match_player_changed = true; + } } } - } - - match.btp_needsync = cur_match.btp_needsync; - match.network_team1_left = cur_match.network_team1_left; - match.network_team1_serving = cur_match.network_team1_serving; - match.network_teams_player1_even = cur_match.network_teams_player1_even; - match.presses = cur_match.presses; - match.duration_ms = cur_match.duration_ms; - match.end_ts = cur_match.end_ts; + if (!current_match.team1_won && current_match.team1_won != match.team1_won) { + if (!match.end_ts) { + result_enterd_in_btp = true; + match.setup.warmup = 'none'; + match.end_ts = Date.now(); - if (match.setup.now_on_court === false) { - if (cur_match.setup.warmup) { - match.setup.warmup = cur_match.setup.warmup; + app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { + if (err) { + return callback(err); + } + if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { + const match_utils = require('./match_utils'); + match_utils.reset_player_tabletoperator(app, tkey, match._id, match.end_ts); + } + }); + } } - if (cur_match.setup.warmup_ready) { - match.setup.warmup_ready = cur_match.setup.warmup_ready; + if (utils.plucked_deep_equal(match, current_match, Object.keys(match), true)) { + only_change_check_in = true; } - if (cur_match.setup.warmup_start) { - match.setup.warmup_start = cur_match.setup.warmup_start; + if(match_player_changed) { + matches_player_changed.push(match); } - } - for (let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { - for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { - if ('tablet_break_active' in cur_match.setup.teams[team_index].players[player_index]) { - match.setup.teams[team_index].players[player_index].tablet_break_active = cur_match.setup.teams[team_index].players[player_index].tablet_break_active; - } + const update_ops = build_match_update_operations(current_match, match); + if (Object.keys(update_ops).length === 0) { + cb(null); + return; } - } - if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { - // No update required - cb(null); - return; - } - // equals checked_in changed and check if it was the only change - let only_change_check_in = false; - let result_enterd_in_btp = false; - let match_player_changed = false; - - for (let team_index = 0; team_index < Math.min(cur_match.setup.teams.length, match.setup.teams.length); team_index++) { - if(cur_match.setup.teams[team_index].players.length < match.setup.teams[team_index].players.length){ - for (let player_index = 0; player_index < match.setup.teams[team_index].players.length; player_index++) { - match_player_changed = true; + app.db.matches.update({ _id: current_match._id }, update_ops, {}, (err) => { + if (err) { + cb(err); + return; } - } - for (let player_index = 0; player_index < Math.min(cur_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { - cur_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; - if(match.setup.teams[team_index].players[player_index].btp_id != cur_match.setup.teams[team_index].players[player_index].btp_id) { - match_player_changed = true; - } - } - } - if (!cur_match.team1_won && cur_match.team1_won != match.team1_won) { - if (!match.end_ts) { - result_enterd_in_btp = true; - match.setup.warmup = 'none'; - match.end_ts = Date.now(); - - app.db.tournaments.findOne({ key: tkey }, async (err, tournament) => { - if (err) { - return callback(err); - } - if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { - const match_utils = require('./match_utils'); - match_utils.reset_player_tabletoperator(app, tkey, match._id, match.end_ts); + if (match.setup.is_match) { + if (!only_change_check_in || result_enterd_in_btp) { + changes = true; + admin.notify_change(app, match.tournament_key, 'match_edit', { + match__id: match._id, + match: match + }); + } else { + admin.notify_change(app, match.tournament_key, 'update_player_status', { + match__id: match._id, + btp_winner: match.btp_winner, + setup: match.setup + }); } - }); - } - } - - if (utils.plucked_deep_equal(match, cur_match, Object.keys(match), true)) { - only_change_check_in = true; - } - - if(match_player_changed) { - matches_player_changed.push(match); - } - - app.db.matches.update({ _id: cur_match._id }, { $set: match }, {}, (err) => { - if (err) { - cb(err); - return; - }; - - // render onli if is_match flag is set. else it's nessasary to have the game (it's a link) in the db, but not to rerender - if (match.setup.is_match) { - if (!only_change_check_in || result_enterd_in_btp) { - changes = true; - admin.notify_change(app, match.tournament_key, 'match_edit', { - match__id: match._id, - match: match - }); - } else { - admin.notify_change(app, match.tournament_key, 'update_player_status', { - match__id: match._id, - btp_winner: match.btp_winner, - setup: match.setup - }); } - } + cb(null); + }); }); - cb(null); return; } changes = true; @@ -965,8 +1091,6 @@ async function reconcile_match_officials(app, tkey, callback) { if ((existing.surname || '') !== (official.surname || '')) setObj.surname = official.surname || ''; if ((existing.country || '') !== (official.country || '')) setObj.country = official.country || ''; if (official.btp_id != null && existing.btp_id !== official.btp_id) setObj.btp_id = official.btp_id; - if (role === 'umpire' && existing.is_umpire !== true) setObj.is_umpire = true; - if (role === 'service_judge' && existing.is_service_judge !== true) setObj.is_service_judge = true; if (!is_finished) { if (!is_on_court && existing[planned_key] !== true) { @@ -975,6 +1099,18 @@ async function reconcile_match_officials(app, tkey, callback) { if (is_on_court && existing[on_court_key] == null) { setObj[on_court_key] = match.setup.court_id || true; } + if (existing.umpire_wait != null) { + setObj.umpire_wait = null; + } + if (existing.service_judge_wait != null) { + setObj.service_judge_wait = null; + } + if (existing.umpire_pause != null) { + setObj.umpire_pause = null; + } + if (existing.service_judge_pause != null) { + setObj.service_judge_pause = null; + } if (existing.inactive_list != null) { setObj.inactive_list = null; } @@ -1822,6 +1958,140 @@ function pause_is_done(match, team_nr, player_nr, btp_settings) { return; } +function buildOfficialReferenceState(matches) { + const referenced_ids = new Set(); + const referenced_btp_ids = new Set(); + const planned_umpire_ids = new Set(); + const planned_umpire_btp_ids = new Set(); + const planned_service_judge_ids = new Set(); + const planned_service_judge_btp_ids = new Set(); + const on_court_umpire_ids = new Set(); + const on_court_umpire_btp_ids = new Set(); + const on_court_service_judge_ids = new Set(); + const on_court_service_judge_btp_ids = new Set(); + + (matches || []).forEach((match) => { + const setup = match.setup || {}; + const is_finished = typeof match.team1_won === 'boolean' || match.btp_winner || match.btp_needsync; + const is_on_court = setup.now_on_court === true; + const is_planned = !is_finished && !is_on_court && !!setup.state; + + const addRef = (official, idSet, btpSet) => { + if (!official) return; + if (official._id) idSet.add(String(official._id)); + if (official.btp_id != null) btpSet.add(String(official.btp_id)); + }; + + if (!is_finished) { + [setup.umpire, setup.service_judge].forEach((official) => { + addRef(official, referenced_ids, referenced_btp_ids); + }); + } + + if (is_on_court) { + addRef(setup.umpire, on_court_umpire_ids, on_court_umpire_btp_ids); + addRef(setup.service_judge, on_court_service_judge_ids, on_court_service_judge_btp_ids); + } else if (is_planned) { + addRef(setup.umpire, planned_umpire_ids, planned_umpire_btp_ids); + addRef(setup.service_judge, planned_service_judge_ids, planned_service_judge_btp_ids); + } + }); + + return { + referenced_ids, + referenced_btp_ids, + planned_umpire_ids, + planned_umpire_btp_ids, + planned_service_judge_ids, + planned_service_judge_btp_ids, + on_court_umpire_ids, + on_court_umpire_btp_ids, + on_court_service_judge_ids, + on_court_service_judge_btp_ids, + }; +} + +function computeOfficialVisibilityPatch(official, refState) { + const hasId = (set) => set.has(String(official._id)); + const hasBtpId = (set) => official.btp_id != null && set.has(String(official.btp_id)); + const inSet = (ids, btpIds) => hasId(ids) || hasBtpId(btpIds); + + const in_active_list = + official.umpire_wait != null || + official.service_judge_wait != null || + official.umpire_pause != null || + official.service_judge_pause != null; + const referenced_somewhere = inSet(refState.referenced_ids, refState.referenced_btp_ids); + const should_be_planned_as_umpire = inSet(refState.planned_umpire_ids, refState.planned_umpire_btp_ids); + const should_be_planned_as_service_judge = inSet(refState.planned_service_judge_ids, refState.planned_service_judge_btp_ids); + const should_be_umpire_on_court = inSet(refState.on_court_umpire_ids, refState.on_court_umpire_btp_ids); + const should_be_service_judge_on_court = inSet(refState.on_court_service_judge_ids, refState.on_court_service_judge_btp_ids); + + const setObj = {}; + if (!!official.is_planed_as_umpire !== should_be_planned_as_umpire) { + setObj.is_planed_as_umpire = should_be_planned_as_umpire; + } + if (!!official.is_planed_as_service_judge !== should_be_planned_as_service_judge) { + setObj.is_planed_as_service_judge = should_be_planned_as_service_judge; + } + if ((official.umpire_on_court != null) !== should_be_umpire_on_court) { + setObj.umpire_on_court = should_be_umpire_on_court ? (official.umpire_on_court || true) : null; + } + if ((official.service_judge_on_court != null) !== should_be_service_judge_on_court) { + setObj.service_judge_on_court = should_be_service_judge_on_court ? (official.service_judge_on_court || true) : null; + } + + const on_court = should_be_umpire_on_court || should_be_service_judge_on_court; + const visible_somewhere = in_active_list || on_court || referenced_somewhere; + if (!visible_somewhere) { + const now = Date.now(); + const reactivated_wait_ts = Math.floor(now / 10); + let preferred_role = null; + if (official.umpire_wait != null || official.umpire_pause != null || official.is_planed_as_umpire || official.umpire_on_court != null) { + preferred_role = 'umpire'; + } else if (official.service_judge_wait != null || official.service_judge_pause != null || official.is_planed_as_service_judge || official.service_judge_on_court != null) { + preferred_role = 'service_judge'; + } else if (official.is_umpire === true && official.is_service_judge !== true) { + preferred_role = 'umpire'; + } else if (official.is_service_judge === true && official.is_umpire !== true) { + preferred_role = 'service_judge'; + } else if (official.is_umpire === true && official.is_service_judge === true) { + preferred_role = 'umpire'; + } + + if (preferred_role === 'umpire') { + setObj.umpire_wait = official.umpire_wait != null ? official.umpire_wait : reactivated_wait_ts; + setObj.service_judge_wait = null; + } else if (preferred_role === 'service_judge') { + setObj.service_judge_wait = official.service_judge_wait != null ? official.service_judge_wait : reactivated_wait_ts; + setObj.umpire_wait = null; + } + if (official.is_umpire !== true && official.is_service_judge !== true && official.inactive_list == null) { + setObj.inactive_list = now; + } else { + setObj.inactive_list = null; + } + if (setObj.is_planed_as_umpire === undefined) setObj.is_planed_as_umpire = false; + if (setObj.is_planed_as_service_judge === undefined) setObj.is_planed_as_service_judge = false; + if (setObj.umpire_on_court === undefined) setObj.umpire_on_court = null; + if (setObj.service_judge_on_court === undefined) setObj.service_judge_on_court = null; + } + + return setObj; +} + +function findExistingOfficialForBtpImport(officials, tournament_key, btp_id) { + const canonical_id = `${tournament_key}_btp_${btp_id}`; + return (officials || []).find((official) => + official && + official.tournament_key === tournament_key && + ( + (official.btp_id != null && String(official.btp_id) === String(btp_id)) || + String(official._id) === canonical_id + ) + ) || null; +} + function integrate_umpires(app, tournament_key, btp_state, callback) { const admin = require('./admin'); // avoid dependency cycle const stournament = require('./stournament'); // avoid dependency cycle @@ -1829,6 +2099,8 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { const officials = Array.from(btp_state.officials.values()); var changed = false; + app.db.umpires.find({ tournament_key }, (err, existingOfficials) => { + if (err) return callback(err); async.each(officials, (o, cb) => { const firstname = (o.FirstName ? o.FirstName[0] : ''); const surname = (o.Name ? o.Name[0] : ''); @@ -1840,8 +2112,7 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { } - app.db.umpires.findOne({ tournament_key, btp_id }, (err, cur) => { - if (err) return cb(err); + const cur = findExistingOfficialForBtpImport(existingOfficials, tournament_key, btp_id); @@ -1866,11 +2137,17 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { return cb(); } else { const inactive_list = allListsNull ? Date.now() : null; - app.db.umpires.update({ tournament_key, btp_id }, { $set: { btp_id, firstname, surname, name, country, inactive_list} }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { + app.db.umpires.update({ _id: cur._id, tournament_key }, { $set: { btp_id, firstname, surname, name, country, inactive_list} }, { returnUpdatedDocs: true }, function (err, numAffected, changed_umpire) { if (err) { console.error(err); return cb(err); } + const idx = existingOfficials.findIndex((official) => official && official._id === changed_umpire._id); + if (idx >= 0) { + existingOfficials[idx] = changed_umpire; + } else { + existingOfficials.push(changed_umpire); + } const admin = require('./admin'); admin.notify_change(app, tournament_key, 'umpire_updated', changed_umpire); }); @@ -1904,10 +2181,10 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { if (err) { return cb(err); } + existingOfficials.push(inserted_umpire); admin.notify_change(app, tournament_key, 'umpire_add', { umpire: inserted_umpire }); return cb(); }); - }); }, err => { if (changed) { stournament.get_umpires(app.db, tournament_key, function (err, all_umpires) { @@ -1920,6 +2197,7 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { callback(err); } }); + }); } function calculate_match_ids_on_court(btp_state) { @@ -1946,6 +2224,63 @@ function update_umpire(app, tkey, umpire, status, last_time_on_court_ts,court_id admin.notify_change(app, tkey, 'umpire_updated', changed_umpire); }); } + +function normalize_official_visibility(app, tournament_key, callback) { + const admin = require('./admin'); + const stournament = require('./stournament'); + + app.db.matches.find({ tournament_key }, (matchErr, matches) => { + if (matchErr) { + return callback(matchErr); + } + + const refState = buildOfficialReferenceState(matches); + + app.db.umpires.find({ tournament_key }, (err, officials) => { + if (err) { + return callback(err); + } + + let changed = false; + async.eachSeries(officials, (official, cb) => { + const setObj = computeOfficialVisibilityPatch(official, refState); + if (Object.keys(setObj).length === 0) { + return cb(); + } + + app.db.umpires.update( + { _id: official._id, tournament_key }, + { $set: setObj }, + { returnUpdatedDocs: true }, + (updateErr, numAffected, changed_umpire) => { + if (updateErr) { + return cb(updateErr); + } + if (changed_umpire) { + changed = true; + admin.notify_change(app, tournament_key, 'umpire_updated', changed_umpire); + } + cb(); + } + ); + }, (eachErr) => { + if (eachErr) { + return callback(eachErr); + } + if (!changed) { + return callback(null); + } + stournament.get_umpires(app.db, tournament_key, function (getErr, all_umpires) { + if (!getErr) { + admin.notify_change(app, tournament_key, 'umpires_changed', { all_umpires }); + } + callback(getErr); + }); + }); + }); + }); +} + async function integrate_now_on_court(app, tkey, callback) { const admin = require('./admin'); // avoid dependency cycle const btp_manager = require('./btp_manager'); @@ -2032,7 +2367,7 @@ async function integrate_now_on_court(app, tkey, callback) { } assert(tournament); - app.db.matches.find({ 'setup.now_on_court': true }, async (err, now_on_court_matches) => { + app.db.matches.find({ tournament_key: tkey, 'setup.now_on_court': true }, async (err, now_on_court_matches) => { if (err) return callback(err); const activeMatches = now_on_court_matches.filter(match => typeof match.team1_won !== 'boolean'); @@ -2060,23 +2395,28 @@ async function integrate_now_on_court(app, tkey, callback) { await setPlayerStateForMatch(match); })); - app.db.matches.find({ tournament_key: tkey }, async (err, matches) => { - if (err) return callback(err); + app.db.matches.find({ tournament_key: tkey }, async (err, matches) => { + if (err) return callback(err); - const activePlayerIds = collectActivePlayerIds(activeMatches); - const staleMatches = matches.filter(match => - match && - match.setup && - match.setup.now_on_court !== true && - matchHasPlayerOnCourtFlags(match) && - matchHasOnlyStalePlayerFlags(match, activePlayerIds) - ); + app.db.matches.find({ tournament_key: tkey, 'setup.now_on_court': true }, async (err, refreshed_on_court_matches) => { + if (err) return callback(err); + + const refreshedActiveMatches = refreshed_on_court_matches.filter(match => typeof match.team1_won !== 'boolean'); + const activePlayerIds = collectActivePlayerIds(refreshedActiveMatches); + const staleMatches = matches.filter(match => + match && + match.setup && + match.setup.now_on_court !== true && + matchHasPlayerOnCourtFlags(match) && + matchHasOnlyStalePlayerFlags(match, activePlayerIds) + ); - await Promise.all(staleMatches.map(match => clearPlayerStateForMatch(match))); - callback(null); + await Promise.all(staleMatches.map(match => clearPlayerStateForMatch(match))); + callback(null); + }); + }); }); }); - }); // TODO clear courts (better in async) } @@ -2100,6 +2440,7 @@ async function sync_btp_data(app, tkey, response) { (scoring_formats, location_map, cb) => integrate_courts(app, tkey, btp_state, scoring_formats, location_map, cb), (scoring_formats, location_map, court_map, cb) => integrate_matches(app, tkey, btp_state, scoring_formats, location_map, court_map, cb), cb => reconcile_match_officials(app, tkey, cb), + cb => normalize_official_visibility(app, tkey, cb), cb => integrate_now_on_court(app, tkey, cb), cb => cleanup_entities(app, tkey, btp_state, cb), ], (err) => { @@ -2123,6 +2464,11 @@ module.exports = { _fallback_scoring_format: fallbackScoringFormat, _normalize_scoring_format: normalizeScoringFormat, _merge_local_scoring_format: mergeLocalScoringFormat, + _build_official_reference_state: buildOfficialReferenceState, + _compute_official_visibility_patch: computeOfficialVisibilityPatch, + _find_existing_official_for_btp_import: findExistingOfficialForBtpImport, + _reconcile_match_officials: reconcile_match_officials, + _merge_local_match_into_btp_match: mergeLocalMatchIntoBtpMatch, _sanitize_scoring_format: sanitizeScoringFormat, _set_type_to_end_max: setTypeToEndMax, }; diff --git a/bts/match_utils.js b/bts/match_utils.js index 89dcaed..16f1da0 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -244,10 +244,7 @@ async function set_umpires_on_court(app, tournament, match, callback) { function remove_highlight_preparation(match, callback){ const setup = match.setup; - - if(setup.highlight && setup.highlight == 6){ - setup.highlight = 0; - } + setup.highlight = 0; return callback(null); } @@ -944,7 +941,6 @@ function set_umpire_to_standby(app, tournament_key, setup) { umpire.service_judge_on_court = null; umpire.is_planed_as_umpire = false; umpire.last_time_on_court_ts = null; - umpire.checked_in = false; umpire.status = 'standby'; umpire.court_id = null; update_umpire(app, tournament_key, umpire); @@ -956,7 +952,6 @@ function set_umpire_to_standby(app, tournament_key, setup) { service_judge.service_judge_on_court = null; service_judge.is_planed_as_umpire = false; service_judge.last_time_on_court_ts = null; - service_judge.checked_in = false; service_judge.status = 'standby'; service_judge.court_id = null; update_umpire(app, tournament_key, service_judge); diff --git a/test/test_admin.js b/test/test_admin.js new file mode 100644 index 0000000..848fca5 --- /dev/null +++ b/test/test_admin.js @@ -0,0 +1,75 @@ +'use strict'; + +const assert = require('assert'); + +const {_describe, _it} = require('./tutils.js'); + +const admin = require('../bts/admin.js'); + +_describe('admin', () => { + _it('marks official changes in match edit as pending sync and suppresses removed BTP officials', () => { + const old_setup = { + umpire: { _id: 'u1', btp_id: 6, name: 'Stefan Schiedsrichter' }, + service_judge: { _id: 'u2', btp_id: 7, name: 'Michael G-Punkt' } + }; + const new_setup = { + service_judge: { _id: 'u2', btp_id: 7, name: 'Michael G-Punkt' } + }; + + const result = admin._build_match_edit_official_sync_meta(old_setup, new_setup); + + assert.strictEqual(result.has_official_change, true); + assert.strictEqual(new_setup.suppressed_umpire_btp_id, 6); + assert.strictEqual(new_setup.suppressed_service_judge_btp_id, undefined); + }); + + _it('clears stale suppress flag when a new official is set in the same role', () => { + const old_setup = { + umpire: { _id: 'u1', btp_id: 6, name: 'Stefan Schiedsrichter' } + }; + const new_setup = { + umpire: { _id: 'u3', btp_id: 9, name: 'Ralf Referee' }, + suppressed_umpire_btp_id: 6 + }; + + const result = admin._build_match_edit_official_sync_meta(old_setup, new_setup); + + assert.strictEqual(result.has_official_change, true); + assert.strictEqual(new_setup.suppressed_umpire_btp_id, undefined); + }); + + _it('releases dependent service judge when umpire is removed from a setup', () => { + const setup = { + service_judge: { _id: 'u2', btp_id: 7, name: 'Michael G-Punkt' } + }; + + const releases = admin._collect_dependent_official_releases(setup); + + assert.deepStrictEqual(releases, [{ + official_id: 'u2', + wait_field: 'service_judge_wait', + target_position: 'front' + }]); + assert.strictEqual(setup.service_judge, undefined); + assert.strictEqual(setup.suppressed_service_judge_btp_id, 7); + }); + + _it('removing an umpire from a setup also releases a dependent service judge', () => { + const setup = { + umpire: { _id: 'u1', btp_id: 6, name: 'Stefan Schiedsrichter' }, + service_judge: { _id: 'u2', btp_id: 7, name: 'Michael G-Punkt' } + }; + + const releases = admin._remove_official_from_setup(setup, 'umpire'); + + assert.strictEqual(setup.umpire, undefined); + assert.strictEqual(setup.service_judge, undefined); + assert.strictEqual(setup.suppressed_umpire_btp_id, 6); + assert.strictEqual(setup.suppressed_service_judge_btp_id, 7); + assert.deepStrictEqual(releases, [{ + official_id: 'u2', + wait_field: 'service_judge_wait', + target_position: 'front' + }]); + }); +}); diff --git a/test/test_btp_sync.js b/test/test_btp_sync.js index 6e18f3c..93e908a 100644 --- a/test/test_btp_sync.js +++ b/test/test_btp_sync.js @@ -214,6 +214,32 @@ _describe('btp_sync', () => { }); }); + _it('keeps the local court on finished matches when BTP no longer sends a court', () => { + const currentMatch = { + team1_won: true, + btp_winner: 1, + btp_needsync: false, + setup: { + court_id: 'court_7', + now_on_court: false, + state: 'finished', + teams: [{ players: [] }, { players: [] }], + }, + }; + const btpMatch = { + setup: { + now_on_court: false, + state: 'scheduled', + teams: [{ players: [] }, { players: [] }], + }, + }; + + const merged = btp_sync._merge_local_match_into_btp_match(currentMatch, structuredClone(btpMatch)); + + assert.strictEqual(merged.setup.state, 'finished'); + assert.strictEqual(merged.setup.court_id, 'court_7'); + }); + _it('keeps locally edited timing values when BTP scoring formats are normalized again', () => { const existing = { id: 11, @@ -271,4 +297,312 @@ _describe('btp_sync', () => { assert.strictEqual(merged.last_set_points.break_before_set_duration_ms, 35000); assert.strictEqual(merged.last_set_points.interval_enabled, true); }); + + _it('ignores stale suppressed officials once the local match is no longer pending BTP sync', () => { + const currentMatch = { + btp_needsync: false, + setup: { + state: 'scheduled', + teams: [{ players: [] }, { players: [] }], + suppressed_umpire_btp_id: 6, + }, + }; + const btpMatch = { + setup: { + state: 'scheduled', + teams: [{ players: [] }, { players: [] }], + umpire: { + _id: 'default_btp_6', + btp_id: 6, + name: 'Michael G-Punkt', + }, + }, + }; + + const merged = btp_sync._merge_local_match_into_btp_match(currentMatch, structuredClone(btpMatch)); + + assert.ok(merged.setup.umpire); + assert.strictEqual(merged.setup.umpire.btp_id, 6); + assert.strictEqual(merged.setup.suppressed_umpire_btp_id, undefined); + }); + + _it('keeps suppressed officials hidden while a local match update is still pending sync', () => { + const currentMatch = { + btp_needsync: true, + setup: { + state: 'scheduled', + teams: [{ players: [] }, { players: [] }], + suppressed_umpire_btp_id: 6, + }, + }; + const btpMatch = { + setup: { + state: 'scheduled', + teams: [{ players: [] }, { players: [] }], + umpire: { + _id: 'default_btp_6', + btp_id: 6, + name: 'Michael G-Punkt', + }, + }, + }; + + const merged = btp_sync._merge_local_match_into_btp_match(currentMatch, structuredClone(btpMatch)); + + assert.strictEqual(merged.setup.umpire, undefined); + assert.strictEqual(merged.setup.suppressed_umpire_btp_id, 6); + }); + + _it('ignores stale suppressed service judges once the local match is no longer pending BTP sync', () => { + const currentMatch = { + btp_needsync: false, + setup: { + state: 'scheduled', + teams: [{ players: [] }, { players: [] }], + suppressed_service_judge_btp_id: 7, + }, + }; + const btpMatch = { + setup: { + state: 'scheduled', + teams: [{ players: [] }, { players: [] }], + service_judge: { + _id: 'default_btp_7', + btp_id: 7, + name: 'Service Judge', + }, + }, + }; + + const merged = btp_sync._merge_local_match_into_btp_match(currentMatch, structuredClone(btpMatch)); + + assert.ok(merged.setup.service_judge); + assert.strictEqual(merged.setup.service_judge.btp_id, 7); + assert.strictEqual(merged.setup.suppressed_service_judge_btp_id, undefined); + }); + + _it('does not restore role capability flags from match references during reconcile', (done) => { + const official = { + _id: 'o1', + tournament_key: 't1', + btp_id: 11, + firstname: 'Stefan', + surname: 'Schiedsrichter', + name: 'Stefan Schiedsrichter', + is_umpire: false, + is_service_judge: true, + is_planed_as_umpire: false, + is_planed_as_service_judge: false, + umpire_on_court: null, + service_judge_on_court: null, + umpire_wait: null, + service_judge_wait: 123, + umpire_pause: null, + service_judge_pause: null, + inactive_list: null, + }; + const match = { + _id: 'm1', + tournament_key: 't1', + setup: { + now_on_court: false, + umpire: { + _id: 'o1', + btp_id: 11, + firstname: 'Stefan', + surname: 'Schiedsrichter', + name: 'Stefan Schiedsrichter', + } + } + }; + const state = { + matches: [structuredClone(match)], + umpires: [structuredClone(official)] + }; + const app = { + db: { + matches: { + find(query, cb) { + cb(null, state.matches); + } + }, + umpires: { + find(query, cb) { + cb(null, state.umpires); + }, + insert(doc, cb) { + state.umpires.push(doc); + cb(null, doc); + }, + update(query, update, options, cb) { + const idx = state.umpires.findIndex((u) => u._id === query._id); + state.umpires[idx] = { ...state.umpires[idx], ...update.$set }; + cb(null, 1, state.umpires[idx]); + } + } + } + }; + + btp_sync._reconcile_match_officials(app, 't1', (err) => { + assert.ifError(err); + assert.strictEqual(state.umpires[0].is_umpire, false); + assert.strictEqual(state.umpires[0].is_service_judge, true); + done(); + }); + }); + + _it('keeps a local umpire assignment only while the match update is still pending sync', () => { + const pendingCurrentMatch = { + btp_needsync: true, + setup: { + state: 'scheduled', + teams: [{ players: [] }, { players: [] }], + umpire: { + _id: 'default_btp_6', + btp_id: 6, + name: 'Michael G-Punkt', + }, + }, + }; + const staleCurrentMatch = { + btp_needsync: false, + setup: { + state: 'scheduled', + teams: [{ players: [] }, { players: [] }], + umpire: { + _id: 'default_btp_6', + btp_id: 6, + name: 'Michael G-Punkt', + }, + }, + }; + const btpMatchWithoutOfficial = { + setup: { + state: 'scheduled', + teams: [{ players: [] }, { players: [] }], + }, + }; + + const pendingMerged = btp_sync._merge_local_match_into_btp_match(pendingCurrentMatch, structuredClone(btpMatchWithoutOfficial)); + const staleMerged = btp_sync._merge_local_match_into_btp_match(staleCurrentMatch, structuredClone(btpMatchWithoutOfficial)); + + assert.ok(pendingMerged.setup.umpire); + assert.strictEqual(staleMerged.setup.umpire, undefined); + }); + + _it('clears stale planned and on-court flags when an official is no longer referenced by matches', () => { + const refState = btp_sync._build_official_reference_state([]); + const patch = btp_sync._compute_official_visibility_patch({ + _id: 'default_btp_6', + btp_id: 6, + is_umpire: true, + is_service_judge: true, + is_planed_as_umpire: true, + is_planed_as_service_judge: false, + umpire_on_court: 'default_1', + service_judge_on_court: null, + umpire_wait: null, + service_judge_wait: null, + umpire_pause: null, + service_judge_pause: null, + inactive_list: null, + }, refState); + + assert.strictEqual(patch.is_planed_as_umpire, false); + assert.strictEqual(patch.umpire_on_court, null); + assert.strictEqual(patch.umpire_wait != null, true); + assert.strictEqual(patch.service_judge_wait, null); + assert.strictEqual(patch.inactive_list, null); + }); + + _it('moves inactive officials back to wait when they are active-capable and no longer referenced', () => { + const refState = btp_sync._build_official_reference_state([]); + const patch = btp_sync._compute_official_visibility_patch({ + _id: 'default_btp_6', + btp_id: 6, + is_umpire: true, + is_service_judge: false, + is_planed_as_umpire: false, + is_planed_as_service_judge: false, + umpire_on_court: null, + service_judge_on_court: null, + umpire_wait: null, + service_judge_wait: null, + umpire_pause: null, + service_judge_pause: null, + inactive_list: 12345, + }, refState); + + assert.strictEqual(patch.umpire_wait != null, true); + assert.strictEqual(patch.service_judge_wait, null); + assert.strictEqual(patch.inactive_list, null); + }); + + _it('preserves planned flags when an official is still referenced by a scheduled match', () => { + const refState = btp_sync._build_official_reference_state([{ + setup: { + state: 'scheduled', + now_on_court: false, + umpire: { _id: 'default_btp_6', btp_id: 6 }, + }, + }]); + const patch = btp_sync._compute_official_visibility_patch({ + _id: 'default_btp_6', + btp_id: 6, + is_planed_as_umpire: true, + is_planed_as_service_judge: false, + umpire_on_court: null, + service_judge_on_court: null, + umpire_wait: null, + service_judge_wait: null, + umpire_pause: null, + service_judge_pause: null, + inactive_list: null, + }, refState); + + assert.deepStrictEqual(patch, {}); + }); + + _it('does not treat finished-match officials as still referenced for visibility', () => { + const refState = btp_sync._build_official_reference_state([{ + team1_won: true, + setup: { + state: 'finished', + now_on_court: false, + umpire: { _id: 'default_btp_5', btp_id: 5 }, + }, + }]); + const patch = btp_sync._compute_official_visibility_patch({ + _id: 'default_btp_5', + btp_id: 5, + is_umpire: true, + is_service_judge: true, + is_planed_as_umpire: false, + is_planed_as_service_judge: false, + umpire_on_court: null, + service_judge_on_court: null, + umpire_wait: null, + service_judge_wait: null, + umpire_pause: null, + service_judge_pause: null, + inactive_list: 12345, + }, refState); + + assert.strictEqual(patch.umpire_wait != null, true); + assert.strictEqual(patch.service_judge_wait, null); + assert.strictEqual(patch.inactive_list, null); + }); + + _it('reuses an existing official by canonical _id when btp_id is missing locally', () => { + const existing = { + _id: 'default_btp_6', + tournament_key: 'default', + btp_id: null, + name: 'Michael G-Punkt', + }; + + const found = btp_sync._find_existing_official_for_btp_import([existing], 'default', 6); + + assert.strictEqual(found, existing); + }); }); From bbcb56b072e4f2cb7f568692a371197c1a65e933 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 7 Apr 2026 02:07:46 +0200 Subject: [PATCH 219/224] Refine official assignment UI and match editor --- static/css/admin.css | 446 +++++- static/css/cmatch.css | 96 +- static/js/change.js | 118 +- static/js/change_helpers.js | 31 + static/js/ci18n_de.js | 12 +- static/js/ci18n_en.js | 12 +- static/js/cmatch.js | 395 ++++- static/js/cmatch_official_select_helpers.js | 177 +++ static/js/ctournament.js | 1511 +++++++++++++++---- static/js/cumpires.js | 1 - test/test_change_helpers.js | 53 + test/test_cmatch_official_select.js | 83 + 12 files changed, 2562 insertions(+), 373 deletions(-) create mode 100644 static/js/change_helpers.js create mode 100644 static/js/cmatch_official_select_helpers.js create mode 100644 test/test_change_helpers.js create mode 100644 test/test_cmatch_official_select.js diff --git a/static/css/admin.css b/static/css/admin.css index 84ccca2..0111154 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -529,90 +529,432 @@ h2.edit { overflow-wrap: break-word; } +.official_split_section, .paralel { + display: grid; + grid-template-columns: minmax(0, 1fr) 10px minmax(0, 1fr); + gap: 0; + align-items: start; +} + +.official_role_split_column { + min-width: 0; +} + +.official_role_split_space { + background: #eee; + align-self: stretch; +} + +.space { + background: #eee; + align-self: stretch; +} + +.officials_list, +.officials_dual_list { display: flex; - flex-direction: row; - justify-content: space-between; - align-items: flex-start; /* <<< DAS ist entscheidend */ + flex-direction: column; + gap: 1px; + background: #ddd; } -.paralel > table { - flex: 1; +.officials_row, +.officials_dual_row { + background: #fff; } -/* Tabellenlayout stabil halten */ -.officials_table { - table-layout: fixed; - width: 100%; +.officials_row { + display: grid; + grid-template-columns: 38px minmax(0, 1fr) 50px 50px; + align-items: stretch; +} + +.officials_row:not(.officials_list_header)[data-official-id] { + cursor: grab; +} + +.officials_row.dragging, +.official_assignment_slot.dragging { + cursor: grabbing; + opacity: 0.5; +} + +.officials_list_header, +.officials_dual_header { + background: #ccc; + font-weight: 600; +} + +.officials_cell, +.officials_dual_cell { + min-width: 0; + padding: 4px 6px; + display: flex; + align-items: center; + box-sizing: border-box; +} + +.officials_cell_name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -/* rechte zwei Spalten fix 50px */ -.officials_table th:nth-child(2), -.officials_table td:nth-child(2), -.officials_table th:nth-child(3), -.officials_table td:nth-child(3) { +.officials_cell_leading { + width: 38px; + min-width: 38px; + max-width: 38px; + justify-content: center; + padding-left: 0; + padding-right: 0; +} + +.officials_cell_role { width: 50px; min-width: 50px; max-width: 50px; + justify-content: center; +} + +.officials_cell_role input { + margin: 0; } -/* Checkbox-Zellen zentrieren */ -.officials_table td:nth-child(2), -.officials_table td:nth-child(3) { - text-align: center; - vertical-align: middle; +.officials_dual_list { + gap: 1px; } -/* Falls create_simple_checkbox ein erzeugt */ -.officials_table td:nth-child(2) input, -.officials_table td:nth-child(3) input { - margin: 0 auto; - display: block; +.officials_dual_row { + display: grid; + grid-template-columns: 38px minmax(0, 1fr) 50px 50px 10px minmax(0, 1fr) 50px 50px; + align-items: stretch; } -/* Header-Icons zentrieren */ -.officials_table th:nth-child(2), -.officials_table th:nth-child(3) { - text-align: center; - vertical-align: middle; +.officials_dual_center_space { + background: #eee; + padding: 0; } -.officials_table th:nth-child(2) > div, -.officials_table th:nth-child(3) > div { - margin: 0 auto; +.officials_table_on_court .officials_cell_name { + color: #666; } -.space { - width: 10px; +.officials_table_on_court .officials_cell_leading, +.officials_table_on_court .officials_cell_role, +.officials_table_in_preparation .officials_cell_leading, +.officials_table_in_preparation .officials_cell_role { + color: inherit; +} + +.officials_table_court_inactive { + filter: grayscale(1); + opacity: 0.45; } - * /* Normalzustand: gleiche Grundfarbe wie Tabelle/Rows (oder transparent) */ -.drop-zone td { - background: inherit; +.officials_role_toggle { + display: flex; + justify-content: center; + width: 100%; } +.officials_drop_zone { + height: 12px; + background: inherit; +} -/* Sobald eine Row gezogen wird, werden alle Drop-Zonen gelblich */ -tbody.drop-zones-active .drop-zone td { - background: rgba(255, 215, 0, 0.35); +.officials_list.drop-zones-active .officials_drop_zone, +.officials_dual_list.drop-zones-active .official_assignment_slot.drop-zone { + background: rgba(255, 215, 0, 0.35); } -/* Aktuell anvisierte Zone stärker */ -tbody.drop-zones-active .drop-zone.drop-zone-hover td { - background: rgba(255, 215, 0, 0.65); +.officials_list.drop-zones-active .officials_drop_zone.drop-zone-hover, +.officials_dual_list.drop-zones-active .official_assignment_slot.drop-zone.drop-zone-hover { + background: rgba(255, 215, 0, 0.65); } -tr[data-official-id] { - cursor: grab; +.official_drag_image_table { + border-collapse: separate; + border-spacing: 0; } -tr.dragging { - cursor: grabbing; - opacity: 0.5; +.official_drag_image_cell { + box-sizing: border-box; } -tr.table-spacer td { - padding: 0; - border: none; - background: #eee; +.official_section_body { + display: flex; + gap: 10px; + align-items: stretch; + flex-wrap: wrap; + margin-bottom: 12px; + row-gap: 0; +} + +.official_on_court_row { + display: grid; + grid-template-columns: 65px minmax(0, 1fr) minmax(0, 1fr); + gap: 10px; + align-items: stretch; + width: 100%; + padding: 8px 0; +} + +.official_inactive_row { + grid-template-columns: 65px minmax(0, 1fr); +} + +.official_section_body > .official_on_court_row:nth-child(odd) { + background: #bbb; +} + +.official_section_body > .official_on_court_row:nth-child(even) { + background: #ccc; +} + +.official_on_court_leading { + display: flex; + align-items: center; + justify-content: center; +} + +.official_preparation_leading { + font-weight: 700; +} + +.official_on_court_leading .court { + transform: scale(1.5); + transform-origin: center; +} + +.official_on_court_slot { + min-height: 1px; +} + +.official_on_court_slot > .official_card_frame { + width: 100%; +} + +.official_card_frame { + width: 400px; + height: 38px; + box-sizing: border-box; + display: flex; + align-items: center; + gap: 6px; + border-radius: 8px; +} + +.official_card_skin { + background: #fff4b8; + border: 1px solid #d6c36a; + padding: 4px 9px 4px 4px; +} + +.official_card_skin.official_card_variant_on_court { + background: #dddddd; + border-color: #666666; + color: #666666; +} + +.official_card_skin.official_card_variant_checked_in { + background: #adffcb; + border-color: #6fbe8d; + color: #000000; +} + +.official_card_skin.official_card_variant_not_checked_in { + background: #ffabab; + border-color: #cf7f7f; + color: #000000; +} + +.official_card_skin.official_card_variant_list { + background: #dddddd; + border-color: #000000; + color: #000000; +} + +.official_card_skin.official_card_placeholder { + background: transparent; + border: 1px dashed #999; +} + +.official_card_skin.official_card_placeholder > * { + visibility: hidden; +} + +.official_card_skin.official_card_variant_on_court .official_card_icon, +.official_card_skin.official_card_variant_on_court .official_card_trail_icon { + filter: none; + opacity: 1; +} + +.official_card_skin.official_card_variant_on_court .official_card_trail_swap { + color: #000000; + opacity: 1; +} + +.official_card_frame.official_card_placeholder_compact { + height: 12px; + margin: 0; + transition: none; +} + +.official_card_frame.official_card_placeholder_hoverlike { + height: 38px; + margin: 14px 0; +} + +.official_card_skin.official_card_placeholder.official_card_placeholder_activelike { + background: #fff4b8; + border-color: #d6c36a; +} + +.official_card_skin.official_card_placeholder.official_card_placeholder_hoverlike { + background: #ffe97a; + border-color: #c6a800; +} + +.official_card_drop { + border: 1px dashed transparent; + background: transparent; + transition: background-color 90ms linear, border-color 90ms linear, height 110ms ease-out, margin 110ms ease-out; +} + +.officials_drag_active .official_card_drop { + border-color: #999; +} + +.official_card_drop.official_card_drop_disabled { + border-color: transparent; +} + +.official_card_drop.official_card_drop_active { + background: #fff4b8; + border-color: #d6c36a; +} + +.official_card_drop.official_card_drop_hover { + background: #ffe97a; + border-color: #c6a800; +} + +.official_card_stack > .official_card_drop { + height: 12px; + margin: 0; +} + +.official_card_stack > .official_card_drop.official_card_drop_terminal { + height: 38px; + margin: 14px 0; +} + +.official_card_stack > .official_card_drop.official_card_drop_active { + height: 12px; +} + +.official_card_stack > .official_card_drop.official_card_drop_terminal.official_card_drop_active, +.official_card_stack > .official_card_drop.official_card_drop_terminal.official_card_drop_hover, +.official_card_stack > .official_card_drop.official_card_drop_terminal.official_card_drop_hover.official_card_drop_hover_expand { + height: 38px; + margin: 14px 0; +} + +.official_card_stack.official_card_stack_has_nonterminal_hover > .official_card_drop.official_card_drop_terminal, +.official_card_stack.official_card_stack_has_nonterminal_hover > .official_card_drop.official_card_drop_terminal.official_card_drop_active, +.official_card_stack.official_card_stack_has_nonterminal_hover > .official_card_drop.official_card_drop_terminal.official_card_drop_hover, +.official_card_stack.official_card_stack_has_nonterminal_hover > .official_card_drop.official_card_drop_terminal.official_card_drop_hover.official_card_drop_hover_expand { + height: 12px; + margin: 0; +} + +.official_card_stack > .official_card_drop.official_card_drop_suppressed { + display: none; + background: transparent; + border-color: transparent; + pointer-events: none; +} + +.official_card_stack > .official_card_drop.official_card_drop_hover { + height: 12px; +} + +.official_card_stack > .official_card_drop.official_card_drop_hover.official_card_drop_hover_expand { + height: 38px; + margin: 14px 0; +} + +.official_card_stack { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.official_card_frame .official_card_icon { + flex: 0 0 auto; + width: 1.75em; + min-width: 1.75em; + align-self: stretch; + height: auto; + min-height: 0; + margin: 0 !important; + background-size: contain; + background-position: center center; + background-repeat: no-repeat; +} + +.official_card_name { + min-width: 0; + font-size: 1.2em; + display: flex; + align-items: center; + flex: 1 1 auto; +} + +.official_card_dragging { + opacity: 1; +} + +.official_card_trail { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0; + margin-left: auto; + cursor: pointer; +} + +.official_card_frame .official_card_trail_icon { + width: 1em; + min-width: 1em; + height: 1em; + min-height: 1em; + margin: 0 !important; + background-size: contain; + background-position: center center; + background-repeat: no-repeat; +} + +.official_card_trail_swap { + display: flex; + align-items: center; + justify-content: center; + width: 0.7em; + min-width: 0.7em; + font-size: 0.75em; + line-height: 1; + color: #444; +} + +.official_card_trail[data-state="service_only"] .official_card_trail_icon.umpire, +.official_card_trail[data-state="umpire_only"] .official_card_trail_icon.service_judge { + filter: grayscale(1); + opacity: 0.45; +} + +.official_card_trail[data-state="service_only"] .official_card_trail_swap, +.official_card_trail[data-state="umpire_only"] .official_card_trail_swap { + visibility: hidden; } diff --git a/static/css/cmatch.css b/static/css/cmatch.css index 68d9880..e33d451 100644 --- a/static/css/cmatch.css +++ b/static/css/cmatch.css @@ -374,22 +374,48 @@ match_manual_call_button, font-weight: bold; } -.checked_in { +.can_check_out { + cursor: url('../icons/check_out.png')15 15, auto; +} + +.can_check_in { + cursor: url('../icons/check_in.png')15 15, auto; +} + +.person_status_target { + display: inline-block; + padding: 0 0.15em; +} + +.checked_in > .person_status_target { background-color: #adffcb; } -.can_check_out { - cursor: url('../icons/check_out.png')15 15, auto; +.not_checked_in > .person_status_target { + background-color: #ffabab; } -.not_checked_in { +.player.checked_in { + background-color: #adffcb; +} + +.player.not_checked_in { background-color: #ffabab; } -.can_check_in { +.person.can_check_in:not(.player), +.person.can_check_out:not(.player) { + cursor: default; +} + +.person.can_check_in:not(.player) > .person_status_target { cursor: url('../icons/check_in.png')15 15, auto; } +.person.can_check_out:not(.player) > .person_status_target { + cursor: url('../icons/check_out.png')15 15, auto; +} + .now_playing { background-color: #fbe44d; } @@ -681,6 +707,7 @@ match_manual_call_button, .umpire, .service_judge, .no_umpire, +.no_service_judge, .court, .court_history, .court_upcoming { @@ -722,6 +749,7 @@ match_manual_call_button, .umpire, .service_judge, .no_umpire, +.no_service_judge, .match_no_umpire { text-wrap: nowrap; } @@ -744,6 +772,64 @@ match_manual_call_button, background-image: url(../icons/no_umpire.svg); } +.no_umpire_add { + position: relative; + filter: grayscale(0); + cursor: pointer; +} + +.no_umpire_add:hover { + filter: grayscale(0); + background-image: url(../icons/umpire.svg); +} + +.umpire.can_add_service_judge { + position: relative; + overflow: visible; + cursor: pointer; +} + +.umpire.can_add_service_judge::before { + content: ''; + position: absolute; + left: 0.45em; + top: 0.18em; + width: 1.45em; + height: 1em; + background-image: url(../icons/service_judge.svg); + background-size: contain; + background-repeat: no-repeat; + background-position: center center; + opacity: 0; + pointer-events: none; +} + +.umpire.can_add_service_judge:hover::before { + opacity: 1; +} + +.no_umpire_add::after, +.umpire.can_add_service_judge::after { + content: '+'; + position: absolute; + right: -0.18em; + bottom: -0.45em; + font-size: 2.3em; + line-height: 1; + font-weight: 900; + color: #1fb14e; + text-shadow: none; + -webkit-text-stroke-width: 0; + opacity: 0; + transition: opacity 120ms ease-out; + pointer-events: none; +} + +.no_umpire_add:hover::after, +.umpire.can_add_service_judge:hover::after { + opacity: 1; +} + .court{ background-image: url(../icons/court.svg); height: 1em; diff --git a/static/js/change.js b/static/js/change.js index 79168de..e873416 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -6,6 +6,36 @@ function default_handler(rerender, special_funcs) { }; } + function _announcement_claim_key(change_obj, fallback_kind) { + if (!change_obj || !change_obj.val) { + return `${fallback_kind}:na`; + } + if (change_obj.val._announcement_claim_key) { + return change_obj.val._announcement_claim_key; + } + const ts = change_obj.val._announcement_ts || 'na'; + const match_id = + change_obj.val.match__id || + change_obj.val._id || + change_obj.val.btp_id || + (change_obj.val.match && change_obj.val.match._id) || + (change_obj.val.setup && (change_obj.val.setup._match_id || change_obj.val.setup.btp_id)) || + 'unknown'; + return `${fallback_kind}:${match_id}:${ts}`; + } + + function _attach_setup_announcement_claim(change_obj, fallback_kind) { + if (!change_obj || !change_obj.val || !change_obj.val.setup) { + return; + } + change_obj.val.setup._announcement_claim_key = _announcement_claim_key(change_obj, fallback_kind); + change_obj.val.setup._match_id = + change_obj.val.match__id || + change_obj.val._id || + change_obj.val.btp_id || + change_obj.val.setup._match_id; + } + function _handle_announcement_event(kind, payload, announce_fn) { if (ctournament && typeof ctournament.handle_view_announcement === 'function') { const handled = ctournament.handle_view_announcement(kind, payload); @@ -79,6 +109,37 @@ function default_handler(rerender, special_funcs) { m.team1_won = cval.team1_won; } + function apply_umpires_changed(update, deps) { + const helper = (typeof change_helpers !== 'undefined' && change_helpers && change_helpers.apply_umpires_changed) + ? change_helpers.apply_umpires_changed + : null; + const resolved_deps = deps || { + curt_ref: curt, + uiu_ref: uiu, + cmatch_ref: cmatch, + current_view_ref: current_view, + cumpires_ref: cumpires, + ctournament_ref: ctournament + }; + if (helper) { + helper(update, resolved_deps); + return; + } + resolved_deps.curt_ref.umpires = (update.all_umpires || []).map((official) => { + if (resolved_deps.ctournament_ref && typeof resolved_deps.ctournament_ref.apply_pending_official_role_override === 'function') { + return resolved_deps.ctournament_ref.apply_pending_official_role_override(official); + } + return official; + }); + resolved_deps.uiu_ref.qsEach('select[name="umpire_name"]', function(select) { + resolved_deps.cmatch_ref.render_umpire_options(select, select.value); + }); + if(resolved_deps.current_view_ref === 'show') { + resolved_deps.cumpires_ref.ui_status(resolved_deps.uiu_ref.qs('.umpire_container')); + } + resolved_deps.ctournament_ref.update_officials(); + } + function default_handler_func(rerender, special_funcs, c) { if (special_funcs && special_funcs[c.ctype]) { special_funcs[c.ctype](c); @@ -87,7 +148,7 @@ function default_handler(rerender, special_funcs) { switch (c.ctype) { case 'free_announce': - _handle_announcement_event('free_announce', c.val, () => announce([c.val.text])); + _handle_announcement_event('free_announce', c.val, () => announce([c.val.text], false, _announcement_claim_key(c, 'free_announce'))); break; case 'emergency_announce': curt.enable_emergency = c.val; @@ -154,6 +215,7 @@ function default_handler(rerender, special_funcs) { break; case 'match_edit': ctournament.update_match(c); + ctournament.update_officials(); break; case 'match_add': const match_id = c.val.match__id; @@ -223,6 +285,10 @@ function default_handler(rerender, special_funcs) { ctournament.update_location_logo(c.val.location_id, loc.logo_id, loc.logo_name); break; case 'match_preparation_call': + if (c.val && c.val.match && c.val.match.setup) { + c.val.match.setup._announcement_claim_key = `match_preparation_call:${c.val.match__id}:${c.val._announcement_ts || 'na'}`; + c.val.match.setup._match_id = c.val.match__id; + } _handle_announcement_event('match_preparation_call', c.val, () => announcePreparationMatch(c.val.match.setup)); curt.matches.forEach((match) => { match.setup.location_id = c.val.location_id; @@ -231,39 +297,51 @@ function default_handler(rerender, special_funcs) { ctournament.update_upcoming_match(c); break; case 'match_called_on_court': + _attach_setup_announcement_claim(c, 'match_called_on_court'); _handle_announcement_event('match_called_on_court', c.val, () => announceNewMatch(c.val.setup)); break; case 'begin_to_play_call': + _attach_setup_announcement_claim(c, 'begin_to_play_call'); _handle_announcement_event('begin_to_play_call', c.val, () => announceBeginnToPlay(c.val.setup)); break; case 'second_call_tabletoperator': + _attach_setup_announcement_claim(c, 'second_call_tabletoperator'); _handle_announcement_event('second_call_tabletoperator', c.val, () => announceSecondCallTabletoperator(c.val.setup)); break; case 'second_call_umpire': + _attach_setup_announcement_claim(c, 'second_call_umpire'); _handle_announcement_event('second_call_umpire', c.val, () => announceSecondCallUmpire(c.val.setup)); break; case 'second_call_servicejudge': + _attach_setup_announcement_claim(c, 'second_call_servicejudge'); _handle_announcement_event('second_call_servicejudge', c.val, () => announceSecondCallServiceJudge(c.val.setup)); break; case 'second_call_team_one': + _attach_setup_announcement_claim(c, 'second_call_team_one'); _handle_announcement_event('second_call_team_one', c.val, () => announceSecondCallTeamOne(c.val.setup)); break; case 'second_call_team_two': + _attach_setup_announcement_claim(c, 'second_call_team_two'); _handle_announcement_event('second_call_team_two', c.val, () => announceSecondCallTeamTwo(c.val.setup)); break; case 'second_preparation_call_tabletoperator': + _attach_setup_announcement_claim(c, 'second_preparation_call_tabletoperator'); _handle_announcement_event('second_preparation_call_tabletoperator', c.val, () => announceSecondPreparationCallTabletoperator(c.val.setup)); break; case 'second_preparation_call_umpire': + _attach_setup_announcement_claim(c, 'second_preparation_call_umpire'); _handle_announcement_event('second_preparation_call_umpire', c.val, () => announceSecondPreparationCallUmpire(c.val.setup)); break; case 'second_preparation_call_servicejudge': + _attach_setup_announcement_claim(c, 'second_preparation_call_servicejudge'); _handle_announcement_event('second_preparation_call_servicejudge', c.val, () => announceSecondPreparationCallServiceJudge(c.val.setup)); break; case 'second_preparation_call_team_one': + _attach_setup_announcement_claim(c, 'second_preparation_call_team_one'); _handle_announcement_event('second_preparation_call_team_one', c.val, () => announceSecondPreparationCallTeamOne(c.val.setup)); break; case 'second_preparation_call_team_two': + _attach_setup_announcement_claim(c, 'second_preparation_call_team_two'); _handle_announcement_event('second_preparation_call_team_two', c.val, () => announceSecondPreparationCallTeamTwo(c.val.setup)); break; case 'btp_status': @@ -275,6 +353,9 @@ function default_handler(rerender, special_funcs) { case 'bts_status': ctournament.bts_status_changed(c); break; + case 'queue_hang_warning': + cerror.silent('BTS hängt in Aufgabe "' + c.val.task + '" seit ' + Math.round((c.val.runtime_ms || 0) / 1000) + 's.'); + break; case 'normalization_add': ctournament.add_normalization(c); break; @@ -301,13 +382,13 @@ function default_handler(rerender, special_funcs) { // m.setup = cval.setup;} // break; case 'umpires_changed': - curt.umpires = c.val.all_umpires; - uiu.qsEach('select[name="umpire_name"]', function(select) { - cmatch.render_umpire_options(select, select.value); - }); + apply_umpires_changed(c.val); break; case 'umpire_updated': const umpire = c.val; + if (ctournament && typeof ctournament.apply_pending_official_role_override === 'function') { + ctournament.apply_pending_official_role_override(umpire); + } const u = utils.find(curt.umpires, m => m._id === umpire._id); if (u) { u.last_time_on_court_ts = umpire.last_time_on_court_ts; @@ -328,6 +409,8 @@ function default_handler(rerender, special_funcs) { u.umpire_pause = umpire.umpire_pause; u.service_judge_pause = umpire.service_judge_pause; u.inactive_list = umpire.inactive_list; + } else { + curt.umpires.push(umpire); } if(current_view === 'show') { cumpires.ui_status(uiu.qs('.umpire_container')); @@ -353,15 +436,18 @@ function default_handler(rerender, special_funcs) { break; case 'official_list_move': const official = utils.find(curt.umpires, u => u._id === c.val.official_id); - official[c.val.inactive_list] = null; - official[c.val.service_judge_pause] = null; - official[c.val.umpire_pause] = null; - official[c.val.service_judge_wait] = null; - official[c.val.umpire_wait] = null; - official[c.val.service_judge_on_court] = null; - official[c.val.umpire_on_court] = null; - official[c.val.is_planed_as_service_judge] = false; - official[c.val.is_planed_as_umpirefrom_list] = false; + if (!official) { + break; + } + official.inactive_list = null; + official.service_judge_pause = null; + official.umpire_pause = null; + official.service_judge_wait = null; + official.umpire_wait = null; + official.service_judge_on_court = null; + official.umpire_on_court = null; + official.is_planed_as_service_judge = false; + official.is_planed_as_umpire = false; official[c.val.to_list] = c.val.new_ts; ctournament.update_officials(); break; @@ -494,7 +580,8 @@ function default_handler(rerender, special_funcs) { } return { - default_handler + default_handler, + _apply_umpires_changed: apply_umpires_changed }; })(); @@ -507,6 +594,7 @@ if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { var uiu = require('../bup/js/uiu'); var utils = require('./utils'); var cumpires = require('./cumpires'); + var change_helpers = require('./change_helpers'); module.exports = change; } /*/@DEV*/ diff --git a/static/js/change_helpers.js b/static/js/change_helpers.js new file mode 100644 index 0000000..236bb0d --- /dev/null +++ b/static/js/change_helpers.js @@ -0,0 +1,31 @@ +'use strict'; + +var change_helpers = (function() { + function apply_umpires_changed(update, deps) { + const { + curt_ref, + uiu_ref, + cmatch_ref, + current_view_ref, + cumpires_ref, + ctournament_ref + } = deps; + + curt_ref.umpires = update.all_umpires; + uiu_ref.qsEach('select[name="umpire_name"]', function(select) { + cmatch_ref.render_umpire_options(select, select.value); + }); + if(current_view_ref === 'show') { + cumpires_ref.ui_status(uiu_ref.qs('.umpire_container')); + } + ctournament_ref.update_officials(); + } + + return { + apply_umpires_changed + }; +})(); + +if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { + module.exports = change_helpers; +} diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index f51a2de..021d331 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -38,6 +38,8 @@ var ci18n_de = { 'Confirm_Finish': 'Spielende bestätigen', 'No umpire': 'Kein Schiedsrichter', 'No service judge': 'Kein Aufschlagrichter', + 'Waiting list umpire': 'Warteliste Schiedsrichter', + 'Waiting list service judge': 'Warteliste Aufschlagrichter', 'Edit match': 'Match bearbeiten', 'Edit display setting': 'Anzeigeeinstellung bearbeiten', 'Back': 'Zurück', @@ -65,6 +67,10 @@ var ci18n_de = { 'inactivate_court': 'Spielfeld sperren', 'Waiting for the next game:': 'Wartet auf das nächste Spiel:', 'Currently on break:': 'Aktuell in der Pause:', + 'On court:': 'Auf dem Feld:', + 'In preparation:': 'In Vorbereitung:', + 'Assigned to a match:': 'Einem Spiel zugewiesen:', + 'Assigned to matches in preparation:': 'Für Spiele in Vorbereitung zugewiesen:', 'Not available:': 'Nicht verfügbar:', @@ -279,8 +285,12 @@ var ci18n_de = { 'match:edit:scheduled_date': 'Datum:', 'match:edit:delete': 'Löschen', 'match:edit:now_on_court': 'Jetzt auf dem Feld', + 'match:edit:error:service_judge_requires_umpire': 'Ein Aufschlagrichter kann nur gesetzt werden, wenn auch ein Schiedsrichter gesetzt ist.', + 'match:edit:show_all_officials': 'Alle Offiziellen anzeigen', + 'match:edit:swap_hint': 'Tausch', 'match:delete:really': 'Wirklich Spiel {match_id} löschen?', - 'match:add_officials': 'Schiedsrichter und Aufschlagrichter hinzufügen', + 'match:add_umpire': 'Schiedsrichter hinzufügen', + 'match:add_service_judge': 'Aufschlagrichter hinzufügen', 'tournament:edit:logo': 'Logo', 'tournament:edit:logo:nologo': 'Kein Logo', 'tournament:edit:logo:upload': 'Logo hochladen', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 1b9f617..8719832 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -38,6 +38,8 @@ var ci18n_en = { 'Confirm_Finish': 'Confirm end of game', 'No umpire': 'No umpire', 'No service judge': 'No service judge', + 'Waiting list umpire': 'Waiting list umpire', + 'Waiting list service judge': 'Waiting list service judge', 'Edit match': 'Edit match', 'Edit display setting': 'Edit display setting', 'Back': 'Back', @@ -65,6 +67,10 @@ var ci18n_en = { 'inactivate_court': 'Spielfeld sperren', 'Waiting for the next game:': 'Waiting for the next game:', 'Currently on break:': 'Currently on break:', + 'On court:': 'On court:', + 'In preparation:': 'In preparation:', + 'Assigned to a match:': 'Assigned to a match:', + 'Assigned to matches in preparation:': 'Assigned to matches in preparation:', 'Not available:': 'Not available:', 'announcements:begin_to_play': 'Start to play!', @@ -274,8 +280,12 @@ var ci18n_en = { 'match:edit:scheduled_date': 'Date:', 'match:edit:delete': 'Delete match', 'match:edit:now_on_court': 'Now on court', + 'match:edit:error:service_judge_requires_umpire': 'A service judge can only be assigned if an umpire is also assigned.', + 'match:edit:show_all_officials': 'Show all officials', + 'match:edit:swap_hint': 'Swap', 'match:delete:really': 'Really delete match {match_id}?', - 'match:add_officials': 'Add umpire and service judge', + 'match:add_umpire': 'Add umpire', + 'match:add_service_judge': 'Add service judge', 'tournament:edit:logo': 'Logo', 'tournament:edit:logo:nologo': 'No logo', 'tournament:edit:logo:upload': 'Upload logo', diff --git a/static/js/cmatch.js b/static/js/cmatch.js index b6165a6..d57c48b 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -27,6 +27,18 @@ function calc_section(m) { return 'unassigned'; } +function resolve_match_court(match, court) { + if (court) { + return court; + } + if (!match || !match.setup || !match.setup.court_id || !curt) { + return null; + } + return (curt.courts_by_id && curt.courts_by_id[match.setup.court_id]) + || utils.find(curt.courts || [], (c) => c._id == match.setup.court_id) + || null; +} + function auto_scroll() { if (is_paused) { return; @@ -203,9 +215,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ return; } - if (!court && match.setup.court_id) { - court = curt.courts_by_id[match.setup.court_id]; - } + court = resolve_match_court(match, court); const completeMatch = (match.setup.teams[0].players.length >= 1 && match.setup.teams[1].players.length >= 1); @@ -372,28 +382,32 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ if(style != 'public') { const to_td = uiu.el(tr, 'td', 'umpire_and_tablet'); - const show_participant_check_in_status = !setup.now_on_court; + const show_participant_check_in_status = (style === 'unasigned'); if (style === 'default' || style === 'plain' || style === 'unasigned') { if (setup.umpire && setup.umpire.name) { const umpire_span = render_match_participant_el(to_td, setup.umpire, match._id, 'umpire', 'umpire', show_participant_check_in_status); - if(style === 'unasigned' && match.setup.highlight >= 1){ + if (style === 'unasigned' && match.setup.highlight >= 1) { create_match_button(umpire_span, 'vlink match_second_preparation_call_button', 'match:secondcallumpire', on_second_preparation_call_umpire_button_click, match._id); } - if (style === 'default' || style === 'plain' || style === 'unasigned') { create_match_button(umpire_span, 'vlink match_second_call_button', 'match:secondcallumpire', on_second_call_umpire_button_click, match._id); - } - if (setup.service_judge && setup.service_judge.name) { + if (setup.service_judge && setup.service_judge.name) { const service_judge_span = render_match_participant_el(to_td, setup.service_judge, match._id, 'service_judge', 'service_judge', show_participant_check_in_status); - if(style === 'unasigned' && match.setup.highlight >= 1){ + if (style === 'unasigned' && match.setup.highlight >= 1) { create_match_button(service_judge_span, 'vlink match_second_preparation_call_button', 'match:secondcallservicejudge', on_second_preparation_call_servicejudge_button_click, match._id); } - if (style === 'default' || style === 'plain' || style === 'unasigned') { create_match_button(service_judge_span, 'vlink match_second_call_button', 'match:secondcallservicejudge', on_second_call_servicejudge_button_click, match._id); + } else { + const umpire_icon = umpire_span.querySelector('.umpire'); + if (umpire_icon) { + umpire_icon.classList.add('can_add_service_judge'); + umpire_icon.setAttribute('title', ci18n('match:add_service_judge')); + umpire_icon.setAttribute('data-match_id', match._id); + umpire_icon.addEventListener('click', on_add_service_judge_button); + } } } - } if (setup.tabletoperators && setup.tabletoperators.length > 0) { const tablet_div = uiu.el(to_td, 'div', 'tablet_operator', ''); @@ -413,7 +427,8 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ if (!setup.umpire && (!setup.tabletoperators || setup.tabletoperators.length == 0)) { const no_umpire_span = uiu.el(to_td, 'span', 'person'); - create_match_button(no_umpire_span, 'vlink no_umpire', 'match:add_officials', on_add_officials_button, match._id); + const no_umpire_button_class = style === 'unasigned' ? 'vlink no_umpire no_umpire_add' : 'vlink no_umpire'; + create_match_button(no_umpire_span, no_umpire_button_class, 'match:add_umpire', on_add_officials_button, match._id); uiu.el(no_umpire_span, 'span', 'match_no_umpire', ci18n('No umpire')); } @@ -572,7 +587,7 @@ function render_match_row(tr, match, court, style, show_player_status, show_add_ return resizable_elements; } -function on_add_officials_button(e) { +const on_add_officials_button = (e) => { const match = fetchMatchFromEvent(e); if (match != null) { send({ @@ -585,7 +600,22 @@ function on_add_officials_button(e) { } }); } -} +}; + +const on_add_service_judge_button = (e) => { + const match = fetchMatchFromEvent(e); + if (match != null) { + send({ + type: 'add_service_judge_to_match', + tournament_key: curt.key, + match_id: match._id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + } +}; function short_name (first_names, last_name, name) { if(first_names && last_name){ @@ -601,7 +631,18 @@ function create_match_button(targetEl, cssClass, title, listener, matchId,) { 'title': ci18n(title), 'data-match_id': matchId, }); - btn.addEventListener('click', listener); + btn.draggable = false; + btn.addEventListener('mousedown', function (ev) { + ev.stopPropagation(); + }); + btn.addEventListener('dragstart', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + }); + btn.addEventListener('click', function (ev) { + ev.stopPropagation(); + listener(ev); + }); } function create_match_prepparation_button(targetEl, cssClass, title, listener, matchId, location){ @@ -805,7 +846,7 @@ function render_match_participant_el(parentNode, participant, match_id, role, ic participant_el.innerHTML = ''; uiu.el(participant_el, 'div', icon_class, ''); - uiu.el(participant_el, 'span', 'name', participant.name || short_name(participant.firstname, participant.lastname || participant.surname, participant.name)); + const name_el = uiu.el(participant_el, 'span', 'name', participant.name || short_name(participant.firstname, participant.lastname || participant.surname, participant.name)); if (show_check_in_status && participant.btp_id != null && participant.btp_id >= 0) { if (participant_status === 'checked_in') { @@ -814,7 +855,8 @@ function render_match_participant_el(parentNode, participant, match_id, role, ic participant_el.classList.add('can_check_in'); } - participant_el.addEventListener('click', function(ev) { + name_el.classList.add('person_status_target'); + name_el.addEventListener('click', function(ev) { send({ type: 'match_participant_check_in', match_id, @@ -828,6 +870,7 @@ function render_match_participant_el(parentNode, participant, match_id, role, ic } }); ev.stopPropagation(); + ev.preventDefault(); }, false); } @@ -856,9 +899,9 @@ function render_player_el(parentNode, player, match_id, now_on_court, show_playe if(curt.btp_settings.check_in_per_match) { send({ type: 'match_player_check_in', - match_id: ev.target.getAttribute("data-match_id"), - player_id: ev.target.getAttribute("data-btp_id"), - checked_in: (ev.target.classList.contains('not_checked_in') ? true : false), + match_id, + player_id: player.btp_id, + checked_in: (player_status == "not_checked_in"), tournament_key: curt.key }, function (err) { if (err) { @@ -866,6 +909,8 @@ function render_player_el(parentNode, player, match_id, now_on_court, show_playe } }); } + ev.stopPropagation(); + ev.preventDefault(); }, false); @@ -1437,8 +1482,10 @@ function on_announce_match_manually_button_click(e) { } } function fetchMatchFromEvent(e) { - const btn = e.target; - const match_id = btn.getAttribute('data-match_id'); + const btn = (e.currentTarget && e.currentTarget.getAttribute && e.currentTarget.getAttribute('data-match_id')) + ? e.currentTarget + : (e.target && e.target.closest ? e.target.closest('[data-match_id]') : null); + const match_id = btn ? btn.getAttribute('data-match_id') : null; const match = utils.find(curt.matches, m => m._id === match_id); if (!match) { cerror.silent('Match ' + match_id + ' konnte nicht gefunden werden'); @@ -1468,6 +1515,23 @@ function _nation_team_name(nat0, nat1) { return ''; } +function _pack_official_for_match_setup(official) { + if (!official) { + return official; + } + return { + _id: official._id, + btp_id: official.btp_id, + name: official.name, + firstname: official.firstname, + surname: official.surname, + country: official.country, + is_umpire: !!official.is_umpire, + is_service_judge: !!official.is_service_judge, + checked_in: false, + }; +} + function _update_setup(setup, d) { if(!setup) { return _make_setup(d); @@ -1499,11 +1563,11 @@ function _update_setup(setup, d) { for (const u of curt.umpires) { if (u.name === d.umpire_name) { - result.umpire = u; + result.umpire = _pack_official_for_match_setup(u); } if (u.name === d.service_judge_name) { - result.service_judge = u; + result.service_judge = _pack_official_for_match_setup(u); } } @@ -1653,6 +1717,10 @@ function ui_edit(match_id) { }, ci18n('Change')); form_utils.onsubmit(form, function(d) { + if (!d.umpire_name && d.service_judge_name) { + cerror.silent(ci18n('match:edit:error:service_judge_requires_umpire')); + return; + } match.setup = _update_setup(match.setup, d); btn.setAttribute('disabled', 'disabled'); send({ @@ -2114,6 +2182,7 @@ function create_court_button(targetEl, cssClass, title, listener, court_id, text function allowDrop(ev) { ev.preventDefault(); + ev.stopPropagation(); } function validate_match_complete(match_id) { @@ -2131,9 +2200,10 @@ function validate_match_complete(match_id) { } function drag(ev) { - let match_id = ev.target.getAttribute("data-match_id"); + const drag_source = ev.currentTarget || ev.target; + let match_id = drag_source && drag_source.getAttribute ? drag_source.getAttribute("data-match_id") : null; if (validate_match_complete(match_id)) { - ev.dataTransfer.setData('text', ev.target.getAttribute("data-match_id")); + ev.dataTransfer.setData('text', match_id); for (const dropp_row of document.getElementsByClassName ("droppable")) { dropp_row.classList.add("droppable_active"); @@ -2149,11 +2219,15 @@ function dragend(ev) { function drop(ev) { ev.preventDefault(); + ev.stopPropagation(); let match_id = ev.dataTransfer.getData('text'); + const drop_target = ev.currentTarget || ev.target; + const court_target = drop_target && drop_target.closest ? drop_target.closest('[data-court_id]') : drop_target; + const court_id = court_target && court_target.getAttribute ? court_target.getAttribute('data-court_id') : null; if (validate_match_complete(match_id)) { send({ type: 'match_call_on_court', - court_id: ev.target.getAttribute('data-court_id'), + court_id: court_id, match_id: match_id, tournament_key: curt.key, }, function (err) { @@ -2471,7 +2545,6 @@ function render_edit(form, match) { name: 'umpire_name', size: 1, }); - render_umpire_options(umpire_select, (setup.umpire && setup.umpire.name) ? setup.umpire.name : ''); // Service judge uiu.el(tos_container, 'span', { @@ -2482,7 +2555,64 @@ function render_edit(form, match) { name: 'service_judge_name', size: 1, }); - render_umpire_options(service_judge_select, (setup.service_judge && setup.service_judge.name) ? setup.service_judge.name : '' , true); + const show_all_officials_label = uiu.el(tos_container, 'label', { + style: 'margin-left: 1em;', + }); + const show_all_officials_checkbox = uiu.el(show_all_officials_label, 'input', { + type: 'checkbox', + name: 'show_all_officials', + }); + show_all_officials_label.appendChild(document.createTextNode(' ' + ci18n('match:edit:show_all_officials'))); + let current_umpire_value = (setup.umpire && setup.umpire.name) ? setup.umpire.name : ''; + let current_service_judge_value = (setup.service_judge && setup.service_judge.name) ? setup.service_judge.name : ''; + const rerender_official_selects = () => { + const umpire_value = current_umpire_value; + if (!umpire_value) { + current_service_judge_value = ''; + } + const service_judge_value = current_service_judge_value; + render_umpire_options( + umpire_select, + umpire_value, + false, + !!show_all_officials_checkbox.checked, + service_judge_value, + service_judge_value + ); + render_umpire_options( + service_judge_select, + service_judge_value, + true, + !!show_all_officials_checkbox.checked, + umpire_value, + umpire_value + ); + service_judge_select.disabled = !umpire_value; + }; + show_all_officials_checkbox.addEventListener('change', rerender_official_selects); + umpire_select.addEventListener('change', () => { + const previous_umpire_value = current_umpire_value; + const previous_service_judge_value = current_service_judge_value; + current_umpire_value = umpire_select.value; + if (current_umpire_value && current_umpire_value === previous_service_judge_value) { + current_service_judge_value = previous_umpire_value; + } else if (current_umpire_value && current_umpire_value === current_service_judge_value) { + current_service_judge_value = ''; + } + rerender_official_selects(); + }); + service_judge_select.addEventListener('change', () => { + const previous_umpire_value = current_umpire_value; + const previous_service_judge_value = current_service_judge_value; + current_service_judge_value = service_judge_select.value; + if (current_service_judge_value && current_service_judge_value === previous_umpire_value) { + current_umpire_value = previous_service_judge_value; + } else if (current_service_judge_value && current_service_judge_value === current_umpire_value) { + current_umpire_value = ''; + } + rerender_official_selects(); + }); + rerender_official_selects(); render_override_colors(edit_match_container, setup); } @@ -2547,20 +2677,214 @@ function update_override_color_checkbox(e) { } } -function render_umpire_options(select, curval, is_service_judge) { +function build_official_select_entries(tournament, is_service_judge, show_all_officials) { + const primary_role = is_service_judge ? 'service_judge' : 'umpire'; + const secondary_role = is_service_judge ? 'umpire' : 'service_judge'; + const role_label = (role) => role === 'umpire' ? ci18n('Umpire') : ci18n('Service judge'); + const with_role_label = (official, role) => `${official.name} (${role_label(role)})`; + const entries = []; + const append_separator = (label) => { + entries.push({ + type: 'separator', + label: `--- ${label} ---` + }); + }; + const append_options = (items, role, secondary_mode = false) => { + for (const official of items) { + entries.push({ + type: 'option', + value: official.name, + label: secondary_mode ? with_role_label(official, role) : official.name + }); + } + }; + if (show_all_officials) { + const visible_official_ids = new Set(); + const mark_visible = (official) => { + if (official && official._id) { + visible_official_ids.add(official._id); + } + }; + const sort_by_name = (items) => [...items].sort((a, b) => cbts_utils.natcmp(a.name || '', b.name || '')); + const sort_by_wait = (items, field) => [...items].sort((a, b) => { + const ts_a = a[field] || 0; + const ts_b = b[field] || 0; + if (ts_a !== ts_b) return ts_a - ts_b; + return cbts_utils.natcmp(a.name || '', b.name || ''); + }); + const all_officials = tournament.umpires || []; + const should_render_in_lower_lists = (official) => !visible_official_ids.has(official._id); + const primary_wait_field = `${primary_role}_wait`; + const secondary_wait_field = `${secondary_role}_wait`; + const primary_pause_field = `${primary_role}_pause`; + const secondary_pause_field = `${secondary_role}_pause`; + const preparation_matches = [...(curt.matches || [])] + .filter((match) => (match.setup || {}).state === 'preparation') + .sort((a, b) => (a.setup?.preparation_call_timestamp || 0) - (b.setup?.preparation_call_timestamp || 0)); + const assigned_matches = [...(curt.matches || [])] + .filter((match) => { + const setup = match.setup || {}; + return setup.state !== 'preparation' + && !['oncourt', 'blocked', 'finished'].includes(setup.state) + && ((setup.umpire && setup.umpire._id) || (setup.service_judge && setup.service_judge._id)); + }) + .sort((a, b) => cbts_utils.natcmp(String(a.setup?.match_num || ''), String(b.setup?.match_num || ''))); + preparation_matches.forEach((match) => { + mark_visible(match.setup && match.setup.umpire); + mark_visible(match.setup && match.setup.service_judge); + }); + assigned_matches.forEach((match) => { + mark_visible(match.setup && match.setup.umpire); + mark_visible(match.setup && match.setup.service_judge); + }); + for (const official of all_officials) { + if (official.umpire_on_court != null || official.service_judge_on_court != null) { + mark_visible(official); + } + } + const primary_wait_items = sort_by_wait( + all_officials.filter((u) => u[primary_wait_field] != null && should_render_in_lower_lists(u)), + primary_wait_field + ); + const secondary_wait_items = sort_by_wait( + all_officials.filter((u) => u[secondary_wait_field] != null && should_render_in_lower_lists(u)), + secondary_wait_field + ); + const primary_pause_items = sort_by_wait( + all_officials.filter((u) => u[primary_pause_field] != null && should_render_in_lower_lists(u)), + primary_pause_field + ); + const secondary_pause_items = sort_by_wait( + all_officials.filter((u) => u[secondary_pause_field] != null && should_render_in_lower_lists(u)), + secondary_pause_field + ); + const preparation_primary = sort_by_name(preparation_matches.map((match) => match.setup && match.setup[primary_role]).filter(Boolean)); + const preparation_secondary = sort_by_name(preparation_matches.map((match) => match.setup && match.setup[secondary_role]).filter(Boolean)); + const assigned_primary = sort_by_name(assigned_matches.map((match) => match.setup && match.setup[primary_role]).filter(Boolean)); + const assigned_secondary = sort_by_name(assigned_matches.map((match) => match.setup && match.setup[secondary_role]).filter(Boolean)); + const inactive_primary = sort_by_wait( + all_officials.filter((u) => u.inactive_list != null && should_render_in_lower_lists(u) && !(u.is_service_judge && !u.is_umpire)), + 'inactive_list' + ); + const inactive_secondary = sort_by_wait( + all_officials.filter((u) => u.inactive_list != null && should_render_in_lower_lists(u) && u.is_service_judge && !u.is_umpire), + 'inactive_list' + ); + const fallback_inactive_officials = all_officials + .filter((u) => !visible_official_ids.has(u._id)) + .filter((u) => u.umpire_wait == null && u.service_judge_wait == null && u.umpire_pause == null && u.service_judge_pause == null && u.inactive_list == null) + .sort((a, b) => cbts_utils.natcmp(a.name || '', b.name || '')); + fallback_inactive_officials.forEach((official) => { + if (official.is_umpire && !official.is_service_judge) { + inactive_primary.push(official); + return; + } + if (official.is_service_judge && !official.is_umpire) { + inactive_secondary.push(official); + return; + } + inactive_primary.push(official); + }); + const on_court_primary = sort_by_name(all_officials.filter((u) => u[`${primary_role}_on_court`] != null)); + const on_court_secondary = sort_by_name(all_officials.filter((u) => u[`${secondary_role}_on_court`] != null)); + const sections = [ + { label: primary_role === 'umpire' ? ci18n('Waiting list umpire') : ci18n('Waiting list service judge'), items: primary_wait_items, role: primary_role, secondary_mode: false }, + { label: secondary_role === 'umpire' ? ci18n('Waiting list umpire') : ci18n('Waiting list service judge'), items: secondary_wait_items, role: secondary_role, secondary_mode: true }, + { label: ci18n('Currently on break:') + ' ' + role_label(primary_role), items: primary_pause_items, role: primary_role, secondary_mode: false }, + { label: ci18n('Currently on break:') + ' ' + role_label(secondary_role), items: secondary_pause_items, role: secondary_role, secondary_mode: true }, + { label: ci18n('Assigned to a match:'), items: assigned_primary, role: primary_role, secondary_mode: false }, + { label: ci18n('Assigned to a match:'), items: assigned_secondary, role: secondary_role, secondary_mode: true }, + { label: ci18n('Not available:'), items: inactive_primary, role: primary_role, secondary_mode: false }, + { label: ci18n('Not available:'), items: inactive_secondary, role: secondary_role, secondary_mode: true }, + { label: ci18n('In preparation:'), items: preparation_primary, role: primary_role, secondary_mode: false }, + { label: ci18n('In preparation:'), items: preparation_secondary, role: secondary_role, secondary_mode: true }, + { label: ci18n('On court:'), items: on_court_primary, role: primary_role, secondary_mode: false }, + { label: ci18n('On court:'), items: on_court_secondary, role: secondary_role, secondary_mode: true }, + ]; + let rendered_any = false; + sections.forEach((section) => { + if (!section.items.length) return; + if (rendered_any) { + append_separator(section.label.replace(/:$/, '')); + } + append_options(section.items, section.role, section.secondary_mode); + rendered_any = true; + }); + return entries; + } + const primary_wait_field = is_service_judge ? 'service_judge_wait' : 'umpire_wait'; + const secondary_wait_field = is_service_judge ? 'umpire_wait' : 'service_judge_wait'; + const sort_wait_list = (wait_field) => [...(tournament.umpires || [])] + .filter((u) => u[wait_field] != null) + .sort((a, b) => { + const ts_a = a[wait_field] || 0; + const ts_b = b[wait_field] || 0; + if (ts_a !== ts_b) return ts_a - ts_b; + return cbts_utils.natcmp(a.name || '', b.name || ''); + }); + const officials = [ + ...sort_wait_list(primary_wait_field).map((u) => ({ official: u, label: u.name })), + ...sort_wait_list(secondary_wait_field).map((u) => ({ official: u, label: with_role_label(u, secondary_wait_field === 'umpire_wait' ? 'umpire' : 'service_judge') })) + ]; + const primary_count = sort_wait_list(primary_wait_field).length; + const secondary_label = secondary_wait_field === 'umpire_wait' + ? '--- ' + ci18n('Waiting list umpire') + ' ---' + : '--- ' + ci18n('Waiting list service judge') + ' ---'; + for (const [index, entry] of officials.entries()) { + if (index === primary_count && officials.length > primary_count) { + entries.push({ + type: 'separator', + label: secondary_label + }); + } + entries.push({ + type: 'option', + value: entry.official.name, + label: entry.label + }); + } + return entries; +} + +function render_umpire_options(select, curval, is_service_judge, show_all_officials, disabled_value, swap_value) { uiu.empty(select); uiu.el(select, 'option', { value: '', style: 'font-style: italic;', }, is_service_judge ? ci18n('No service judge') : ci18n('No umpire')); - for (const u of curt.umpires) { + const entries = build_official_select_entries(curt, is_service_judge, show_all_officials); + const has_option = (value) => !!value && entries.some((entry) => entry.type === 'option' && entry.value === value); + const has_current_option = has_option(curval); + if (!!curval && !has_current_option) { + uiu.el(select, 'option', { + value: curval, + selected: 'selected', + }, curval); + } + if (!!swap_value && swap_value !== curval && !has_option(swap_value)) { + uiu.el(select, 'option', { + value: swap_value, + }, `${swap_value} (${ci18n('match:edit:swap_hint')})`); + } + for (const entry of entries) { + if (entry.type === 'separator') { + uiu.el(select, 'option', { + value: '', + disabled: 'disabled', + style: 'font-style: italic;', + }, entry.label); + continue; + } const attrs = { - value: u.name, + value: entry.value, }; - if (u.name === curval) { + if (disabled_value && entry.value === disabled_value && entry.value !== curval && entry.value !== swap_value) { + attrs.disabled = 'disabled'; + } + if (entry.value === curval) { attrs.selected = 'selected'; } - uiu.el(select, 'option', attrs, u.name); + uiu.el(select, 'option', attrs, entry.label); } } @@ -2613,7 +2937,8 @@ return { remove_match_from_gui, update_players, create_timer, - update_tables + update_tables, + _build_official_select_entries: build_official_select_entries }; })(); diff --git a/static/js/cmatch_official_select_helpers.js b/static/js/cmatch_official_select_helpers.js new file mode 100644 index 0000000..727fe88 --- /dev/null +++ b/static/js/cmatch_official_select_helpers.js @@ -0,0 +1,177 @@ +'use strict'; + +var cmatch_official_select_helpers = (function() { + function build_official_select_entries(tournament, is_service_judge, show_all_officials, deps) { + const { ci18n_fn, natcmp_fn } = deps; + const primary_role = is_service_judge ? 'service_judge' : 'umpire'; + const secondary_role = is_service_judge ? 'umpire' : 'service_judge'; + const role_label = (role) => role === 'umpire' ? ci18n_fn('Umpire') : ci18n_fn('Service judge'); + const with_role_label = (official, role) => `${official.name} (${role_label(role)})`; + const entries = []; + const append_separator = (label) => { + entries.push({ type: 'separator', label: `--- ${label} ---` }); + }; + const append_options = (items, role, secondary_mode = false) => { + for (const official of items) { + entries.push({ + type: 'option', + value: official.name, + label: secondary_mode ? with_role_label(official, role) : official.name + }); + } + }; + const sort_by_name = (items) => [...items].sort((a, b) => natcmp_fn(a.name || '', b.name || '')); + const sort_by_wait = (items, field) => [...items].sort((a, b) => { + const ts_a = a[field] || 0; + const ts_b = b[field] || 0; + if (ts_a !== ts_b) return ts_a - ts_b; + return natcmp_fn(a.name || '', b.name || ''); + }); + + if (show_all_officials) { + const visible_official_ids = new Set(); + const mark_visible = (official) => { + if (official && official._id) { + visible_official_ids.add(official._id); + } + }; + const all_officials = tournament.umpires || []; + const preparation_matches = [...(tournament.matches || [])] + .filter((match) => (match.setup || {}).state === 'preparation') + .sort((a, b) => (a.setup?.preparation_call_timestamp || 0) - (b.setup?.preparation_call_timestamp || 0)); + const assigned_matches = [...(tournament.matches || [])] + .filter((match) => { + const setup = match.setup || {}; + return setup.state !== 'preparation' + && !['oncourt', 'blocked', 'finished'].includes(setup.state) + && ((setup.umpire && setup.umpire._id) || (setup.service_judge && setup.service_judge._id)); + }) + .sort((a, b) => natcmp_fn(String(a.setup?.match_num || ''), String(b.setup?.match_num || ''))); + preparation_matches.forEach((match) => { + mark_visible(match.setup && match.setup.umpire); + mark_visible(match.setup && match.setup.service_judge); + }); + assigned_matches.forEach((match) => { + mark_visible(match.setup && match.setup.umpire); + mark_visible(match.setup && match.setup.service_judge); + }); + for (const official of all_officials) { + if (official.umpire_on_court != null || official.service_judge_on_court != null) { + mark_visible(official); + } + } + const should_render_in_lower_lists = (official) => !visible_official_ids.has(official._id); + const primary_wait_field = `${primary_role}_wait`; + const secondary_wait_field = `${secondary_role}_wait`; + const primary_pause_field = `${primary_role}_pause`; + const secondary_pause_field = `${secondary_role}_pause`; + const primary_wait_items = sort_by_wait( + all_officials.filter((u) => u[primary_wait_field] != null && should_render_in_lower_lists(u)), + primary_wait_field + ); + const secondary_wait_items = sort_by_wait( + all_officials.filter((u) => u[secondary_wait_field] != null && should_render_in_lower_lists(u)), + secondary_wait_field + ); + const primary_pause_items = sort_by_wait( + all_officials.filter((u) => u[primary_pause_field] != null && should_render_in_lower_lists(u)), + primary_pause_field + ); + const secondary_pause_items = sort_by_wait( + all_officials.filter((u) => u[secondary_pause_field] != null && should_render_in_lower_lists(u)), + secondary_pause_field + ); + const preparation_primary = sort_by_name(preparation_matches.map((match) => match.setup && match.setup[primary_role]).filter(Boolean)); + const preparation_secondary = sort_by_name(preparation_matches.map((match) => match.setup && match.setup[secondary_role]).filter(Boolean)); + const assigned_primary = sort_by_name(assigned_matches.map((match) => match.setup && match.setup[primary_role]).filter(Boolean)); + const assigned_secondary = sort_by_name(assigned_matches.map((match) => match.setup && match.setup[secondary_role]).filter(Boolean)); + const inactive_primary = sort_by_wait( + all_officials.filter((u) => u.inactive_list != null && should_render_in_lower_lists(u) && !(u.is_service_judge && !u.is_umpire)), + 'inactive_list' + ); + const inactive_secondary = sort_by_wait( + all_officials.filter((u) => u.inactive_list != null && should_render_in_lower_lists(u) && u.is_service_judge && !u.is_umpire), + 'inactive_list' + ); + const fallback_inactive_officials = all_officials + .filter((u) => !visible_official_ids.has(u._id)) + .filter((u) => u.umpire_wait == null && u.service_judge_wait == null && u.umpire_pause == null && u.service_judge_pause == null && u.inactive_list == null) + .sort((a, b) => natcmp_fn(a.name || '', b.name || '')); + fallback_inactive_officials.forEach((official) => { + if (official.is_umpire && !official.is_service_judge) { + inactive_primary.push(official); + return; + } + if (official.is_service_judge && !official.is_umpire) { + inactive_secondary.push(official); + return; + } + inactive_primary.push(official); + }); + const on_court_primary = sort_by_name(all_officials.filter((u) => u[`${primary_role}_on_court`] != null)); + const on_court_secondary = sort_by_name(all_officials.filter((u) => u[`${secondary_role}_on_court`] != null)); + const sections = [ + { label: primary_role === 'umpire' ? ci18n_fn('Waiting list umpire') : ci18n_fn('Waiting list service judge'), items: primary_wait_items, role: primary_role, secondary_mode: false }, + { label: secondary_role === 'umpire' ? ci18n_fn('Waiting list umpire') : ci18n_fn('Waiting list service judge'), items: secondary_wait_items, role: secondary_role, secondary_mode: true }, + { label: ci18n_fn('Currently on break:') + ' ' + role_label(primary_role), items: primary_pause_items, role: primary_role, secondary_mode: false }, + { label: ci18n_fn('Currently on break:') + ' ' + role_label(secondary_role), items: secondary_pause_items, role: secondary_role, secondary_mode: true }, + { label: ci18n_fn('Assigned to a match:'), items: assigned_primary, role: primary_role, secondary_mode: false }, + { label: ci18n_fn('Assigned to a match:'), items: assigned_secondary, role: secondary_role, secondary_mode: true }, + { label: ci18n_fn('Not available:'), items: inactive_primary, role: primary_role, secondary_mode: false }, + { label: ci18n_fn('Not available:'), items: inactive_secondary, role: secondary_role, secondary_mode: true }, + { label: ci18n_fn('In preparation:'), items: preparation_primary, role: primary_role, secondary_mode: false }, + { label: ci18n_fn('In preparation:'), items: preparation_secondary, role: secondary_role, secondary_mode: true }, + { label: ci18n_fn('On court:'), items: on_court_primary, role: primary_role, secondary_mode: false }, + { label: ci18n_fn('On court:'), items: on_court_secondary, role: secondary_role, secondary_mode: true }, + ]; + let rendered_any = false; + sections.forEach((section) => { + if (!section.items.length) return; + if (rendered_any) { + append_separator(section.label.replace(/:$/, '')); + } + append_options(section.items, section.role, section.secondary_mode); + rendered_any = true; + }); + return entries; + } + + const primary_wait_field = is_service_judge ? 'service_judge_wait' : 'umpire_wait'; + const secondary_wait_field = is_service_judge ? 'umpire_wait' : 'service_judge_wait'; + const sort_wait_list = (wait_field) => [...(tournament.umpires || [])] + .filter((u) => u[wait_field] != null) + .sort((a, b) => { + const ts_a = a[wait_field] || 0; + const ts_b = b[wait_field] || 0; + if (ts_a !== ts_b) return ts_a - ts_b; + return natcmp_fn(a.name || '', b.name || ''); + }); + const officials = [ + ...sort_wait_list(primary_wait_field).map((u) => ({ official: u, label: u.name })), + ...sort_wait_list(secondary_wait_field).map((u) => ({ official: u, label: with_role_label(u, secondary_wait_field === 'umpire_wait' ? 'umpire' : 'service_judge') })) + ]; + const primary_count = sort_wait_list(primary_wait_field).length; + const secondary_label = secondary_wait_field === 'umpire_wait' + ? '--- ' + ci18n_fn('Waiting list umpire') + ' ---' + : '--- ' + ci18n_fn('Waiting list service judge') + ' ---'; + for (const [index, entry] of officials.entries()) { + if (index === primary_count && officials.length > primary_count) { + entries.push({ type: 'separator', label: secondary_label }); + } + entries.push({ + type: 'option', + value: entry.official.name, + label: entry.label + }); + } + return entries; + } + + return { + build_official_select_entries + }; +})(); + +if ((typeof module !== 'undefined') && (typeof require !== 'undefined')) { + module.exports = cmatch_official_select_helpers; +} diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 467ece6..28dbd25 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -12,6 +12,11 @@ let self_check_in_measure_probe = null; let self_check_in_fit_scheduled = false; let self_check_in_fit_roots = new Set(); let self_check_in_called_overlay_timeout = null; +let skip_next_official_list_move = null; +let official_drag_image_el = null; +let official_drag_active = false; +let official_drag_refresh_pending = false; +let pending_official_role_overrides = new Map(); var ctournament = (function() { function _route_single(rex, func, handler) { @@ -89,6 +94,30 @@ var ctournament = (function() { list_show(response.tournaments); }); } + + function set_pending_official_role_override(official_id, values) { + if (!official_id) return; + pending_official_role_overrides.set(official_id, { + is_umpire: !!values.is_umpire, + is_service_judge: !!values.is_service_judge + }); + } + + function apply_pending_official_role_override(official) { + if (!official || !official._id) return official; + const pending = pending_official_role_overrides.get(official._id); + if (!pending) return official; + if ( + !!official.is_umpire === pending.is_umpire && + !!official.is_service_judge === pending.is_service_judge + ) { + pending_official_role_overrides.delete(official._id); + return official; + } + official.is_umpire = pending.is_umpire; + official.is_service_judge = pending.is_service_judge; + return official; + } crouting.register(/^t\/$/, ui_list, change.default_handler); function list_show(tournaments) { @@ -3072,74 +3101,39 @@ var ctournament = (function() { * ============================================================ */ function add_drop_zones_to_tbody(tbody, { - row_selector = 'tr', + row_selector = '.officials_row', zone_class = 'drop-zone', zone_active_class = 'drop-zones-active', - is_header_row = (tr) => !!tr.querySelector('th'), + is_header_row = (row) => row.classList.contains('officials_list_header'), col_count = 3, on_zone_dragover = (tbody, insertBeforeRow, e) => {}, } = {}) { - // alte Zonen entfernen - for (const z of [...tbody.querySelectorAll(`tr.${zone_class}`)]) z.remove(); + for (const z of [...tbody.querySelectorAll(`.${zone_class}`)]) z.remove(); tbody.classList.add(zone_active_class); const rows = [...tbody.querySelectorAll(row_selector)]; const header = rows.find(is_header_row) || null; - // Spacer erkennen (kommt von ensure_min_table_height) - const spacer = tbody.querySelector('tr.table-spacer') || null; - - // Datenzeilen: data-official-id (und NICHT spacer) - const dataRows = rows.filter(tr => - tr !== header && - tr !== spacer && - tr.getAttribute('data-official-id') + const dataRows = rows.filter(row => + row !== header && + row.getAttribute('data-official-id') && + !row.classList.contains(zone_class) ); - function makeZone(insertBeforeRow, heightPx = null) { - const ztr = document.createElement('tr'); - ztr.className = zone_class; - - const ztd = document.createElement('td'); - ztd.colSpan = col_count; - ztr.appendChild(ztd); - - if (heightPx != null) { - ztd.style.height = `${heightPx}px`; - ztd.style.padding = '0'; - ztd.style.border = 'none'; - } - - ztr.addEventListener('dragover', (e) => { + function makeZone(insertBeforeRow) { + const zone = document.createElement('div'); + zone.className = `officials_drop_zone ${zone_class}`; + zone.addEventListener('dragover', (e) => { e.preventDefault(); on_zone_dragover(tbody, insertBeforeRow, e); }); - - ztr.addEventListener('drop', (e) => e.preventDefault()); - ztr.addEventListener('dragenter', () => ztr.classList.add('drop-zone-hover')); - ztr.addEventListener('dragleave', () => ztr.classList.remove('drop-zone-hover')); - - return ztr; - } - - // Wenn Spacer existiert: Spacer zu einer einzigen großen Dropzone machen - if (spacer) { - // Spacer-Höhe ermitteln (td height oder inline style) - const spacerTd = spacer.querySelector('td'); - const h = spacerTd ? spacerTd.getBoundingClientRect().height : 0; - - // Spacer entfernen und als Dropzone mit gleicher Höhe ersetzen. - // Drop soll "unter letzte Datenzeile" sein -> insertBeforeRow = null - spacer.remove(); - - const bigZone = makeZone(null, Math.max(0, Math.ceil(h))); - tbody.appendChild(bigZone); - return; + zone.addEventListener('drop', (e) => e.preventDefault()); + zone.addEventListener('dragenter', () => zone.classList.add('drop-zone-hover')); + zone.addEventListener('dragleave', () => zone.classList.remove('drop-zone-hover')); + return zone; } - // Normalfall ohne Spacer: Top/Between/Bottom Zonen const topZone = makeZone(dataRows[0] || null); - if (header) { if (header.nextSibling) tbody.insertBefore(topZone, header.nextSibling); else tbody.appendChild(topZone); @@ -3162,7 +3156,7 @@ function remove_drop_zones_from_tbody(tbody, { zone_active_class = 'drop-zones-active', } = {}) { tbody.classList.remove(zone_active_class); - for (const z of [...tbody.querySelectorAll(`tr.${zone_class}`)]) { + for (const z of [...tbody.querySelectorAll(`.${zone_class}`)]) { z.remove(); } } @@ -3172,11 +3166,11 @@ function remove_drop_zones_from_tbody(tbody, { * ============================================================ */ function enable_multitable_row_dragdrop(tbodies, { - row_selector = 'tr', + row_selector = '.officials_row', table_id_attr = 'data-table-id', row_id_attr = 'data-official-id', - is_header_row = (tr) => !!tr.querySelector('th'), - can_drag_row = (tr) => !is_header_row(tr) && !tr.classList.contains('drop-zone'), + is_header_row = (row) => row.classList.contains('officials_list_header'), + can_drag_row = (row) => !is_header_row(row) && !row.classList.contains('drop-zone'), col_count = 3, on_move = ({ row_id, from_table, to_table, from_order, to_order }) => {}, } = {}) { @@ -3195,24 +3189,16 @@ function enable_multitable_row_dragdrop(tbodies, { function get_order_ids(tbody) { if (!tbody) return []; return [...tbody.querySelectorAll(row_selector)] - .filter(tr => !is_header_row(tr) && !tr.classList.contains('drop-zone')) - .map(tr => tr.getAttribute(row_id_attr)) + .filter(row => !is_header_row(row) && !row.classList.contains('drop-zone')) + .map(row => row.getAttribute(row_id_attr)) .filter(Boolean); } - // >>> NEU: ans Ende bedeutet "vor Spacer", falls vorhanden - function append_before_spacer(tbody, row) { - const spacer = tbody.querySelector('tr.table-spacer'); - if (spacer) tbody.insertBefore(row, spacer); - else tbody.appendChild(row); - } - function insert_dragged_into_tbody(tbody, insertBeforeRow) { if (!dragged_tr) return; if (insertBeforeRow == null) { - // ans Ende (unter letzte Datenzeile) => aber vor Spacer, falls vorhanden - append_before_spacer(tbody, dragged_tr); + tbody.appendChild(dragged_tr); } else { tbody.insertBefore(dragged_tr, insertBeforeRow); } @@ -3272,14 +3258,25 @@ function enable_multitable_row_dragdrop(tbodies, { const row_id = dragged_tr.getAttribute(row_id_attr) || ''; const to_tbody = dragged_tr.closest('tbody'); - const from_table = get_table_id(from_tbody); - const to_table = get_table_id(to_tbody); + const from_table = get_table_id(from_tbody); + const to_table = get_table_id(to_tbody); - const from_order = get_order_ids(from_tbody); - const to_order = get_order_ids(to_tbody); + const from_order = get_order_ids(from_tbody); + const to_order = get_order_ids(to_tbody); + if ( + skip_next_official_list_move && + skip_next_official_list_move.row_id === row_id && + skip_next_official_list_move.from_table === from_table + ) { + skip_next_official_list_move = null; dragged_tr = null; from_tbody = null; + return; + } + + dragged_tr = null; + from_tbody = null; on_move({ row_id, from_table, to_table, from_order, to_order }); }); @@ -3292,31 +3289,19 @@ for (const tbody of tbodies) { e.preventDefault(); // 1) Wenn wir über dem Spacer sind: vor Spacer einfügen (Preview korrekt) - const spacer_tr = e.target.closest ? e.target.closest('tr.table-spacer') : null; - if (spacer_tr) { - tbody.insertBefore(dragged_tr, spacer_tr); - return; - } - - // 2) Wenn wir über dem Header sind: vor die erste Datenzeile einfügen - const header_tr = e.target.closest ? e.target.closest('tr') : null; - const isHeader = header_tr && header_tr.querySelector && header_tr.querySelector('th'); + const header_tr = e.target.closest ? e.target.closest('.officials_row') : null; + const isHeader = header_tr && is_header_row(header_tr); if (isHeader) { - const first_data = tbody.querySelector(`tr[${row_id_attr}]:not(.drop-zone)`); + const first_data = tbody.querySelector(`${row_selector}[${row_id_attr}]:not(.drop-zone)`); if (first_data) { tbody.insertBefore(dragged_tr, first_data); } else { - // keine Datenzeile vorhanden -> vor Spacer falls vorhanden, sonst ans Ende - const spacer = tbody.querySelector('tr.table-spacer'); - if (spacer) tbody.insertBefore(dragged_tr, spacer); - else tbody.appendChild(dragged_tr); + tbody.appendChild(dragged_tr); } return; } - // 3) Optional: Wenn über einer Datenzeile, nahe oben -> davor einfügen - // (macht "oben" noch leichter zu treffen, ohne Drop-Zones zu vergrößern) - const data_tr = e.target.closest ? e.target.closest(`tr[${row_id_attr}]`) : null; + const data_tr = e.target.closest ? e.target.closest(`${row_selector}[${row_id_attr}]`) : null; if (data_tr && data_tr !== dragged_tr) { const box = data_tr.getBoundingClientRect(); const before = e.clientY < (box.top + box.height / 2); @@ -3340,107 +3325,86 @@ for (const tbody of tbodies) { * DEINE TABELLE (pro Feld) - gibt TBODY zurück * ============================================================ */ +function create_official_role_checkbox(host, official, field) { + const wrap = uiu.el(host, 'div', 'officials_role_toggle'); + const cb = create_simple_checkbox( + wrap, + { name: `${field}_cb`, 'data-official-id': official._id || '' }, + !!official[field] + ); + if (!official._id) { + cb.disabled = true; + return cb; + } + cb.addEventListener('change', (e) => { + send_with_live_status({ + type: 'official_edit', + tournament_key: official.tournament_key, + official_id: official._id, + field, + value: e.target.checked + }, err => { if (err) return cerror.net(err); }); + }); + return cb; +} + function render_officials_table(main, { title = null, rows, table_id, - min_height_px = 240 + name_header = 'Name', + first_cell_render = null, + leading_header = null, + leading_cell_render = null }) { if (title) { uiu.el(main, 'h2', 'edit', title); } - const table = uiu.el(main, 'table', 'officials_table'); - const tbody = uiu.el(table, 'tbody', { 'data-table-id': table_id }); - - /* ---------- Header ---------- */ - const trHead = uiu.el(tbody, 'tr'); - uiu.el(trHead, 'th', {}, 'Name'); - - const thUmpire = uiu.el(trHead, 'th', {}); - uiu.el(thUmpire, 'div', { class: 'umpire' }); + const list = uiu.el(main, 'div', 'officials_list'); + list.setAttribute('data-table-id', table_id); - const thService = uiu.el(trHead, 'th', {}); - uiu.el(thService, 'div', { class: 'service_judge' }); - - /* ---------- Data rows ---------- */ - for (const o of rows) { - const tr = uiu.el(tbody, 'tr', { 'data-official-id': o._id }); - - uiu.el(tr, 'td', {}, o.name || `${o.firstname} ${o.surname}`.trim()); - - const umpire_td = uiu.el(tr, 'td', {}); - const umpire_cb = create_simple_checkbox( - umpire_td, - { name: 'umpire_cb', 'data-official-id': o._id }, - !!o.is_umpire - ); - umpire_cb.addEventListener('change', (e) => { - send_with_live_status({ - type: 'official_edit', - tournament_key: o.tournament_key, - official_id: o._id, - field: 'is_umpire', - value: e.target.checked - }, err => { if (err) return cerror.net(err); }); - }); - - const service_td = uiu.el(tr, 'td', {}); - const service_cb = create_simple_checkbox( - service_td, - { name: 'service_judge_cb', 'data-official-id': o._id }, - !!o.is_service_judge - ); - service_cb.addEventListener('change', (e) => { - send_with_live_status({ - type: 'official_edit', - tournament_key: o.tournament_key, - official_id: o._id, - field: 'is_service_judge', - value: e.target.checked - }, err => { if (err) return cerror.net(err); }); - }); - } - - /* ---------- Mindesthöhe per Spacer ---------- */ - function ensure_min_table_height() { - const old = tbody.querySelector('tr.table-spacer'); - if (old) old.remove(); - - if (!document.body.contains(table)) return; - - requestAnimationFrame(() => { - if (!document.body.contains(table)) return; - - const current = table.getBoundingClientRect().height; - const missing = Math.max(0, min_height_px - current); - if (missing <= 0) return; - - const spacer_tr = document.createElement('tr'); - spacer_tr.className = 'table-spacer'; - - const spacer_td = document.createElement('td'); - spacer_td.colSpan = trHead.children.length; - spacer_td.style.height = `${Math.ceil(missing)}px`; - - spacer_tr.appendChild(spacer_td); - tbody.appendChild(spacer_tr); - }); + const header = uiu.el(list, 'div', 'officials_row officials_list_header'); + if (leading_header !== null) { + uiu.el(header, 'div', 'officials_cell officials_cell_leading', leading_header); } + uiu.el(header, 'div', 'officials_cell officials_cell_name', name_header); + const umpireHead = uiu.el(header, 'div', 'officials_cell officials_cell_role'); + uiu.el(umpireHead, 'div', { class: 'umpire' }); + const serviceHead = uiu.el(header, 'div', 'officials_cell officials_cell_role'); + uiu.el(serviceHead, 'div', { class: 'service_judge' }); + + rows.forEach((o) => { + const row = uiu.el(list, 'div', 'officials_row', { 'data-official-id': o._id || '' }); + + if (leading_cell_render) { + const leading = uiu.el(row, 'div', 'officials_cell officials_cell_leading'); + leading_cell_render(leading, o); + } - ensure_min_table_height(); + const nameCell = uiu.el(row, 'div', 'officials_cell officials_cell_name'); + if (first_cell_render) { + first_cell_render(nameCell, o); + } else { + uiu.text(nameCell, o.name || `${o.firstname} ${o.surname}`.trim()); + } - // für globalen Resize-Recalc - table._ensureMinHeight = ensure_min_table_height; + create_official_role_checkbox(uiu.el(row, 'div', 'officials_cell officials_cell_role'), o, 'is_umpire'); + create_official_role_checkbox(uiu.el(row, 'div', 'officials_cell officials_cell_role'), o, 'is_service_judge'); + }); - return { table, tbody }; + return { table: list, tbody: list }; } function render_officials_by_timestamp(main, { title = null, officials, timestamp_field, - min_height_px = 240 + min_height_px = 240, + name_header = 'Name', + first_cell_render = null, + leading_header = null, + leading_cell_render = null }) { const rows = officials .filter(o => o[timestamp_field] !== null) @@ -3450,143 +3414,1162 @@ function render_officials_by_timestamp(main, { title, rows, table_id: timestamp_field, - min_height_px + min_height_px, + name_header, + first_cell_render, + leading_header, + leading_cell_render }); } -function enable_min_height_resize_recalc(tables) { - window._officialMinHeightTables = tables; +function render_officials_by_filter(main, { + title = null, + officials, + table_id, + filter_fn, + sort_fn = null, + min_height_px = 240, + name_header = 'Name', + first_cell_render = null, + leading_header = null, + leading_cell_render = null +}) { + const rows = officials.filter(filter_fn); + if (sort_fn) { + rows.sort(sort_fn); + } else { + rows.sort((a, b) => cbts_utils.natcmp(a.name || '', b.name || '')); + } - if (window._officialMinHeightResizeHandlerInstalled) return; - window._officialMinHeightResizeHandlerInstalled = true; + return render_officials_table(main, { + title, + rows, + table_id, + min_height_px, + name_header, + first_cell_render, + leading_header, + leading_cell_render + }); +} - window.addEventListener('resize', () => { - const list = window._officialMinHeightTables || []; - for (const t of list) { - if (t && t._ensureMinHeight) t._ensureMinHeight(); - } +function render_official_role_split_section(main, { + title, + left_table_id, + right_table_id, + left_filter_fn, + right_filter_fn, + first_cell_render = null, + left_leading_header = null, + left_leading_cell_render = null, + sort_fn = null, + min_height_px = 240 +}) { + uiu.el(main, 'h3', 'edit', title); + const section_div = uiu.el(main, 'div', 'official_split_section'); + + const left_div = uiu.el(section_div, 'div', 'official_role_split_column'); + const left = render_officials_by_filter(left_div, { + officials: curt.umpires, + table_id: left_table_id, + filter_fn: left_filter_fn, + sort_fn, + min_height_px, + name_header: ci18n('Umpire'), + first_cell_render, + leading_header: left_leading_header, + leading_cell_render: left_leading_cell_render + }); + + uiu.el(section_div, 'div', 'official_role_split_space'); + + const right_div = uiu.el(section_div, 'div', 'official_role_split_column'); + const right = render_officials_by_filter(right_div, { + officials: curt.umpires, + table_id: right_table_id, + filter_fn: right_filter_fn, + sort_fn, + min_height_px, + name_header: ci18n('Service judge'), + first_cell_render }); + + return { left, right }; } -function update_official_tables(officials_host) { - // officials_host: DOM-Element, in das die gesamte Officials-UI gerendert wird - // curt.umpires ist hier verfügbar (wie von dir beschrieben) +function render_on_court_officials_table(main, { + rows, + table_id, + min_height_px = 240, + table_class = 'officials_table_on_court', + leading_cell_render = null +}) { + const table = uiu.el(main, 'div', `officials_dual_list ${table_class}`, { 'data-table-id': table_id }); + + const head = uiu.el(table, 'div', 'officials_dual_row officials_dual_header'); + uiu.el(head, 'div', 'officials_dual_cell officials_cell_leading', ''); + uiu.el(head, 'div', 'officials_dual_cell officials_cell_name', ci18n('Umpire')); + const headLeftUmpire = uiu.el(head, 'div', 'officials_dual_cell officials_cell_role'); + uiu.el(headLeftUmpire, 'div', { class: 'umpire' }); + const headLeftService = uiu.el(head, 'div', 'officials_dual_cell officials_cell_role'); + uiu.el(headLeftService, 'div', { class: 'service_judge' }); + uiu.el(head, 'div', 'officials_dual_cell officials_dual_center_space', ''); + uiu.el(head, 'div', 'officials_dual_cell officials_cell_name', ci18n('Service judge')); + const headRightUmpire = uiu.el(head, 'div', 'officials_dual_cell officials_cell_role'); + uiu.el(headRightUmpire, 'div', { class: 'umpire' }); + const headRightService = uiu.el(head, 'div', 'officials_dual_cell officials_cell_role'); + uiu.el(headRightService, 'div', { class: 'service_judge' }); + + for (const row of rows) { + const tr = uiu.el(table, 'div', 'officials_dual_row'); + if (row.match_id) { + tr.setAttribute('data-match-id', row.match_id); + } + const leftOfficial = row.left; + const rightOfficial = row.right; - // alles neu bauen - officials_host.innerHTML = ''; + const leadingTd = uiu.el(tr, 'div', 'officials_dual_cell officials_cell_leading'); + if (leading_cell_render) { + leading_cell_render(leadingTd, row, leftOfficial, rightOfficial); + } else { + const courtClass = leftOfficial._is_empty_on_court_slot ? 'court officials_table_court_inactive' : 'court'; + uiu.el(leadingTd, 'div', courtClass, row.court_num || ''); + } - const tbodies = []; - const tables = []; + const leftNameTd = uiu.el(tr, 'div', row.match_id ? { + class: 'officials_dual_cell officials_cell_name official_assignment_slot', + 'data-match-id': row.match_id, + 'data-role': 'umpire', + 'data-slot-group': `${row.match_id}:umpire`, + 'data-official-id': leftOfficial._id || '' + } : { class: 'officials_dual_cell officials_cell_name' }); + uiu.text(leftNameTd, leftOfficial.name || `${leftOfficial.firstname} ${leftOfficial.surname}`.trim()); + + const leftUmpireTd = uiu.el(tr, 'div', 'officials_dual_cell officials_cell_role'); + if (!row.match_id && !leftOfficial._id) { + // leerer On-Court-Slot: keine Checkbox anzeigen + } else if (!row.match_id || leftOfficial._id) { + create_official_role_checkbox(leftUmpireTd, leftOfficial, 'is_umpire'); + } else { + leftUmpireTd.classList.add('official_assignment_slot'); + leftUmpireTd.setAttribute('data-match-id', row.match_id); + leftUmpireTd.setAttribute('data-role', 'umpire'); + leftUmpireTd.setAttribute('data-slot-group', `${row.match_id}:umpire`); + leftUmpireTd.setAttribute('data-official-id', ''); + } - const officials_div = uiu.el(officials_host, 'div', 'settings'); - uiu.el(officials_div, 'h2', 'edit', ci18n('Umpire:')); + const leftServiceTd = uiu.el(tr, 'div', 'officials_dual_cell officials_cell_role'); + if (!row.match_id && !leftOfficial._id) { + // leerer On-Court-Slot: keine Checkbox anzeigen + } else if (!row.match_id || leftOfficial._id) { + create_official_role_checkbox(leftServiceTd, leftOfficial, 'is_service_judge'); + } else { + leftServiceTd.classList.add('official_assignment_slot'); + leftServiceTd.setAttribute('data-match-id', row.match_id); + leftServiceTd.setAttribute('data-role', 'umpire'); + leftServiceTd.setAttribute('data-slot-group', `${row.match_id}:umpire`); + leftServiceTd.setAttribute('data-official-id', ''); + } - uiu.el(officials_div, 'h3', 'edit', ci18n('Waiting for the next game:')); - const waiting_officials_div = uiu.el(officials_div, 'div', 'paralel'); + uiu.el(tr, 'div', 'officials_dual_cell officials_dual_center_space', ''); + + const rightNameTd = uiu.el(tr, 'div', row.match_id ? { + class: 'officials_dual_cell officials_cell_name official_assignment_slot', + 'data-match-id': row.match_id, + 'data-role': 'service_judge', + 'data-slot-group': `${row.match_id}:service_judge`, + 'data-official-id': rightOfficial._id || '' + } : { class: 'officials_dual_cell officials_cell_name' }); + uiu.text(rightNameTd, rightOfficial.name || `${rightOfficial.firstname} ${rightOfficial.surname}`.trim()); + + const rightUmpireTd = uiu.el(tr, 'div', 'officials_dual_cell officials_cell_role'); + if (!row.match_id && !rightOfficial._id) { + // leerer On-Court-Slot: keine Checkbox anzeigen + } else if (!row.match_id || rightOfficial._id) { + create_official_role_checkbox(rightUmpireTd, rightOfficial, 'is_umpire'); + } else { + rightUmpireTd.classList.add('official_assignment_slot'); + rightUmpireTd.setAttribute('data-match-id', row.match_id); + rightUmpireTd.setAttribute('data-role', 'service_judge'); + rightUmpireTd.setAttribute('data-slot-group', `${row.match_id}:service_judge`); + rightUmpireTd.setAttribute('data-official-id', ''); + } - { - const r = render_officials_by_timestamp(waiting_officials_div, { - officials: curt.umpires, - timestamp_field: 'umpire_wait' - }); - tbodies.push(r.tbody); - tables.push(r.table); + const rightServiceTd = uiu.el(tr, 'div', 'officials_dual_cell officials_cell_role'); + if (!row.match_id && !rightOfficial._id) { + // leerer On-Court-Slot: keine Checkbox anzeigen + } else if (!row.match_id || rightOfficial._id) { + create_official_role_checkbox(rightServiceTd, rightOfficial, 'is_service_judge'); + } else { + rightServiceTd.classList.add('official_assignment_slot'); + rightServiceTd.setAttribute('data-match-id', row.match_id); + rightServiceTd.setAttribute('data-role', 'service_judge'); + rightServiceTd.setAttribute('data-slot-group', `${row.match_id}:service_judge`); + rightServiceTd.setAttribute('data-official-id', ''); + } } - uiu.el(waiting_officials_div, 'div', 'space'); + return { table, tbody: table }; +} + +function enable_min_height_resize_recalc(tables) { + return tables; +} - { - const r = render_officials_by_timestamp(waiting_officials_div, { - officials: curt.umpires, - timestamp_field: 'service_judge_wait' +function enable_preparation_official_dragdrop(preparation_table, lower_tbodies, officialById) { + if (!preparation_table) return; + + const set_drag_meta = (meta) => { + window._dragged_official_meta = meta; + }; + const clear_drag_meta = () => { + window._dragged_official_meta = null; + }; + const set_slot_group_hover = (slot, active) => { + const group = slot.getAttribute('data-slot-group'); + if (!group) return; + preparation_table.querySelectorAll(`.official_assignment_slot[data-slot-group=${JSON.stringify(group)}]`).forEach((groupSlot) => { + groupSlot.classList.toggle('drop-zone-hover', !!active); + }); + }; + const set_slot_group_dragging = (slot, active) => { + const group = slot.getAttribute('data-slot-group'); + if (!group) return; + preparation_table.querySelectorAll(`.official_assignment_slot[data-slot-group=${JSON.stringify(group)}]`).forEach((groupSlot) => { + groupSlot.classList.toggle('dragging', !!active); + }); + }; + const set_lower_dropzones_active = (active) => { + for (const tbody of lower_tbodies) { + if (active) { + add_drop_zones_to_tbody(tbody, { + col_count: 3, + on_zone_dragover: () => {} + }); + } else { + remove_drop_zones_from_tbody(tbody); + } + } + }; + const remove_official_drag_image = () => { + if (official_drag_image_el && official_drag_image_el.parentNode) { + official_drag_image_el.parentNode.removeChild(official_drag_image_el); + } + official_drag_image_el = null; + }; + const create_official_drag_image = (slot) => { + remove_official_drag_image(); + const group = slot.getAttribute('data-slot-group'); + if (!group) return null; + const groupSlots = [...preparation_table.querySelectorAll(`.official_assignment_slot[data-slot-group=${JSON.stringify(group)}]`)]; + if (!groupSlots.length) return null; + + const table = document.createElement('table'); + table.className = 'official_drag_image_table'; + const tbody = document.createElement('tbody'); + const tr = document.createElement('tr'); + table.appendChild(tbody); + tbody.appendChild(tr); + + groupSlots.forEach((groupSlot) => { + const clone = groupSlot.cloneNode(true); + clone.classList.remove('drop-zone-hover'); + clone.classList.add('official_drag_image_cell'); + clone.style.width = `${Math.ceil(groupSlot.getBoundingClientRect().width)}px`; + clone.style.height = `${Math.ceil(groupSlot.getBoundingClientRect().height)}px`; + tr.appendChild(clone); }); - tbodies.push(r.tbody); - tables.push(r.table); - } - uiu.el(officials_div, 'h3', 'edit', ci18n('Currently on break:')); - const paused_officials_div = uiu.el(officials_div, 'div', 'paralel'); + table.style.position = 'fixed'; + table.style.left = '-10000px'; + table.style.top = '-10000px'; + table.style.pointerEvents = 'none'; + table.style.zIndex = '9999'; + document.body.appendChild(table); + official_drag_image_el = table; + return table; + }; + + for (const tbody of lower_tbodies) { + for (const tr of tbody.querySelectorAll('.officials_row[data-official-id]')) { + tr.addEventListener('dragstart', () => { + const official_id = tr.getAttribute('data-official-id'); + if (!official_id) return; + set_drag_meta({ + source_type: 'list', + official_id, + from_table: tbody.getAttribute('data-table-id') || '', + official: officialById.get(official_id) || null + }); + preparation_table.classList.add('drop-zones-active'); + }); + tr.addEventListener('dragend', () => { + clear_drag_meta(); + preparation_table.classList.remove('drop-zones-active'); + preparation_table.querySelectorAll('.official_assignment_slot.drop-zone-hover').forEach((slot) => { + slot.classList.remove('drop-zone-hover'); + }); + }); + } - { - const r = render_officials_by_timestamp(paused_officials_div, { - officials: curt.umpires, - timestamp_field: 'umpire_pause' + tbody.addEventListener('dragover', (e) => { + const meta = window._dragged_official_meta; + if (!meta || meta.source_type !== 'match') return; + e.preventDefault(); + }); + tbody.addEventListener('drop', (e) => { + const meta = window._dragged_official_meta; + if (!meta || meta.source_type !== 'match') return; + e.preventDefault(); + clear_drag_meta(); + send_with_live_status({ + type: 'remove_official_from_preparation_match', + tournament_key: curt.key, + official_id: meta.official_id, + match_id: meta.match_id, + role: meta.role, + to_list: tbody.getAttribute('data-table-id') || '' + }, (err) => { + if (err) return cerror.net(err); + }); }); - tbodies.push(r.tbody); - tables.push(r.table); } - uiu.el(paused_officials_div, 'div', 'space'); + for (const slot of preparation_table.querySelectorAll('.official_assignment_slot')) { + const official_id = slot.getAttribute('data-official-id'); + const match_id = slot.getAttribute('data-match-id'); + const role = slot.getAttribute('data-role'); + + if (official_id) { + slot.draggable = true; + slot.addEventListener('dragstart', (e) => { + set_drag_meta({ + source_type: 'match', + official_id, + match_id, + role + }); + set_slot_group_dragging(slot, true); + preparation_table.classList.add('drop-zones-active'); + set_lower_dropzones_active(true); + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', official_id); + const dragImage = create_official_drag_image(slot); + if (dragImage) { + e.dataTransfer.setDragImage(dragImage, 10, 10); + } + } + }); + slot.addEventListener('dragend', () => { + clear_drag_meta(); + set_slot_group_dragging(slot, false); + preparation_table.classList.remove('drop-zones-active'); + set_lower_dropzones_active(false); + remove_official_drag_image(); + }); + } else { + slot.classList.add('drop-zone'); + } - { - const r = render_officials_by_timestamp(paused_officials_div, { - officials: curt.umpires, - timestamp_field: 'service_judge_pause' + slot.addEventListener('dragover', (e) => { + const meta = window._dragged_official_meta; + if (!meta || meta.source_type !== 'list' || slot.getAttribute('data-official-id')) return; + if (!meta.official) return; + if (role === 'umpire' && !meta.official.is_umpire) return; + if (role === 'service_judge' && !meta.official.is_service_judge) return; + e.preventDefault(); + set_slot_group_hover(slot, true); }); - tbodies.push(r.tbody); - tables.push(r.table); - } - uiu.el(officials_div, 'h3', 'edit', ci18n('Not available:')); - { - const r = render_officials_by_timestamp(officials_div, { - officials: curt.umpires, - timestamp_field: 'inactive_list' + slot.addEventListener('dragenter', (e) => { + const meta = window._dragged_official_meta; + if (!meta || meta.source_type !== 'list' || slot.getAttribute('data-official-id')) return; + if (!meta.official) return; + if (role === 'umpire' && !meta.official.is_umpire) return; + if (role === 'service_judge' && !meta.official.is_service_judge) return; + e.preventDefault(); + set_slot_group_hover(slot, true); }); - tbodies.push(r.tbody); - tables.push(r.table); - } - // Map neu aufbauen (wichtig, weil curt.umpires aktualisiert ist) - const officialById = new Map(); - for (const o of curt.umpires) { - officialById.set(o._id, o); + slot.addEventListener('dragleave', () => { + set_slot_group_hover(slot, false); + }); + + slot.addEventListener('drop', (e) => { + const meta = window._dragged_official_meta; + if (!meta || meta.source_type !== 'list' || slot.getAttribute('data-official-id')) return; + if (!meta.official) return; + if (role === 'umpire' && !meta.official.is_umpire) return; + if (role === 'service_judge' && !meta.official.is_service_judge) return; + e.preventDefault(); + set_slot_group_hover(slot, false); + clear_drag_meta(); + set_slot_group_dragging(slot, false); + preparation_table.classList.remove('drop-zones-active'); + set_lower_dropzones_active(false); + remove_official_drag_image(); + skip_next_official_list_move = { + row_id: meta.official_id, + from_table: meta.from_table || '' + }; + send_with_live_status({ + type: 'assign_official_to_preparation_match', + tournament_key: curt.key, + official_id: meta.official_id, + match_id, + role + }, (err) => { + if (err) return cerror.net(err); + }); + }); } +} - // Drag & Drop (jedes Render neu aktivieren) - enable_multitable_row_dragdrop(tbodies, { - col_count: 3, - is_header_row: (tr) => !!tr.querySelector('th'), - on_move: ({ row_id, from_table, to_table, from_order, to_order }) => { +function update_official_tables(officials_host) { + officials_host.innerHTML = ''; - // Mindesthöhe nach DOM-Move neu setzen - for (const t of tables) { - if (t && t._ensureMinHeight) t._ensureMinHeight(); + const officials_div = uiu.el(officials_host, 'div', 'settings'); + const all_officials = (curt.umpires || []).filter((official) => official && official._id); + const officialById = new Map(all_officials.map((official) => [official._id, official])); + let dragged_meta = null; + const sorted_courts = [...(curt.courts || [])].sort((a, b) => cbts_utils.natcmp(String(a.num || ''), String(b.num || ''))); + const preparation_matches = [...(curt.matches || [])] + .filter((match) => (match.setup || {}).state === 'preparation') + .sort((a, b) => (a.setup?.preparation_call_timestamp || 0) - (b.setup?.preparation_call_timestamp || 0)); + const assigned_matches = [...(curt.matches || [])] + .filter((match) => { + const setup = match.setup || {}; + return setup.state !== 'preparation' + && !['oncourt', 'blocked', 'finished'].includes(setup.state) + && ((setup.umpire && setup.umpire._id) || (setup.service_judge && setup.service_judge._id)); + }) + .sort((a, b) => cbts_utils.natcmp(String(a.setup?.match_num || ''), String(b.setup?.match_num || ''))); + const visible_official_ids = new Set(); + const mark_visible = (official) => { + if (official && official._id) { + visible_official_ids.add(official._id); + } + }; + + const official_display_name = (official) => official.name || `${official.firstname || ''} ${official.surname || ''}`.trim(); + const official_card_variant_class = (variant, official = null) => { + switch (variant) { + case 'on_court': + return 'official_card_variant_on_court'; + case 'assignment': + return official && official.checked_in + ? 'official_card_variant_checked_in' + : 'official_card_variant_not_checked_in'; + case 'list': + default: + return 'official_card_variant_list'; + } + }; + const role_state_from_official = (official) => { + if (!!official.is_umpire && !!official.is_service_judge) return 'all'; + if (!!official.is_umpire) return 'umpire_only'; + if (!!official.is_service_judge) return 'service_only'; + return 'all'; + }; + const cycle_role_state = (official, current_state) => { + if (current_state === 'all') { + return official.is_umpire ? 'umpire_only' : 'service_only'; + } + if (current_state === 'umpire_only') return 'service_only'; + return 'all'; + }; + const clear_same_stack_active_drop = () => { + officials_div.querySelectorAll('.official_card_drop_hover_expand').forEach((el) => { + el.classList.remove('official_card_drop_hover_expand', 'official_card_drop_hover'); + }); + officials_div.querySelectorAll('.official_card_stack_has_nonterminal_hover').forEach((el) => { + el.classList.remove('official_card_stack_has_nonterminal_hover'); + }); + if (dragged_meta) { + dragged_meta.active_drop = null; + } + }; + const activate_drop_target = (drop) => { + if (!dragged_meta || !drop) return; + if (dragged_meta.active_drop === drop) return; + if (dragged_meta.active_drop) { + const previous_stack = dragged_meta.active_drop.parentElement; + if (previous_stack?.classList.contains('official_card_stack')) { + previous_stack.classList.remove('official_card_stack_has_nonterminal_hover'); } - - // prev/next aus der Ziel-Reihenfolge - const idx = to_order.indexOf(row_id); - const prev_id = idx > 0 ? to_order[idx - 1] : null; - const next_id = (idx >= 0 && idx < to_order.length - 1) ? to_order[idx + 1] : null; - - const prev_btp_id = prev_id ? officialById.get(prev_id)?.btp_id : null; - const next_btp_id = next_id ? officialById.get(next_id)?.btp_id : null; - + dragged_meta.active_drop.classList.remove('official_card_drop_hover', 'official_card_drop_hover_expand'); + } + dragged_meta.active_drop = drop; + drop.classList.add('official_card_drop_hover'); + if (drop.parentElement?.classList.contains('official_card_stack')) { + if (!drop.classList.contains('official_card_drop_terminal')) { + drop.parentElement.classList.add('official_card_stack_has_nonterminal_hover'); + } + drop.classList.add('official_card_drop_hover_expand'); + } + }; + const set_source_placeholder_state = (state) => { + if (!dragged_meta?.source_card) return; + const card = dragged_meta.source_card; + const in_stack = !!dragged_meta.source_stack; + card.classList.remove( + 'official_card_placeholder_compact', + 'official_card_placeholder_activelike', + 'official_card_placeholder_hoverlike' + ); + if (state === 'hover') { + card.classList.add('official_card_placeholder_hoverlike'); + return; + } + if (state === 'active') { + card.classList.add('official_card_placeholder_activelike'); + if (in_stack) { + card.classList.add('official_card_placeholder_compact'); + } + } + }; + const set_drop_highlight = (drop, active) => { + if (drop.classList.contains('official_card_drop_suppressed')) { + drop.classList.remove('official_card_drop_hover', 'official_card_drop_hover_expand'); + return; + } + const same_stack = !!dragged_meta?.source_stack && drop.parentElement === dragged_meta.source_stack; + if (!same_stack) { + if (active) { + activate_drop_target(drop); + set_source_placeholder_state('active'); + } else if (dragged_meta?.active_drop !== drop) { + drop.classList.remove('official_card_drop_hover', 'official_card_drop_hover_expand'); + } + document.querySelectorAll('.official_card_drop_suppressed').forEach((el) => { + el.classList.remove('official_card_drop_suppressed'); + }); + if (!dragged_meta?.active_drop) { + set_source_placeholder_state('hover'); + } + update_source_placeholder_suppression(); + return; + } + if (active) { + activate_drop_target(drop); + update_source_placeholder_suppression(); + set_source_placeholder_state('active'); + } else { + if (!dragged_meta?.active_drop) { + set_source_placeholder_state('hover'); + document.querySelectorAll('.official_card_drop_suppressed').forEach((el) => { + el.classList.remove('official_card_drop_suppressed'); + }); + update_source_placeholder_suppression(); + } + } + }; + const register_drop_target = (drop, can_drop) => { + drop._officialCanDrop = can_drop; + }; + const update_source_placeholder_suppression = () => { + document.querySelectorAll('.official_card_drop_suppressed').forEach((drop) => { + drop.classList.remove('official_card_drop_suppressed'); + }); + if (!dragged_meta?.source_card) { + return; + } + const card = dragged_meta.source_card; + if (card.previousElementSibling && card.previousElementSibling.classList.contains('official_card_drop')) { + card.previousElementSibling.classList.add('official_card_drop_suppressed'); + } + if (card.nextElementSibling && card.nextElementSibling.classList.contains('official_card_drop')) { + card.nextElementSibling.classList.add('official_card_drop_suppressed'); + } + }; + const move_placeholder_relative_to_card = (target_card, place_after) => { + if (!dragged_meta?.source_stack) { + return; + } + if (target_card.parentElement !== dragged_meta.source_stack) { + return; + } + const target_drop = place_after + ? target_card.nextElementSibling + : target_card.previousElementSibling; + if (target_drop && target_drop.classList.contains('official_card_drop')) { + set_drop_highlight(target_drop, true); + } + }; + const set_all_drop_targets_active = (active) => { + officials_div.classList.toggle('officials_drag_active', !!active && !!dragged_meta); + officials_div.querySelectorAll('.official_card_drop').forEach((drop) => { + if (drop.classList.contains('official_card_drop_suppressed')) { + drop.classList.remove('official_card_drop_active'); + return; + } + const can_drop = typeof drop._officialCanDrop === 'function' ? drop._officialCanDrop(dragged_meta) : false; + drop.classList.toggle('official_card_drop_active', !!active && !!dragged_meta && can_drop); + }); + }; + const clear_all_drop_states = () => { + officials_div.classList.remove('officials_drag_active'); + document.querySelectorAll('.official_card_drop_active').forEach((drop) => { + drop.classList.remove('official_card_drop_active'); + }); + document.querySelectorAll('.official_card_drop_hover').forEach((drop) => { + drop.classList.remove('official_card_drop_hover'); + }); + document.querySelectorAll('.official_card_drop_hover_expand').forEach((drop) => { + drop.classList.remove('official_card_drop_hover_expand'); + }); + document.querySelectorAll('.official_card_placeholder').forEach((card) => { + card.classList.remove('official_card_placeholder'); + }); + document.querySelectorAll('.official_card_placeholder_compact').forEach((card) => { + card.classList.remove('official_card_placeholder_compact'); + }); + document.querySelectorAll('.official_card_placeholder_hoverlike').forEach((card) => { + card.classList.remove('official_card_placeholder_hoverlike'); + }); + document.querySelectorAll('.official_card_drop_suppressed').forEach((drop) => { + drop.classList.remove('official_card_drop_suppressed'); + }); + document.querySelectorAll('.official_card_stack_has_nonterminal_hover').forEach((stack) => { + stack.classList.remove('official_card_stack_has_nonterminal_hover'); + }); + document.querySelectorAll('.official_section_body_stacklist').forEach((section) => { + section.style.height = ''; + }); + }; + const freeze_stack_section_heights = () => { + officials_div.querySelectorAll('.official_section_body_stacklist').forEach((section) => { + section.style.height = `${Math.ceil(section.getBoundingClientRect().height)}px`; + }); + }; + const apply_stack_section_base_heights = () => { + officials_div.querySelectorAll('.official_section_body_stacklist').forEach((section) => { + section.style.height = ''; + section.style.minHeight = ''; + const content_height = section.scrollHeight; + const bottom_buffer_px = 6; + section.style.minHeight = `${Math.ceil(content_height + bottom_buffer_px)}px`; + }); + }; + const clear_drop_states_global = () => { + dragged_meta = null; + official_drag_active = false; + clear_all_drop_states(); + if (official_drag_refresh_pending) { + official_drag_refresh_pending = false; + setTimeout(() => { + if (current_view === 'edit') { + ctournament.update_officials(); + } + }, 0); + } + }; + const document_drop_cleanup = () => setTimeout(() => clear_drop_states_global(), 0); + const window_dragend_cleanup = () => clear_drop_states_global(); + document.addEventListener('drop', document_drop_cleanup, true); + window.addEventListener('dragend', window_dragend_cleanup, true); + update_official_tables._document_drop_cleanup = document_drop_cleanup; + update_official_tables._window_dragend_cleanup = window_dragend_cleanup; + const get_stack_card_ids = (stack) => [...stack.querySelectorAll('.official_card_frame[data-official-id]')].map((card) => card.getAttribute('data-official-id')).filter(Boolean); + const move_meta_to_list = (stack, meta, to_list, error_handler = cerror.net) => { + const ids = get_stack_card_ids(stack); + const virtual_ids = ids.filter((id) => id !== meta.official_id); + virtual_ids.push(meta.official_id); + const idx = virtual_ids.indexOf(meta.official_id); + const prev_id = idx > 0 ? virtual_ids[idx - 1] : null; + const next_id = idx >= 0 && idx < virtual_ids.length - 1 ? virtual_ids[idx + 1] : null; + send_with_live_status({ + type: 'official_list_move', + tournament_key: curt.key, + official_id: meta.official_id, + from_list: meta.from_list, + to_list, + prev_btp_id: prev_id ? officialById.get(prev_id)?.btp_id : null, + next_btp_id: next_id ? officialById.get(next_id)?.btp_id : null + }, (err) => { + if (err) return error_handler(err); + }); + }; + const remove_meta_from_match_to_list = (stack, meta, to_list, before_official_id = null, error_handler = cerror.net) => { + const type = meta.source_type === 'assigned' + ? 'remove_official_from_match' + : 'remove_official_from_preparation_match'; + const ids = get_stack_card_ids(stack); + const virtual_ids = ids.filter((id) => id !== meta.official_id); + const insert_at = before_official_id ? virtual_ids.indexOf(before_official_id) : virtual_ids.length; + if (insert_at < 0) { + virtual_ids.push(meta.official_id); + } else { + virtual_ids.splice(insert_at, 0, meta.official_id); + } + send_with_live_status({ + type, + tournament_key: curt.key, + official_id: meta.official_id, + match_id: meta.match_id, + role: meta.role, + to_list, + ordered_official_ids: virtual_ids + }, (err) => { + if (err) return error_handler(err); + }); + }; + const render_official_card = (parent, official, icon_class, drag_meta_factory = null, stack_drop_options = null, variant = 'list') => { + const card = uiu.el(parent, 'div', `official_card_frame official_card_skin ${official_card_variant_class(variant, official)}`); + card.setAttribute('data-official-id', official._id); + card.draggable = !!drag_meta_factory; + uiu.el(card, 'div', `official_card_icon ${icon_class}`); + uiu.el(card, 'div', 'official_card_name', official_display_name(official)); + const icon_trail = uiu.el(card, 'div', 'official_card_trail'); + icon_trail.setAttribute('data-state', role_state_from_official(official)); + icon_trail.draggable = false; + uiu.el(icon_trail, 'div', 'official_card_trail_icon umpire'); + uiu.el(icon_trail, 'div', 'official_card_trail_swap', '⇄'); + uiu.el(icon_trail, 'div', 'official_card_trail_icon service_judge'); + icon_trail.addEventListener('mousedown', (e) => { + e.stopPropagation(); + }); + icon_trail.addEventListener('dragstart', (e) => { + e.preventDefault(); + e.stopPropagation(); + }); + icon_trail.addEventListener('click', (e) => { + e.stopPropagation(); + const current = icon_trail.getAttribute('data-state') || 'all'; + const next = cycle_role_state(official, current); + const previous_values = { + is_umpire: !!official.is_umpire, + is_service_judge: !!official.is_service_judge + }; + icon_trail.setAttribute('data-state', next); + const next_values = next === 'all' + ? { is_umpire: true, is_service_judge: true } + : next === 'umpire_only' + ? { is_umpire: true, is_service_judge: false } + : { is_umpire: false, is_service_judge: true }; + official.is_umpire = next_values.is_umpire; + official.is_service_judge = next_values.is_service_judge; + set_pending_official_role_override(official._id, next_values); + ctournament.update_officials(); send_with_live_status({ - type: 'official_list_move', + type: 'official_roles_edit', tournament_key: curt.key, - official_id: row_id, - from_list: from_table, - to_list: to_table, - prev_btp_id, - next_btp_id + official_id: official._id, + is_umpire: next_values.is_umpire, + is_service_judge: next_values.is_service_judge }, (err) => { - if (err) return cerror.net(err); + if (err) { + official.is_umpire = previous_values.is_umpire; + official.is_service_judge = previous_values.is_service_judge; + set_pending_official_role_override(official._id, previous_values); + ctournament.update_officials(); + return cerror.net(err); + } + }); + }); + if (drag_meta_factory) { + card.addEventListener('dragstart', (e) => { + if (icon_trail.contains(e.target)) { + e.preventDefault(); + return; + } + dragged_meta = drag_meta_factory(official); + official_drag_active = true; + dragged_meta.source_card = card; + dragged_meta.source_stack = parent.classList.contains('official_card_stack') ? parent : null; + dragged_meta.active_drop = null; + let drag_image = null; + if (e.dataTransfer) { + drag_image = card.cloneNode(true); + drag_image.classList.remove('official_card_dragging', 'official_card_placeholder'); + drag_image.style.position = 'fixed'; + drag_image.style.top = '-1000px'; + drag_image.style.left = '-1000px'; + drag_image.style.pointerEvents = 'none'; + drag_image.style.width = `${card.offsetWidth}px`; + drag_image.style.height = `${card.offsetHeight}px`; + document.body.appendChild(drag_image); + } + card.classList.add('official_card_dragging', 'official_card_placeholder'); + set_source_placeholder_state('hover'); + set_all_drop_targets_active(true); + update_source_placeholder_suppression(); + freeze_stack_section_heights(); + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', official._id); + e.dataTransfer.setDragImage(drag_image, 20, 19); + setTimeout(() => drag_image.remove(), 0); + } + }); + card.addEventListener('dragend', () => { + card.classList.remove('official_card_dragging'); + dragged_meta = null; + clear_all_drop_states(); + }); + } + if (parent.classList.contains('official_card_stack')) { + card.addEventListener('dragover', (e) => { + if (!dragged_meta) { + return; + } + const same_stack_drag = !!dragged_meta.source_stack && dragged_meta.source_stack === parent; + const can_drop_here = stack_drop_options && stack_drop_options.can_drop(dragged_meta); + if (!same_stack_drag && !can_drop_here) { + return; + } + if (dragged_meta.source_card === card) { + e.preventDefault(); + clear_same_stack_active_drop(); + set_source_placeholder_state('hover'); + update_source_placeholder_suppression(); + return; + } + e.preventDefault(); + const rect = card.getBoundingClientRect(); + const midpoint = rect.top + rect.height / 2; + const deadzone = Math.min(8, rect.height * 0.2); + if (Math.abs(e.clientY - midpoint) <= deadzone && dragged_meta.active_drop) { + return; + } + const place_after = e.clientY > midpoint; + move_placeholder_relative_to_card(card, place_after); + }); + card.addEventListener('drop', (e) => { + if (!dragged_meta || !stack_drop_options || !stack_drop_options.can_drop(dragged_meta)) { + return; + } + if (dragged_meta.source_card === card) { + e.preventDefault(); + clear_all_drop_states(); + return; + } + e.preventDefault(); + const rect = card.getBoundingClientRect(); + const midpoint = rect.top + rect.height / 2; + const place_after = e.clientY > midpoint; + let before_official_id = official._id; + if (place_after) { + const cards_in_stack = [...parent.querySelectorAll('.official_card_frame[data-official-id]')]; + const current_index = cards_in_stack.indexOf(card); + const next_card = current_index >= 0 ? cards_in_stack[current_index + 1] : null; + before_official_id = next_card ? next_card.getAttribute('data-official-id') : null; + } + clear_all_drop_states(); + stack_drop_options.on_drop(dragged_meta, before_official_id); + }); + } + return card; + }; + const render_card_stack_with_drops = (parent, icon_class, officials, options) => { + const append_drop = (before_official_id = null) => { + const drop = uiu.el(parent, 'div', 'official_card_frame official_card_drop'); + if (before_official_id == null) { + drop.classList.add('official_card_drop_terminal'); + } + register_drop_target(drop, options.can_drop); + drop.addEventListener('dragover', (e) => { + if (!dragged_meta || !options.can_drop(dragged_meta)) return; + e.preventDefault(); + set_drop_highlight(drop, true); + }); + drop.addEventListener('dragleave', () => { + set_drop_highlight(drop, false); }); + drop.addEventListener('drop', (e) => { + if (!dragged_meta || !options.can_drop(dragged_meta)) return; + e.preventDefault(); + set_drop_highlight(drop, false); + clear_all_drop_states(); + options.on_drop(dragged_meta, before_official_id); + }); + return drop; + }; + const filtered_officials = officials.filter(Boolean); + append_drop(filtered_officials[0]?._id || null); + filtered_officials.forEach((official, index) => { + render_official_card( + parent, + official, + icon_class, + options.drag_meta_factory, + options, + options.variant || 'list' + ); + append_drop(filtered_officials[index + 1]?._id || null); + }); + }; + const create_official_section = (title) => { + uiu.el(officials_div, 'h3', 'edit', title); + return uiu.el(officials_div, 'div', 'official_section_body'); + }; + const send_list_reorder = (meta, to_list, virtual_ids, error_handler = cerror.net) => { + const idx = virtual_ids.indexOf(meta.official_id); + const prev_id = idx > 0 ? virtual_ids[idx - 1] : null; + const next_id = idx >= 0 && idx < virtual_ids.length - 1 ? virtual_ids[idx + 1] : null; + send_with_live_status({ + type: 'official_list_move', + tournament_key: curt.key, + official_id: meta.official_id, + from_list: meta.from_list, + to_list, + ordered_official_ids: virtual_ids, + prev_official_id: prev_id, + next_official_id: next_id, + prev_btp_id: prev_id ? (officialById.get(prev_id)?.btp_id ?? null) : null, + next_btp_id: next_id ? (officialById.get(next_id)?.btp_id ?? null) : null + }, (err) => { + if (err) return error_handler(err); + }); + }; + const build_reordered_ids = (stack, official_id, before_official_id = null) => { + const ids = get_stack_card_ids(stack); + const virtual_ids = ids.filter((id) => id !== official_id); + const insert_at = before_official_id ? virtual_ids.indexOf(before_official_id) : virtual_ids.length; + if (insert_at < 0) { + virtual_ids.push(official_id); + } else { + virtual_ids.splice(insert_at, 0, official_id); + } + return virtual_ids; + }; + const meta_can_fill_role = (meta, role) => { + if (!meta) return false; + const official = meta.official || officialById.get(meta.official_id); + if (role === 'umpire') return !!official?.is_umpire; + if (role === 'service_judge') return !!official?.is_service_judge; + return false; + }; + const role_from_list_name = (list_name) => { + if (list_name === 'umpire_wait' || list_name === 'umpire_pause') return 'umpire'; + if (list_name === 'service_judge_wait' || list_name === 'service_judge_pause') return 'service_judge'; + return null; + }; + const meta_can_drop_to_list = (meta, to_list) => { + if (!meta) return false; + if (to_list === 'inactive_list') { + return meta.source_type === 'list' || meta.source_type === 'preparation' || meta.source_type === 'assigned'; + } + const role = role_from_list_name(to_list); + return !!role && meta_can_fill_role(meta, role); + }; + const render_match_assignment_row = (section, match, source_type, assign_type) => { + const setup = match.setup || {}; + mark_visible(setup.umpire); + mark_visible(setup.service_judge); + const row = uiu.el(section, 'div', 'official_on_court_row'); + const leading = uiu.el(row, 'div', 'official_on_court_leading official_preparation_leading'); + uiu.text(leading, `#${setup.match_num || ''}`); + const render_assignment_slot = (role, icon_class) => { + const slot = uiu.el(row, 'div', 'official_card_frame official_card_drop'); + const slot_enabled = () => role !== 'service_judge' || !!(setup.umpire && setup.umpire._id); + register_drop_target(slot, (meta) => { + if (!slot_enabled()) return false; + if (!meta || !['list', 'preparation', 'assigned'].includes(meta.source_type)) return false; + if (!meta_can_fill_role(meta, role)) return false; + if (slot.querySelector('[data-official-id]')) return false; + return true; + }); + if (!slot_enabled() && !setup[role]) { + slot.classList.add('official_card_drop_disabled'); + } + slot.addEventListener('dragover', (e) => { + if (!dragged_meta || !['list', 'preparation', 'assigned'].includes(dragged_meta.source_type)) return; + if (!slot_enabled()) return; + if (!meta_can_fill_role(dragged_meta, role)) return; + if (slot.querySelector('[data-official-id]')) return; + e.preventDefault(); + set_drop_highlight(slot, true); + }); + slot.addEventListener('dragleave', () => set_drop_highlight(slot, false)); + slot.addEventListener('drop', (e) => { + if (!dragged_meta || !['list', 'preparation', 'assigned'].includes(dragged_meta.source_type)) return; + if (!slot_enabled()) return; + if (!meta_can_fill_role(dragged_meta, role)) return; + if (slot.querySelector('[data-official-id]')) return; + e.preventDefault(); + set_drop_highlight(slot, false); + clear_all_drop_states(); + const payload = { + type: assign_type, + tournament_key: curt.key, + official_id: dragged_meta.official_id, + match_id: match._id, + role + }; + if (dragged_meta.source_type === 'preparation' || dragged_meta.source_type === 'assigned') { + payload.source_match_id = dragged_meta.match_id; + payload.source_type = dragged_meta.source_type; + payload.source_role = dragged_meta.role; + } + send_with_live_status(payload, (err) => { + if (err) return cerror.net(err); + }); + }); + const official = setup[role]; + if (official) { + const live_official = officialById.get(official._id) || official; + const assignment_official = { + ...live_official, + checked_in: official.checked_in + }; + render_official_card( + slot, + assignment_official, + icon_class, + (assigned_official) => ({ + source_type, + official_id: assigned_official._id, + official: assignment_official, + match_id: match._id, + role + }), + null, + 'assignment' + ); + } + }; + render_assignment_slot('umpire', 'umpire'); + render_assignment_slot('service_judge', 'service_judge'); + }; + const render_vertical_list_section = (title, specs, row_class = '') => { + const section = create_official_section(title); + section.classList.add('official_section_body_stacklist'); + const row = uiu.el(section, 'div', `official_on_court_row${row_class ? ' ' + row_class : ''}`); + uiu.el(row, 'div', 'official_on_court_leading official_preparation_leading'); + specs.forEach((spec) => { + const stack = uiu.el(row, 'div', 'official_card_stack'); + spec.items.forEach(mark_visible); + render_card_stack_with_drops(stack, spec.icon_class, spec.items, { + can_drop: (meta) => meta_can_drop_to_list(meta, spec.to_list), + drag_meta_factory: (official) => ({ source_type: 'list', official_id: official._id, from_list: spec.to_list, official }), + variant: spec.variant || 'list', + on_drop: (meta, before_official_id) => { + if (meta.source_type === 'preparation' || meta.source_type === 'assigned') { + remove_meta_from_match_to_list(stack, meta, spec.to_list, before_official_id); + return; + } + send_list_reorder(meta, spec.to_list, build_reordered_ids(stack, meta.official_id, before_official_id)); + } + }); + }); + return { section, row }; + }; + uiu.el(officials_div, 'h2', 'edit', ci18n('Umpire:')); + const on_court_placeholder_row = create_official_section(ci18n('On court:')); + sorted_courts.forEach((court) => { + const court_umpire = curt.umpires.find((official) => official.umpire_on_court === court._id); + const court_service_judge = curt.umpires.find((official) => official.service_judge_on_court === court._id); + const has_match_on_court = !!court.match_id; + mark_visible(court_umpire); + mark_visible(court_service_judge); + const court_row = uiu.el(on_court_placeholder_row, 'div', 'official_on_court_row'); + const court_leading = uiu.el(court_row, 'div', 'official_on_court_leading'); + uiu.el(court_leading, 'div', 'court', String(court.num || '')); + const umpire_slot = uiu.el(court_row, 'div', 'official_card_frame official_card_drop'); + const service_judge_slot = uiu.el(court_row, 'div', 'official_card_frame official_card_drop'); + register_drop_target(umpire_slot, () => false); + register_drop_target(service_judge_slot, () => false); + if (!has_match_on_court) { + umpire_slot.classList.add('official_card_drop_disabled'); + service_judge_slot.classList.add('official_card_drop_disabled'); + } + if (court_umpire) { + render_official_card( + umpire_slot, + court_umpire, + 'umpire', + null, + null, + 'on_court' + ); + } + if (court_service_judge) { + render_official_card( + service_judge_slot, + court_service_judge, + 'service_judge', + null, + null, + 'on_court' + ); } }); - - // Mindesthöhe bei Resize neu berechnen (globaler Handler) - enable_min_height_resize_recalc(tables); - - // Optional: direkt nach Render einmal neu setzen (falls Fonts/Layout verzögert) - for (const t of tables) { - if (t && t._ensureMinHeight) t._ensureMinHeight(); - } + const in_preparation_section = create_official_section(ci18n('In preparation:')); + preparation_matches.forEach((match) => { + render_match_assignment_row(in_preparation_section, match, 'preparation', 'assign_official_to_preparation_match'); + }); + const assigned_section = create_official_section(ci18n('Assigned to a match:')); + assigned_matches.forEach((match) => { + render_match_assignment_row(assigned_section, match, 'assigned', 'assign_official_to_match'); + }); + const should_render_in_lower_lists = (official) => !visible_official_ids.has(official._id); + const waiting_umpires = [...(curt.umpires || [])] + .filter((official) => official.umpire_wait && should_render_in_lower_lists(official)) + .sort((a, b) => (a.umpire_wait || 0) - (b.umpire_wait || 0)); + const waiting_service_judges = [...(curt.umpires || [])] + .filter((official) => official.service_judge_wait && should_render_in_lower_lists(official)) + .sort((a, b) => (a.service_judge_wait || 0) - (b.service_judge_wait || 0)); + const paused_umpires = [...(curt.umpires || [])] + .filter((official) => official.umpire_pause && should_render_in_lower_lists(official)) + .sort((a, b) => (a.umpire_pause || 0) - (b.umpire_pause || 0)); + const paused_service_judges = [...(curt.umpires || [])] + .filter((official) => official.service_judge_pause && should_render_in_lower_lists(official)) + .sort((a, b) => (a.service_judge_pause || 0) - (b.service_judge_pause || 0)); + const inactive_officials = [...(curt.umpires || [])] + .filter((official) => official.inactive_list && should_render_in_lower_lists(official)) + .sort((a, b) => (a.inactive_list || 0) - (b.inactive_list || 0)); + render_vertical_list_section(ci18n('Waiting for the next game:'), [ + { items: waiting_umpires, icon_class: 'umpire', to_list: 'umpire_wait' }, + { items: waiting_service_judges, icon_class: 'service_judge', to_list: 'service_judge_wait' } + ]); + render_vertical_list_section(ci18n('Currently on break:'), [ + { items: paused_umpires, icon_class: 'umpire', to_list: 'umpire_pause' }, + { items: paused_service_judges, icon_class: 'service_judge', to_list: 'service_judge_pause' } + ]); + const inactive_section = create_official_section(ci18n('Not available:')); + inactive_section.classList.add('official_section_body_stacklist'); + const inactive_row = uiu.el(inactive_section, 'div', 'official_on_court_row official_inactive_row'); + uiu.el(inactive_row, 'div', 'official_on_court_leading official_preparation_leading'); + const inactive_stack = uiu.el(inactive_row, 'div', 'official_card_stack'); + const fallback_inactive_officials = all_officials + .filter((official) => !visible_official_ids.has(official._id)) + .sort((a, b) => cbts_utils.natcmp(String(official_display_name(a)), String(official_display_name(b)))); + const all_inactive_officials = [...inactive_officials]; + fallback_inactive_officials.forEach((official) => { + if (!all_inactive_officials.some((existing) => existing._id === official._id)) { + all_inactive_officials.push(official); + } + }); + render_card_stack_with_drops(inactive_stack, 'umpire', all_inactive_officials, { + can_drop: (meta) => meta_can_drop_to_list(meta, 'inactive_list'), + drag_meta_factory: (official) => ({ source_type: 'list', official_id: official._id, from_list: 'inactive_list', official }), + on_drop: (meta, before_official_id) => { + if (meta.source_type === 'preparation' || meta.source_type === 'assigned') { + remove_meta_from_match_to_list(inactive_stack, meta, 'inactive_list', before_official_id); + return; + } + send_list_reorder(meta, 'inactive_list', build_reordered_ids(inactive_stack, meta.official_id, before_official_id)); + } + }); + apply_stack_section_base_heights(); } function update_officials() { if(current_view === 'edit') { + if (official_drag_active) { + official_drag_refresh_pending = true; + return; + } + if (update_official_tables._document_drop_cleanup) { + document.removeEventListener('drop', update_official_tables._document_drop_cleanup, true); + update_official_tables._document_drop_cleanup = null; + } + if (update_official_tables._window_dragend_cleanup) { + window.removeEventListener('dragend', update_official_tables._window_dragend_cleanup, true); + update_official_tables._window_dragend_cleanup = null; + } update_official_tables(document.getElementById('officials_host')); } return; @@ -4891,6 +5874,8 @@ function update_officials() { update_emergency_btn, update_scoring_formats, update_stages_scoring_formats, + apply_pending_official_role_override, + set_pending_official_role_override, btp_status_changed, ticker_status_changed, bts_status_changed, diff --git a/static/js/cumpires.js b/static/js/cumpires.js index de6f5d6..2c0468a 100644 --- a/static/js/cumpires.js +++ b/static/js/cumpires.js @@ -2,7 +2,6 @@ var cumpires = (function() { - function _ui_render_table(container, umpires) { const table = uiu.el(container, 'table'); const tbody = uiu.el(table, 'tbody'); diff --git a/test/test_change_helpers.js b/test/test_change_helpers.js new file mode 100644 index 0000000..6205d05 --- /dev/null +++ b/test/test_change_helpers.js @@ -0,0 +1,53 @@ +'use strict'; + +const assert = require('assert'); + +const {_describe, _it} = require('./tutils.js'); + +const change_helpers = require('../static/js/change_helpers.js'); + +_describe('change_helpers', () => { + _it('updates officials view on umpires_changed', () => { + const deps = { + curt_ref: { umpires: [] }, + uiu_ref: { + qsEach(selector, cb) { + assert.strictEqual(selector, 'select[name="umpire_name"]'); + cb({ value: 'Stefan Schiedsrichter' }); + }, + qs(selector) { + assert.strictEqual(selector, '.umpire_container'); + return { id: 'umpire_container' }; + } + }, + cmatch_ref: { + calls: [], + render_umpire_options(select, value) { + this.calls.push({ select, value }); + } + }, + current_view_ref: 'show', + cumpires_ref: { + calls: [], + ui_status(container) { + this.calls.push(container); + } + }, + ctournament_ref: { + update_calls: 0, + update_officials() { + this.update_calls += 1; + } + } + }; + + const officials = [{ _id: 'u1', name: 'Stefan Schiedsrichter' }]; + change_helpers.apply_umpires_changed({ all_umpires: officials }, deps); + + assert.deepStrictEqual(deps.curt_ref.umpires, officials); + assert.strictEqual(deps.cmatch_ref.calls.length, 1); + assert.strictEqual(deps.cmatch_ref.calls[0].value, 'Stefan Schiedsrichter'); + assert.strictEqual(deps.cumpires_ref.calls.length, 1); + assert.strictEqual(deps.ctournament_ref.update_calls, 1); + }); +}); diff --git a/test/test_cmatch_official_select.js b/test/test_cmatch_official_select.js new file mode 100644 index 0000000..8898726 --- /dev/null +++ b/test/test_cmatch_official_select.js @@ -0,0 +1,83 @@ +'use strict'; + +const assert = require('assert'); + +const {_describe, _it} = require('./tutils.js'); + +const cmatch_official_select_helpers = require('../static/js/cmatch_official_select_helpers'); + +function build_entries(tournament, is_service_judge, show_all_officials) { + return cmatch_official_select_helpers.build_official_select_entries( + tournament, + is_service_judge, + show_all_officials, + { + ci18n_fn: (key) => key, + natcmp_fn: (a, b) => String(a).localeCompare(String(b), 'en'), + } + ); +} + +_describe('cmatch official select entries', () => { + _it('groups show-all umpire entries in the agreed order', () => { + const tournament = { + umpires: [ + { _id: 'u1', name: 'Wartet U', umpire_wait: 10 }, + { _id: 'u2', name: 'Wartet SJ', service_judge_wait: 20 }, + { _id: 'u3', name: 'Pause U', umpire_pause: 30 }, + { _id: 'u4', name: 'Pause SJ', service_judge_pause: 40 }, + { _id: 'u5', name: 'Assigned U' }, + { _id: 'u6', name: 'Assigned SJ' }, + { _id: 'u7', name: 'Inactive U', inactive_list: 50, is_umpire: true, is_service_judge: false }, + { _id: 'u8', name: 'Prep U' }, + { _id: 'u9', name: 'Court U', umpire_on_court: 'c1' }, + ], + matches: [ + { setup: { state: 'ready', match_num: 7, umpire: { _id: 'u5', name: 'Assigned U' }, service_judge: { _id: 'u6', name: 'Assigned SJ' } } }, + { setup: { state: 'preparation', match_num: 8, preparation_call_timestamp: 1, umpire: { _id: 'u8', name: 'Prep U' } } }, + ] + }; + + const entries = build_entries(tournament, false, true); + const labels = entries.map((entry) => entry.label); + + assert.deepStrictEqual(labels, [ + 'Wartet U', + '--- Waiting list service judge ---', + 'Wartet SJ (Service judge)', + '--- Currently on break: Umpire ---', + 'Pause U', + '--- Currently on break: Service judge ---', + 'Pause SJ (Service judge)', + '--- Assigned to a match ---', + 'Assigned U', + '--- Assigned to a match ---', + 'Assigned SJ (Service judge)', + '--- Not available ---', + 'Inactive U', + '--- In preparation ---', + 'Prep U', + '--- On court ---', + 'Court U' + ]); + }); + + _it('omits a separator before the first wait-list group in restricted mode', () => { + const tournament = { + umpires: [ + { _id: 'u1', name: 'Alpha', umpire_wait: 10 }, + { _id: 'u2', name: 'Beta', service_judge_wait: 20 }, + ], + matches: [] + }; + + const entries = build_entries(tournament, false, false); + const labels = entries.map((entry) => entry.label); + + assert.deepStrictEqual(labels, [ + 'Alpha', + '--- Waiting list service judge ---', + 'Beta (Service judge)', + ]); + }); +}); From 99a2863179b919e50ea69615f656ef20647a3d2a Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Tue, 7 Apr 2026 02:08:02 +0200 Subject: [PATCH 220/224] Stabilize live announcements and queue processing --- bts/bupws.js | 322 +++++++++++++++++++------------------ bts/update_queue.js | 64 +++++++- bts/wshandler.js | 5 +- static/js/announcements.js | 181 ++++++++++++++++----- 4 files changed, 370 insertions(+), 202 deletions(-) diff --git a/bts/bupws.js b/bts/bupws.js index 9959b8f..61bf948 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -138,167 +138,179 @@ async function handle_persist_display_settings(app, ws, msg) { } } async function handle_score_update(app, ws, msg) { - const match_utils = require('./match_utils'); - const tournament_key = msg.tournament_key; - const score_data = msg.score; - const match_id = score_data.match_id; - - try{ - var match = await match_utils.fetch_match(app, tournament_key, match_id); - } catch { - var match = null; - } - const finish_confirmed = score_data.finish_confirmed ? score_data.finish_confirmed : false; - const allow_finished_confirmation = finish_confirmed && (score_data.team1_won !== undefined && score_data.team1_won !== null); - if (match == null || (match.setup.now_on_court == false && !allow_finished_confirmation)) { - send_error(ws, tournament_key, "Match not found or not on court actualy."); - return; - } - - const update = { - network_score: score_data.network_score, - network_team1_left:score_data.network_team1_left, - network_team1_serving:score_data.network_team1_serving, - network_teams_player1_even:score_data.network_teams_player1_even, - presses:score_data.presses, - duration_ms:score_data.duration_ms, - end_ts:score_data.end_ts, - 'setup.now_on_court': true, - }; - - const device_info = score_data.device; - if (device_info) { - const client_ip = ws._socket.remoteAddress; - device_info.client_ip = client_ip; - } - - const match_finished = score_data.team1_won !== undefined && score_data.team1_won !== null; - if (finish_confirmed) { - update["setup.now_on_court"] = false; - update.team1_won = score_data.team1_won; - } - if (finish_confirmed) { - update.btp_winner = (update.team1_won === true) ? 1 : 2; - update.btp_needsync = true; - } - - if (score_data.shuttle_count) { - update.shuttle_count = score_data.shuttle_count; - } - const match_query = { - _id: match_id, - tournament_key, - }; - - const court_q = { - tournament_key, - _id: score_data.court_id, - }; - const db = app.db; - async.waterfall([ - cb => db.matches.update(match_query, { $set: update }, { returnUpdatedDocs: true }, (err, _, match) => cb(err, match)), - (match, cb) => { - if (match) { - handle_score_change(app, tournament_key, match.setup.court_id); - admin.notify_change(app, tournament_key, 'score', { - match_id, - network_score: update.network_score, - team1_won: update.team1_won, - shuttle_count: update.shuttle_count, - presses: match.presses, - }); - } - cb(null, match); - }, - (match, cb) => { - if (match) { - if (finish_confirmed) { - if (finish_confirmed) { - btp_manager.update_score(app, match); - } - update_queue.instance().execute(match_utils.reset_player_tabletoperator, app, tournament_key, match_id, update.end_ts) - .then(() => { - cb(null, match); - }) - .catch((err) => { - console.error("Error in reset_player_tabletoperator:", err); - cb(null, match); - }); + return update_queue.instance().execute(update_queue.named('handle_score_update', () => new Promise((resolve) => { + const match_utils = require('./match_utils'); + const tournament_key = msg.tournament_key; + const score_data = msg.score; + const match_id = score_data.match_id; + let finished = false; + const finish = (err) => { + if (finished) { + return; + } + finished = true; + clearTimeout(timeout); + if (err) { + send_error(ws, tournament_key, err.message || String(err)); + } + resolve(); + }; + const timeout = setTimeout(() => { + finish(new Error('handle_score_update timeout')); + }, 5000); + + (async () => { + let match = null; + try { + match = await match_utils.fetch_match(app, tournament_key, match_id); + } catch { + match = null; + } + const finish_confirmed = score_data.finish_confirmed ? score_data.finish_confirmed : false; + const allow_finished_confirmation = finish_confirmed && (score_data.team1_won !== undefined && score_data.team1_won !== null); + if (match == null || (match.setup.now_on_court == false && !allow_finished_confirmation)) { + send_error(ws, tournament_key, "Match not found or not on court actualy."); + return finish(); + } + + const update = { + network_score: score_data.network_score, + network_team1_left:score_data.network_team1_left, + network_team1_serving:score_data.network_team1_serving, + network_teams_player1_even:score_data.network_teams_player1_even, + presses:score_data.presses, + duration_ms:score_data.duration_ms, + end_ts:score_data.end_ts, + 'setup.now_on_court': true, + }; - } else { - cb(null, match); - } - } else { - cb(null, match); + const device_info = score_data.device; + if (device_info) { + const client_ip = ws._socket.remoteAddress; + device_info.client_ip = client_ip; } - }, - (match, cb) => db.courts.findOne(court_q, (err, court) => cb(err, match, court)), - (match, court, cb) => { - if (!match) { - if (court.match_id === match_id) { - cb(null, match, court, false); - return; - } - db.courts.update(court_q, { $set: { match_id: match_id } }, {}, (err) => { - cb(err, match, court, true); - }); - } - cb(null, match, court, true); - }, - (match, court, changed_court, cb) => { - if (match && changed_court) { - admin.notify_change(app, tournament_key, 'court_current_match', { - match__id: match_id, - match: match, - }); - } - cb(null, match, changed_court); - }, - (match, changed_court, cb) => { - if (match && match.setup.highlight && - match.setup.highlight == 6 && - match.network_score && - match.network_score.length > 0 && - match.network_score[0].length > 1 && - (match.network_score[0][0] > 0 || match.network_score[0][1] > 0)) { - match.setup.highlight = 0; - btp_manager.update_highlight(app, match); - } - cb(null, match, changed_court); - }, - (match, changed_court, cb) => { - if (changed_court) { - ticker_manager.pushall(app, tournament_key); - } else { - if (match) { - ticker_manager.update_score(app, match); - } - } - cb(null, match, changed_court); - }, - (match, changed_court, cb) => { - if (!match) { - return cb(new Error('Cannot find match ' + JSON.stringify(match))); - } if (finish_confirmed) { - update_queue.instance().execute(match_utils.call_preparation_match_on_court, app, tournament_key, match.setup.court_id); + update["setup.now_on_court"] = false; + update.team1_won = score_data.team1_won; + update.btp_winner = (update.team1_won === true) ? 1 : 2; + update.btp_needsync = true; } - return cb(null, match, changed_court); - }, - (match, changed_court, cb) => { - if (!device_info) { - return cb(null, match, changed_court); + + if (score_data.shuttle_count) { + update.shuttle_count = score_data.shuttle_count; } - update_device_info(app, tournament_key, device_info); - return cb(null, match, changed_court); - }, - ], function (err) { - if (err) { - send_error(ws, tournament_key, err.message); - return; - } - }); + const match_query = { + _id: match_id, + tournament_key, + }; + + const court_q = { + tournament_key, + _id: score_data.court_id, + }; + const db = app.db; + async.waterfall([ + cb => { + db.matches.update(match_query, { $set: update }, { returnUpdatedDocs: true }, (err, _, updated_match) => cb(err, updated_match)); + }, + (updated_match, cb) => { + if (updated_match) { + handle_score_change(app, tournament_key, updated_match.setup.court_id); + admin.notify_change(app, tournament_key, 'score', { + match_id, + network_score: update.network_score, + team1_won: update.team1_won, + shuttle_count: update.shuttle_count, + presses: updated_match.presses, + }); + } + cb(null, updated_match); + }, + (updated_match, cb) => { + if (updated_match && finish_confirmed) { + btp_manager.update_score(app, updated_match); + match_utils.reset_player_tabletoperator(app, tournament_key, match_id, update.end_ts) + .then(() => cb(null, updated_match)) + .catch((err) => { + console.error("Error in reset_player_tabletoperator:", err); + cb(err); + }); + return; + } + cb(null, updated_match); + }, + (updated_match, cb) => { + db.courts.findOne(court_q, (err, court) => cb(err, updated_match, court)); + }, + (updated_match, court, cb) => { + if (!court) { + return cb(new Error('Cannot find court ' + JSON.stringify(score_data.court_id))); + } + if (!updated_match) { + if (court.match_id === match_id) { + cb(null, updated_match, court, false); + return; + } + + db.courts.update(court_q, { $set: { match_id: match_id } }, {}, (err) => { + cb(err, updated_match, court, true); + }); + return; + } + cb(null, updated_match, court, true); + }, + (updated_match, court, changed_court, cb) => { + if (updated_match && changed_court) { + admin.notify_change(app, tournament_key, 'court_current_match', { + match__id: match_id, + match: updated_match, + }); + } + cb(null, updated_match, changed_court); + }, + (updated_match, changed_court, cb) => { + if (updated_match && updated_match.setup.highlight && + updated_match.setup.highlight == 6 && + updated_match.network_score && + updated_match.network_score.length > 0 && + updated_match.network_score[0].length > 1 && + (updated_match.network_score[0][0] > 0 || updated_match.network_score[0][1] > 0)) { + updated_match.setup.highlight = 0; + btp_manager.update_highlight(app, updated_match); + } + cb(null, updated_match, changed_court); + }, + (updated_match, changed_court, cb) => { + if (changed_court) { + ticker_manager.pushall(app, tournament_key); + } else if (updated_match) { + ticker_manager.update_score(app, updated_match); + } + cb(null, updated_match, changed_court); + }, + (updated_match, changed_court, cb) => { + if (!updated_match) { + return cb(new Error('Cannot find match ' + JSON.stringify(updated_match))); + } + if (finish_confirmed) { + match_utils.call_preparation_match_on_court(app, tournament_key, updated_match.setup.court_id) + .then(() => cb(null, updated_match, changed_court)) + .catch((err) => cb(err)); + return; + } + return cb(null, updated_match, changed_court); + }, + (updated_match, changed_court, cb) => { + if (!device_info) { + return cb(null, updated_match, changed_court); + } + update_device_info(app, tournament_key, device_info); + return cb(null, updated_match, changed_court); + }, + ], finish); + })().catch(finish); + }))); } async function handle_device_info(app, ws, msg) { const tournament_key = msg.tournament_key; diff --git a/bts/update_queue.js b/bts/update_queue.js index 342a7a1..eb7def0 100644 --- a/bts/update_queue.js +++ b/bts/update_queue.js @@ -4,6 +4,47 @@ class update_queue { constructor() { this.queue = []; this.active = false; + this.current_task = null; + this.current_task_started_at = null; + this.current_task_watchdog = null; + this.hang_reporter = null; + } + + _task_name(task) { + if (!task) { + return ''; + } + if (task._queue_name) { + return task._queue_name; + } + return task.name && task.name.length > 0 ? task.name : ''; + } + + _start_watchdog(task_name) { + this._clear_watchdog(); + this.current_task_watchdog = setTimeout(() => { + const runtime_ms = this.current_task_started_at ? (Date.now() - this.current_task_started_at) : null; + const payload = { + task: task_name, + runtime_ms, + queue_length: this.queue.length + }; + console.warn('[bts] update_queue:task_still_running', payload); + if (typeof this.hang_reporter === 'function') { + try { + this.hang_reporter(payload); + } catch (err) { + console.warn('[bts] update_queue:hang_reporter_error', err && (err.stack || err.message || String(err))); + } + } + }, 5000); + } + + _clear_watchdog() { + if (this.current_task_watchdog) { + clearTimeout(this.current_task_watchdog); + this.current_task_watchdog = null; + } } async process() { @@ -13,11 +54,20 @@ class update_queue { this.active = true; while (this.queue.length > 0) { const { task, args, resolve, reject } = this.queue.shift(); + const task_name = this._task_name(task); + this.current_task = task_name; + this.current_task_started_at = Date.now(); + this._start_watchdog(task_name); try { - //console.log(`Execution of function: ${task.name}`); const res = await task(...args); + this._clear_watchdog(); + this.current_task = null; + this.current_task_started_at = null; resolve(res); } catch (err) { + this._clear_watchdog(); + this.current_task = null; + this.current_task_started_at = null; reject(err); } } @@ -30,6 +80,10 @@ class update_queue { this.process(); }); } + + set_hang_reporter(fn) { + this.hang_reporter = fn; + } } const update_queue_inst = new update_queue(); @@ -37,6 +91,12 @@ function instance() { return update_queue_inst; } +function named(name, task) { + task._queue_name = name; + return task; +} + module.exports = { - instance + instance, + named }; diff --git a/bts/wshandler.js b/bts/wshandler.js index 633e10e..d0b571e 100644 --- a/bts/wshandler.js +++ b/bts/wshandler.js @@ -46,6 +46,9 @@ function handle(mod, app, ws) { serror.silent('Received error message from client: ' + msg.message); return; } + if (msg.tournament_key) { + ws.last_tournament_key = msg.tournament_key; + } const func = mod['handle_' + msg.type]; if (func) { @@ -83,4 +86,4 @@ function handle(mod, app, ws) { module.exports = { handle, -}; \ No newline at end of file +}; diff --git a/static/js/announcements.js b/static/js/announcements.js index ee3a019..3a7e8c2 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -22,7 +22,11 @@ function announceNewMatch(matchSetup) { const umpire = createUmpire(matchSetup); const serviceJudge = createServiceJudge(matchSetup); const tabletOperator = createTabletOperator(matchSetup); - announce([field, matchNumber, eventName, round, teams, umpire, serviceJudge, tabletOperator, field]); + announce( + [field, matchNumber, eventName, round, teams, umpire, serviceJudge, tabletOperator, field], + false, + buildAnnouncementClaimKey(matchSetup, 'match_called_on_court') + ); } function announcePreparationMatch(matchSetup) { @@ -42,7 +46,11 @@ function announcePreparationMatch(matchSetup) { if (curt.preparation_meetingpoint_enabled) { lastPart = createMeetingPointAnnouncement(matchSetup); } - announce([preparation, field, matchNumber, eventName, round, teams, umpire, serviceJudge, tabletOperator, lastPart]); + announce( + [preparation, field, matchNumber, eventName, round, teams, umpire, serviceJudge, tabletOperator, lastPart], + false, + buildAnnouncementClaimKey(matchSetup, 'match_preparation_call') + ); } function announceSecondCallTeamOne(matchSetup) { if(!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { @@ -404,6 +412,53 @@ function createMeetingPointAnnouncement(matchSetup) { let emergencyInterval = null; let emergencyAudio = null; +const ANNOUNCEMENT_DEDUP_TTL_MS = 2500; +const ANNOUNCEMENT_TAB_ID = `${Date.now()}_${Math.random().toString(36).slice(2)}`; +const announcementPlaybackQueue = []; +let announcementPlaybackActive = false; +let announcementVoicesPromise = null; + +function buildAnnouncementClaimKey(matchSetup, kind) { + const explicit = matchSetup && matchSetup._announcement_claim_key; + if (explicit) { + return explicit; + } + const matchId = matchSetup && (matchSetup._match_id || matchSetup.match_id || matchSetup.id || matchSetup.btp_id); + return matchId ? `${kind}:${matchId}` : null; +} + +function announcementFingerprint(callArray) { + let hash = 0; + const input = JSON.stringify(callArray || []); + for (let i = 0; i < input.length; i++) { + hash = ((hash << 5) - hash) + input.charCodeAt(i); + hash |= 0; + } + return String(hash); +} + +function claimAnnouncementPlayback(callArray, claimKey) { + const now = Date.now(); + const key = `announcement_claim_${claimKey || announcementFingerprint(callArray)}`; + try { + const raw = window.localStorage.getItem(key); + if (raw) { + const current = JSON.parse(raw); + if (current && current.expires_at && current.expires_at > now) { + return false; + } + } + const claim = JSON.stringify({ + owner: ANNOUNCEMENT_TAB_ID, + expires_at: now + ANNOUNCEMENT_DEDUP_TTL_MS + }); + window.localStorage.setItem(key, claim); + const confirmed = JSON.parse(window.localStorage.getItem(key) || 'null'); + return !!confirmed && confirmed.owner === ANNOUNCEMENT_TAB_ID; + } catch (e) { + return true; + } +} function emergency_announce(enable) { @@ -441,55 +496,93 @@ function emergency_announce(enable) { } } -function announce(callArray, local) { - if(!(window.localStorage.getItem('enable_free_announcements') === 'true') && !local) { - return; +function getAnnouncementVoices() { + if (announcementVoicesPromise) { + return announcementVoicesPromise; } - - // Seems like the getVoices() is an asynchronous function where it is not always guaranteed that you get a - // result immediately. The wait for the result must therefore be handled: - // https://stackoverflow.com/questions/21513706/getting-the-list-of-voices-in-speechsynthesis-web-speech-api - const allVoicesObtained = new Promise(function (resolve, reject) { + announcementVoicesPromise = new Promise(function (resolve) { let voices = window.speechSynthesis.getVoices(); if (voices.length !== 0) { resolve(voices); - } else { - window.speechSynthesis.addEventListener("voiceschanged", function () { - voices = window.speechSynthesis.getVoices(); - resolve(voices); - }); + return; } + const onVoicesChanged = function () { + window.speechSynthesis.removeEventListener("voiceschanged", onVoicesChanged); + voices = window.speechSynthesis.getVoices(); + resolve(voices); + }; + window.speechSynthesis.addEventListener("voiceschanged", onVoicesChanged); }); + return announcementVoicesPromise; +} - allVoicesObtained.then(voices => { - var voice = null; - for (var i = 0; i < voices.length; i++) { - if (voices[i].voiceURI == ci18n('announcements:voice')) { - voice = voices[i]; - break; - } +function findAnnouncementVoice(voices) { + for (let i = 0; i < voices.length; i++) { + if (voices[i].voiceURI == ci18n('announcements:voice')) { + return voices[i]; } - for (var i = 0; i < callArray.length; i++) { - const part = callArray[i]; - if (part && part != null) { - var words = new SpeechSynthesisUtterance(part); - words.lang = ci18n('announcements:lang'); - words.rate = curt.announcement_speed ? curt.announcement_speed : 1.05; - words.pitch = 0; - words.volume = 1; - words.voice = voice; - if (i == 0) { - if (window.speechSynthesis.speaking) { - words.onstart = function () { - window.speechSynthesis.pause(); - setTimeout(() => { - window.speechSynthesis.resume(); - }, curt.announcement_pause_time_ms ? curt.announcement_pause_time_ms * 1000 : 2000); - } - } - } - window.speechSynthesis.speak(words); - } + } + return null; +} + +function playAnnouncementBatch(parts, voice, done) { + const filteredParts = (parts || []).filter((part) => !!part); + let index = 0; + const playNext = () => { + if (index >= filteredParts.length) { + done(); + return; } + const words = new SpeechSynthesisUtterance(filteredParts[index]); + words.lang = ci18n('announcements:lang'); + words.rate = curt.announcement_speed ? curt.announcement_speed : 1.05; + words.pitch = 0; + words.volume = 1; + words.voice = voice; + words.onend = function () { + index += 1; + playNext(); + }; + words.onerror = function () { + index += 1; + playNext(); + }; + window.speechSynthesis.speak(words); + }; + playNext(); +} + +function processAnnouncementPlaybackQueue() { + if (announcementPlaybackActive) { + return; + } + const nextBatch = announcementPlaybackQueue.shift(); + if (!nextBatch) { + return; + } + announcementPlaybackActive = true; + getAnnouncementVoices().then((voices) => { + const voice = findAnnouncementVoice(voices); + playAnnouncementBatch(nextBatch.callArray, voice, () => { + const pauseMs = curt.announcement_pause_time_ms ? (curt.announcement_pause_time_ms * 1000) : 2000; + setTimeout(() => { + announcementPlaybackActive = false; + processAnnouncementPlaybackQueue(); + }, pauseMs); + }); + }).catch(() => { + announcementPlaybackActive = false; + processAnnouncementPlaybackQueue(); }); -} \ No newline at end of file +} + +function announce(callArray, local, claimKey) { + if(!(window.localStorage.getItem('enable_free_announcements') === 'true') && !local) { + return; + } + if (!local && !claimAnnouncementPlayback(callArray, claimKey)) { + return; + } + announcementPlaybackQueue.push({ callArray }); + processAnnouncementPlaybackQueue(); +} From ab0e45fa3329821350117b58ba8257852ff8a6f8 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 15 Apr 2026 08:15:44 +0200 Subject: [PATCH 221/224] Ignore local Codex workspace files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 99c66ee..90f4f79 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ ticker_config.json mail.* /.vs +/.codex From 06eefd1a1791630644c740e81b7cf6ff91596321 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 15 Apr 2026 08:16:12 +0200 Subject: [PATCH 222/224] Improve BTP sync and predecessor link handling --- bts/btp_conn.js | 103 +++++++++- bts/btp_manager.js | 10 +- bts/btp_parse.js | 1 + bts/btp_proto.js | 9 +- bts/btp_sync.js | 307 +++++++++++++++++++++++++++--- static/js/cmatch.js | 309 +++++++++++++++++++++++------- test/test_btp_proto.js | 58 ++++-- test/test_btp_sync.js | 130 +++++++++++++ test/test_cmatch.js | 421 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 1211 insertions(+), 137 deletions(-) diff --git a/bts/btp_conn.js b/bts/btp_conn.js index ad65f3b..a4c97dd 100644 --- a/bts/btp_conn.js +++ b/bts/btp_conn.js @@ -13,6 +13,7 @@ const serror = require('./serror'); const AUTOFETCH_TIMEOUT = 30000; const CONNECT_TIMEOUT = 5000; const WAIT_TIMEOUT = 10000; +const FETCH_QUEUE_HANG_TIMEOUT = 30000; const BTP_PORT = 9901; const BLP_PORT = 9911; @@ -73,7 +74,7 @@ function send_request(ip, port, xml_req, timeZone, callback) { class BTPConn { constructor(app, ip, password, tkey, enabled_autofetch, readonly, is_team, timeZone, autofetch_timeout_intervall) { this.app = app; - this.last_status = 'Activated'; + this.last_status = { status: 'activated', message: '' }; this.ip = ip; this.password = password; this.tkey = tkey; @@ -81,9 +82,14 @@ class BTPConn { this.terminated = false; this.enabled_autofetch = enabled_autofetch; this.autofetch_timeout = null; + this.next_fetch_ts = null; + this.fetch_in_progress = false; this.readonly = readonly; this.is_team = is_team; - this.autofetch_timeout_intervall = autofetch_timeout_intervall ? autofetch_timeout_intervall : AUTOFETCH_TIMEOUT; + const parsed_autofetch_timeout = Number(autofetch_timeout_intervall); + this.autofetch_timeout_intervall = Number.isFinite(parsed_autofetch_timeout) && parsed_autofetch_timeout > 0 + ? parsed_autofetch_timeout + : AUTOFETCH_TIMEOUT; this.connect(); } @@ -109,35 +115,72 @@ class BTPConn { this.pushall(); if (this.enabled_autofetch) { - update_queue.instance().execute(this.fetch, this,true); + update_queue.instance().execute( + update_queue.hang_after(FETCH_QUEUE_HANG_TIMEOUT, update_queue.named('fetch', this.fetch)), + this, + true + ); } }); } sync_data() { - update_queue.instance().execute(this.fetch, this, false); + if (this.autofetch_timeout) { + clearTimeout(this.autofetch_timeout); + this.autofetch_timeout = null; + } + this.next_fetch_ts = null; + this.publish_status(); + update_queue.instance().execute( + update_queue.hang_after(FETCH_QUEUE_HANG_TIMEOUT, update_queue.named('fetch', this.fetch)), + this, + this.enabled_autofetch + ); } async fetch(connection, reschedule_fetch ) { return new Promise((resolve, reject) => { try { + connection.fetch_in_progress = true; + connection.next_fetch_ts = null; + connection.publish_status(); const ir = btp_proto.get_info_request(connection.password); connection.send(ir, async (response) => { try { if (response && response != null) { const value = await btp_sync.sync_btp_data(connection.app, connection.tkey, response); + const match_utils = require('./match_utils'); + match_utils.queue_auto_execute_preparation_selections(connection.app, connection.tkey, (selectionErr) => { + if (selectionErr) { + console.warn('[bts] failed to auto select preparation matches after fetch', selectionErr && (selectionErr.stack || selectionErr.message || String(selectionErr))); + return; + } + match_utils.auto_call_matches_on_free_courts(connection.app, connection.tkey, (callErr) => { + if (callErr) { + console.warn('[bts] failed to auto call matches on free courts after fetch', callErr && (callErr.stack || callErr.message || String(callErr))); + } + }); + }); if (reschedule_fetch == true) { connection.schedule_fetch(); } + connection.fetch_in_progress = false; + connection.publish_status(); resolve(value); } else { + connection.fetch_in_progress = false; + connection.publish_status(); resolve(null); } } catch (innerError) { + connection.fetch_in_progress = false; + connection.publish_status(); reject(innerError); } }); } catch (e) { + connection.fetch_in_progress = false; + connection.publish_status(); reject(e); } }); @@ -148,15 +191,27 @@ class BTPConn { return; } if (!this.enabled_autofetch) { + this.next_fetch_ts = null; + this.publish_status(); return; } + this.next_fetch_ts = Date.now() + this.autofetch_timeout_intervall; + this.publish_status(); this.autofetch_timeout = setTimeout(() => { - update_queue.instance().execute(this.fetch,this,true); + this.next_fetch_ts = null; + this.publish_status(); + update_queue.instance().execute( + update_queue.hang_after(FETCH_QUEUE_HANG_TIMEOUT, update_queue.named('fetch', this.fetch)), + this, + true + ); }, this.autofetch_timeout_intervall); } terminate() { this.terminated = true; + this.next_fetch_ts = null; + this.fetch_in_progress = false; this.report_status('deactivated','Terminated.'); } @@ -204,6 +259,9 @@ class BTPConn { clearTimeout(this.autofetch_timeout); this.autofetch_timeout = null; } + this.next_fetch_ts = null; + this.fetch_in_progress = false; + this.publish_status(); setTimeout(() => this.connect(), 500); } @@ -213,11 +271,19 @@ class BTPConn { } report_status(status, message) { - const msg = { + this.last_status = { status: status, message: message - } - this.last_status = msg; + }; + this.publish_status(); + } + + publish_status() { + const msg = { + ...this.last_status, + next_fetch_ts: this.next_fetch_ts, + fetch_in_progress: this.fetch_in_progress, + }; const admin = require('./admin'); admin.notify_change(this.app, this.tkey, 'btp_status', msg); } @@ -300,7 +366,15 @@ class BTPConn { return cb(null, umpire_btp_id, service_judge_btp_id, court ? court.btp_id : null); }); }, - ], (err, umpire_btp_id, service_judge_btp_id, court_btp_id) => { + (umpire_btp_id, service_judge_btp_id, court_btp_id, cb) => { + this.app.db.tournaments.findOne({ key: this.tkey }, (err, tournament) => { + if (err) { + return cb(err); + } + return cb(null, umpire_btp_id, service_judge_btp_id, court_btp_id, tournament); + }); + }, + ], (err, umpire_btp_id, service_judge_btp_id, court_btp_id, tournament) => { if (err) { serror.silent('Error while fetching court/umpire: ' + err.message + '. Skipping sync of match ' + match._id); return; @@ -317,7 +391,16 @@ class BTPConn { } const req = btp_proto.update_request( - match, this.key_unicode, this.password, umpire_btp_id, service_judge_btp_id, court_btp_id); + match, + this.key_unicode, + this.password, + umpire_btp_id, + service_judge_btp_id, + court_btp_id, + { + write_match_check_in_status: tournament?.btp_settings?.check_in_per_match === true, + } + ); this.send(req, response => { const results = response.Action[0].Result; const rescode = results ? results[0] : 'no-result'; diff --git a/bts/btp_manager.js b/bts/btp_manager.js index 7a6c4cc..91261d8 100644 --- a/bts/btp_manager.js +++ b/bts/btp_manager.js @@ -119,10 +119,14 @@ function init(app, cb) { function get_status(tkey) { const conn = conns_by_tkey.get(tkey); if (!conn) { - return { status: 'deactivated', message: '' }; + return { status: 'deactivated', message: '', next_fetch_ts: null, fetch_in_progress: false }; } - return conn.last_status; + return { + ...conn.last_status, + next_fetch_ts: conn.next_fetch_ts, + fetch_in_progress: conn.fetch_in_progress, + }; } module.exports = { @@ -134,4 +138,4 @@ module.exports = { update_players, update_courts, update_highlight, -}; \ No newline at end of file +}; diff --git a/bts/btp_parse.js b/bts/btp_parse.js index aba3c47..2e7c56f 100644 --- a/bts/btp_parse.js +++ b/bts/btp_parse.js @@ -279,6 +279,7 @@ function get_btp_state(response) { stages, matches, links, + planning_nodes: matches_by_pid, officials, is_league, match_types: new Map(Object.entries(MATCH_TYPES)), diff --git a/bts/btp_proto.js b/bts/btp_proto.js index fb81543..b027ec0 100644 --- a/bts/btp_proto.js +++ b/bts/btp_proto.js @@ -46,8 +46,9 @@ function login_request(password) { return res; } -function update_request(match, key_unicode, password, umpire_btp_id, service_judge_btp_id, court_btp_id) { +function update_request(match, key_unicode, password, umpire_btp_id, service_judge_btp_id, court_btp_id, options = {}) { assert(key_unicode); + const write_match_check_in_status = options.write_match_check_in_status !== false; const matches = []; const res = { Header: { @@ -88,7 +89,7 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud PlanningID: btp_m_id.planning, Status: 0, Highlight: (match.setup.highlight ? match.setup.highlight : 0), - DisplayOrder: match.match_order, + MatchOrder: match.match_order, // BTP also sends a boolean ScoreSheetPrinted here }; @@ -138,7 +139,7 @@ function update_request(match, key_unicode, password, umpire_btp_id, service_jud } - if(match.setup.teams.length > 1) { + if (write_match_check_in_status && match.setup.teams.length > 1) { if (match.setup.teams[0].players.length > 0 && match.setup.teams[0].players[0].checked_in) { m.Status = m.Status | 0b0001; } @@ -469,4 +470,4 @@ module.exports = { update_courts_request, // Tests only _req2xml: req2xml, -}; \ No newline at end of file +}; diff --git a/bts/btp_sync.js b/bts/btp_sync.js index 20139ee..2fc0467 100644 --- a/bts/btp_sync.js +++ b/bts/btp_sync.js @@ -6,6 +6,7 @@ const async = require('async'); const btp_parse = require('./btp_parse'); const countries = require('./countries'); +const match_utils = require('./match_utils'); const utils = require('./utils'); const { fix_player } = require('./name_fixup'); @@ -18,7 +19,192 @@ function date_str(dt) { return utils.pad(dt.year, 2, '0') + '-' + utils.pad(dt.month, 2, '0') + '-' + utils.pad(dt.day, 2, '0'); } -async function craft_match(app, tkey, btp_id, location_map, court_map, event, stage, scoring_formats, draw, btp_links, officials, clubs, districts, bm, match_ids_on_court, match_types, is_league) { +function _format_btp_match_relation_label(relation_key, bm) { + if (!bm || !bm.MatchNr || !bm.MatchNr[0]) { + return null; + } + const relation = relation_key === 'winner' ? 'Gewinner' : 'Verlierer'; + const planned_time = bm.PlannedTime && bm.PlannedTime[0]; + if (!planned_time) { + return `${relation} #${bm.MatchNr[0]}`; + } + return `${relation} #${bm.MatchNr[0]} - ${date_str(planned_time)} ${time_str(planned_time)}`; +} + +function _is_displayable_btp_match_node(node) { + return !!(node && node.MatchNr && node.MatchNr[0]); +} + +function _same_btp_from_pair(a, b) { + if (!a || !b || !a.From1 || !a.From2 || !b.From1 || !b.From2) { + return false; + } + return a.From1[0] == b.From1[0] && a.From2[0] == b.From2[0]; +} + +function _find_visible_consolidation_match_for_hidden_node(draw_id, hidden_node, planning_nodes) { + if (!hidden_node || !hidden_node.From1 || !hidden_node.From2) { + return null; + } + const hidden_planning = hidden_node.PlanningID && hidden_node.PlanningID[0]; + for (const candidate of planning_nodes.values()) { + if (!candidate || candidate.DrawID[0] !== draw_id) { + continue; + } + if (hidden_planning != null && candidate.PlanningID && candidate.PlanningID[0] == hidden_planning) { + continue; + } + if (!_is_displayable_btp_match_node(candidate)) { + continue; + } + if (_same_btp_from_pair(candidate, hidden_node)) { + return candidate; + } + } + return null; +} + +function _find_incoming_matches_for_planning(draw_id, source_planning, planning_nodes) { + const incoming = []; + for (const candidate of planning_nodes.values()) { + if (!candidate || candidate.DrawID[0] !== draw_id) { + continue; + } + if (!_is_displayable_btp_match_node(candidate)) { + continue; + } + let relation = null; + if (candidate.WinnerTo && candidate.WinnerTo[0] == source_planning) { + relation = 'winner'; + } else if (candidate.LoserTo && candidate.LoserTo[0] == source_planning) { + relation = 'loser'; + } + if (relation) { + incoming.push({ candidate, relation }); + } + } + return incoming; +} + +function _find_visible_consolidation_match_for_incoming(draw_id, incoming, planning_nodes) { + if (!incoming || incoming.length < 2) { + return null; + } + const incoming_plannings = incoming + .map((entry) => entry.candidate && entry.candidate.PlanningID ? entry.candidate.PlanningID[0] : null) + .filter((planning) => planning != null); + if (incoming_plannings.length !== incoming.length) { + return null; + } + for (const candidate of planning_nodes.values()) { + if (!candidate || candidate.DrawID[0] !== draw_id) { + continue; + } + if (!_is_displayable_btp_match_node(candidate)) { + continue; + } + const candidate_sources = [ + candidate.From1 && candidate.From1[0], + candidate.From2 && candidate.From2[0], + ]; + if (candidate_sources.every((planning) => planning != null && incoming_plannings.includes(planning))) { + return candidate; + } + } + return null; +} + +function _resolve_btp_dependency_link(draw_id, source_planning, target_planning, btp_links, planning_nodes, visited = new Set()) { + if (source_planning == null) { + return null; + } + const visit_key = `${draw_id}_${source_planning}_${target_planning || ''}`; + if (visited.has(visit_key)) { + return null; + } + visited.add(visit_key); + + const direct_link = btp_links.find((l) => l.DrawID[0] === draw_id && l.PlanningID[0] == source_planning); + if (direct_link && direct_link.Link && direct_link.Link[0]) { + return direct_link.Link[0]; + } + + const incoming = _find_incoming_matches_for_planning(draw_id, source_planning, planning_nodes); + if (incoming.length > 1) { + const unique_relations = [...new Set(incoming.map((entry) => entry.relation))]; + if (unique_relations.length === 1) { + const consolidation_match = _find_visible_consolidation_match_for_incoming(draw_id, incoming, planning_nodes); + if (consolidation_match) { + return _format_btp_match_relation_label(unique_relations[0], consolidation_match); + } + } + } + + const node = planning_nodes.get(`${draw_id}_${source_planning}`); + if (!node) { + if (incoming.length === 1) { + return _format_btp_match_relation_label(incoming[0].relation, incoming[0].candidate); + } + return null; + } + + if (_is_displayable_btp_match_node(node)) { + if (target_planning != null && node.WinnerTo && node.WinnerTo[0] == target_planning) { + const direct_label = _format_btp_match_relation_label('winner', node); + if (node.PlannedTime && node.PlannedTime[0]) { + return direct_label; + } + } + if (target_planning != null && node.LoserTo && node.LoserTo[0] == target_planning) { + const direct_label = _format_btp_match_relation_label('loser', node); + if (node.PlannedTime && node.PlannedTime[0]) { + return direct_label; + } + } + } + + const consolidation_match = _find_visible_consolidation_match_for_hidden_node(draw_id, node, planning_nodes); + if (consolidation_match) { + if (target_planning != null && node.WinnerTo && node.WinnerTo[0] == target_planning) { + return _format_btp_match_relation_label('winner', consolidation_match); + } + if (target_planning != null && node.LoserTo && node.LoserTo[0] == target_planning) { + return _format_btp_match_relation_label('loser', consolidation_match); + } + if (consolidation_match.WinnerTo && consolidation_match.WinnerTo[0] == source_planning) { + return _format_btp_match_relation_label('winner', consolidation_match); + } + if (consolidation_match.LoserTo && consolidation_match.LoserTo[0] == source_planning) { + return _format_btp_match_relation_label('loser', consolidation_match); + } + } + + if (_is_displayable_btp_match_node(node)) { + if (target_planning != null && node.WinnerTo && node.WinnerTo[0] == target_planning) { + return _format_btp_match_relation_label('winner', node); + } + if (target_planning != null && node.LoserTo && node.LoserTo[0] == target_planning) { + return _format_btp_match_relation_label('loser', node); + } + } + + if (node.From1 && node.From1[0]) { + const nested_from1 = _resolve_btp_dependency_link(draw_id, node.From1[0], source_planning, btp_links, planning_nodes, visited); + if (nested_from1) { + return nested_from1; + } + } + if (node.From2 && node.From2[0]) { + const nested_from2 = _resolve_btp_dependency_link(draw_id, node.From2[0], source_planning, btp_links, planning_nodes, visited); + if (nested_from2) { + return nested_from2; + } + } + + return null; +} + +async function craft_match(app, tkey, btp_id, location_map, court_map, event, stage, scoring_formats, draw, btp_links, planning_nodes, officials, clubs, districts, bm, match_ids_on_court, match_types, is_league) { return new Promise((resolve, reject) => { const stournament = require('./stournament'); // avoid dependency cycle @@ -27,7 +213,8 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, st const scheduled_time_str = (bm.PlannedTime ? time_str(bm.PlannedTime[0]) : undefined); const scheduled_date = (bm.PlannedTime ? date_str(bm.PlannedTime[0]) : undefined); - var match_name = (bm.RoundName && bm.RoundName[0] ? bm.RoundName[0] : undefined); + const phase_name_raw = (bm.RoundName && bm.RoundName[0] ? bm.RoundName[0] : undefined); + var match_name = phase_name_raw; var event_name = event.Name[0]; const teams = _craft_teams(bm, clubs, districts); @@ -68,6 +255,21 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, st rounds.set("CP- R16", [ 5, 16]); rounds.set("CP- VF", [ 5, 12]); + let phase_block_key = 'UNKNOWN'; + if (phase_name_raw) { + if (/^G\d+$/.test(phase_name_raw)) { + phase_block_key = phase_name_raw; + } else if (['R64', 'R32', 'R16', 'VF', 'HF'].includes(phase_name_raw)) { + phase_block_key = phase_name_raw; + } else if (phase_name_raw === 'CP- R16') { + phase_block_key = 'CP-R16'; + } else if (phase_name_raw === 'CP- VF') { + phase_block_key = 'CP-VF'; + } else if (['Finale', '3/4'].includes(phase_name_raw)) { + phase_block_key = 'FR'; + } + } + if(match_name && rounds.get(match_name)) { const best_place = rounds.get(match_name)[0] + draw.Position[0] - 1; const lowes_place = rounds.get(match_name)[1] + draw.Position[0] - 1; @@ -108,23 +310,11 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, st } if (teams[0].players.length < 1) { - const link1 = btp_links.find(l => { - return (l.DrawID[0] === bm.DrawID[0] && l.PlanningID[0] === links.from1); - }); - - if (link1) { - links.from1_link = link1.Link[0]; - } + links.from1_link = _resolve_btp_dependency_link(bm.DrawID[0], links.from1, bm.PlanningID[0], btp_links, planning_nodes); } if (teams[1].players.length < 1) { - const link2 = btp_links.find(l => { - return (l.DrawID[0] === bm.DrawID[0] && l.PlanningID[0] === links.from2); - }); - - if (link2) { - links.from2_link = link2.Link[0]; - } + links.from2_link = _resolve_btp_dependency_link(bm.DrawID[0], links.from2, bm.PlanningID[0], btp_links, planning_nodes); } @@ -153,6 +343,8 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, st warmup: "none", links: links, highlight: bm.Highlight[0], + phase_name_raw, + phase_block_key, }; app.db.tournaments.findOne({ key: tkey }, (err, tournament) => { @@ -221,14 +413,14 @@ async function craft_match(app, tkey, btp_id, location_map, court_map, event, st const official_id = bm.Official1ID[0]; const o = get_umpire(app, tkey, officials, official_id) || build_fallback_official(official_id, tkey); if (o) { - setup.umpire = { ...o, checked_in: false }; + setup.umpire = { ...o, checked_in: !!o.checked_in }; } } if (bm.Official2ID) { const official_id = bm.Official2ID[0]; const o = get_umpire(app, tkey, officials, official_id) || build_fallback_official(official_id, tkey); if (o) { - setup.service_judge = { ...o, checked_in: false }; + setup.service_judge = { ...o, checked_in: !!o.checked_in }; } } @@ -311,10 +503,22 @@ function mergeLocalMatchIntoBtpMatch(current_match, match) { match.setup.called_timestamp = current_match.setup.called_timestamp; } - if (current_match.setup.preparation_call_timestamp) { + const local_preparation_active = + current_match.setup && + current_match.setup.state === 'preparation' && + Number(current_match.setup.highlight) > 0 && + current_match.setup.preparation_call_timestamp; + + if (local_preparation_active) { match.setup.preparation_call_timestamp = current_match.setup.preparation_call_timestamp; match.setup.state = 'preparation'; } + if (current_match.setup.needs_preparation_successor != null) { + match.setup.needs_preparation_successor = current_match.setup.needs_preparation_successor; + } + if (current_match.setup.needs_preparation_successor_ts != null) { + match.setup.needs_preparation_successor_ts = current_match.setup.needs_preparation_successor_ts; + } const suppression_active = current_match.btp_needsync === true; const suppressed_umpire_btp_id = suppression_active ? current_match.setup.suppressed_umpire_btp_id : null; @@ -338,6 +542,7 @@ function mergeLocalMatchIntoBtpMatch(current_match, match) { if (current_match.setup.umpire && match.setup.umpire && current_match.setup.umpire.btp_id == match.setup.umpire.btp_id && + current_match.btp_needsync === true && ('checked_in' in current_match.setup.umpire)) { match.setup.umpire.checked_in = current_match.setup.umpire.checked_in; } @@ -348,6 +553,7 @@ function mergeLocalMatchIntoBtpMatch(current_match, match) { if (current_match.setup.service_judge && match.setup.service_judge && current_match.setup.service_judge.btp_id == match.setup.service_judge.btp_id && + current_match.btp_needsync === true && ('checked_in' in current_match.setup.service_judge)) { match.setup.service_judge.checked_in = current_match.setup.service_judge.checked_in; } @@ -370,7 +576,8 @@ function mergeLocalMatchIntoBtpMatch(current_match, match) { match.setup.teams[team_index].players[player_index].tablet_break_active = current_match.setup.teams[team_index].players[player_index].tablet_break_active; } - if (current_match.setup.teams[team_index].players[player_index].checked_in != undefined) { + if (current_match.btp_needsync === true && + current_match.setup.teams[team_index].players[player_index].checked_in != undefined) { match.setup.teams[team_index].players[player_index].checked_in = current_match.setup.teams[team_index].players[player_index].checked_in; } } @@ -762,6 +969,11 @@ async function integrate_matches(app, tkey, btp_state, scoring_formats, location const btp_id = calculate_btp_match_id(tkey, bm, draws, events); + if (!(bm.IsMatch && bm.IsMatch[0])) { + cb(null); + return; + } + if (bm.ReverseHomeAway) { cb(null); return; @@ -782,7 +994,7 @@ async function integrate_matches(app, tkey, btp_state, scoring_formats, location return; } - craft_match(app, tkey, btp_id, location_map, court_map, event, stage, scoring_formats, draw, btp_state.links, officials, clubs, districts, bm, match_ids_on_court).then(match => { + craft_match(app, tkey, btp_id, location_map, court_map, event, stage, scoring_formats, draw, btp_state.links, btp_state.planning_nodes, officials, clubs, districts, bm, match_ids_on_court).then(match => { match.setup.state = 'unscheduled'; @@ -859,6 +1071,7 @@ async function integrate_matches(app, tkey, btp_state, scoring_formats, location let only_change_check_in = false; let result_enterd_in_btp = false; let match_player_changed = false; + const current_match_for_check_in_compare = JSON.parse(JSON.stringify(current_match)); for (let team_index = 0; team_index < Math.min(current_match.setup.teams.length, match.setup.teams.length); team_index++) { if(current_match.setup.teams[team_index].players.length < match.setup.teams[team_index].players.length){ @@ -867,7 +1080,7 @@ async function integrate_matches(app, tkey, btp_state, scoring_formats, location } } for (let player_index = 0; player_index < Math.min(current_match.setup.teams[team_index].players.length, match.setup.teams[team_index].players.length); player_index++) { - current_match.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; + current_match_for_check_in_compare.setup.teams[team_index].players[player_index].checked_in = match.setup.teams[team_index].players[player_index].checked_in; if(match.setup.teams[team_index].players[player_index].btp_id != current_match.setup.teams[team_index].players[player_index].btp_id) { match_player_changed = true; } @@ -892,7 +1105,7 @@ async function integrate_matches(app, tkey, btp_state, scoring_formats, location } } - if (utils.plucked_deep_equal(match, current_match, Object.keys(match), true)) { + if (utils.plucked_deep_equal(match, current_match_for_check_in_compare, Object.keys(match), true)) { only_change_check_in = true; } @@ -1004,6 +1217,10 @@ async function reconcile_match_officials(app, tkey, callback) { const admin = require('./admin'); const stournament = require('./stournament'); + app.db.tournaments.findOne({ key: tkey }, (tournamentErr, tournament) => { + if (tournamentErr) { + return callback(tournamentErr); + } app.db.matches.find({ tournament_key: tkey }, (err, matches) => { if (err) { return callback(err); @@ -1068,7 +1285,14 @@ async function reconcile_match_officials(app, tkey, callback) { service_judge_wait: null, umpire_pause: null, service_judge_pause: null, - inactive_list: null + inactive_list: null, + checked_in: match_utils.get_effective_technical_official_checked_in({ + umpire_pause: null, + service_judge_pause: null, + umpire_manual_pause: null, + service_judge_manual_pause: null, + inactive_list: null, + }, tournament) }; changed = true; app.db.umpires.insert(new_official, (insertErr, inserted) => { @@ -1116,6 +1340,11 @@ async function reconcile_match_officials(app, tkey, callback) { } } + const next_checked_in = match_utils.get_effective_technical_official_checked_in({ ...existing, ...setObj }, tournament); + if (!!existing.checked_in !== next_checked_in) { + setObj.checked_in = next_checked_in; + } + if (Object.keys(setObj).length === 0) { return cb(); } @@ -1150,6 +1379,7 @@ async function reconcile_match_officials(app, tkey, callback) { }); }); }); + }); } function generateHallAbbreviation(name) { @@ -1371,6 +1601,8 @@ function integrate_courts(app, tournament_key, btp_state, scoring_formats, locat name, location_id, is_active : true, + has_umpire: true, + has_service_judge: true, }; res.set(btp_id, court._id); @@ -2011,7 +2243,7 @@ function buildOfficialReferenceState(matches) { }; } -function computeOfficialVisibilityPatch(official, refState) { +function computeOfficialVisibilityPatch(official, refState, tournament = null) { const hasId = (set) => set.has(String(official._id)); const hasBtpId = (set) => official.btp_id != null && set.has(String(official.btp_id)); const inSet = (ids, btpIds) => hasId(ids) || hasBtpId(btpIds); @@ -2020,7 +2252,9 @@ function computeOfficialVisibilityPatch(official, refState) { official.umpire_wait != null || official.service_judge_wait != null || official.umpire_pause != null || - official.service_judge_pause != null; + official.service_judge_pause != null || + official.umpire_manual_pause != null || + official.service_judge_manual_pause != null; const referenced_somewhere = inSet(refState.referenced_ids, refState.referenced_btp_ids); const should_be_planned_as_umpire = inSet(refState.planned_umpire_ids, refState.planned_umpire_btp_ids); const should_be_planned_as_service_judge = inSet(refState.planned_service_judge_ids, refState.planned_service_judge_btp_ids); @@ -2047,9 +2281,9 @@ function computeOfficialVisibilityPatch(official, refState) { const now = Date.now(); const reactivated_wait_ts = Math.floor(now / 10); let preferred_role = null; - if (official.umpire_wait != null || official.umpire_pause != null || official.is_planed_as_umpire || official.umpire_on_court != null) { + if (official.umpire_wait != null || official.umpire_pause != null || official.umpire_manual_pause != null || official.is_planed_as_umpire || official.umpire_on_court != null) { preferred_role = 'umpire'; - } else if (official.service_judge_wait != null || official.service_judge_pause != null || official.is_planed_as_service_judge || official.service_judge_on_court != null) { + } else if (official.service_judge_wait != null || official.service_judge_pause != null || official.service_judge_manual_pause != null || official.is_planed_as_service_judge || official.service_judge_on_court != null) { preferred_role = 'service_judge'; } else if (official.is_umpire === true && official.is_service_judge !== true) { preferred_role = 'umpire'; @@ -2077,6 +2311,11 @@ function computeOfficialVisibilityPatch(official, refState) { if (setObj.service_judge_on_court === undefined) setObj.service_judge_on_court = null; } + const next_checked_in = match_utils.get_effective_technical_official_checked_in({ ...official, ...setObj }, tournament); + if (!!official.checked_in !== next_checked_in) { + setObj.checked_in = next_checked_in; + } + return setObj; } @@ -2126,6 +2365,8 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { cur.service_judge_wait == null && cur.umpire_pause == null && cur.service_judge_pause == null && + cur.umpire_manual_pause == null && + cur.service_judge_manual_pause == null && cur.inactive_list == null; @@ -2174,6 +2415,8 @@ function integrate_umpires(app, tournament_key, btp_state, callback) { service_judge_wait: null, umpire_pause: null, service_judge_pause: null, + umpire_manual_pause: null, + service_judge_manual_pause: null, inactive_list: Date.now() }; changed = true; @@ -2229,6 +2472,10 @@ function normalize_official_visibility(app, tournament_key, callback) { const admin = require('./admin'); const stournament = require('./stournament'); + app.db.tournaments.findOne({ key: tournament_key }, (tournamentErr, tournament) => { + if (tournamentErr) { + return callback(tournamentErr); + } app.db.matches.find({ tournament_key }, (matchErr, matches) => { if (matchErr) { return callback(matchErr); @@ -2243,7 +2490,7 @@ function normalize_official_visibility(app, tournament_key, callback) { let changed = false; async.eachSeries(officials, (official, cb) => { - const setObj = computeOfficialVisibilityPatch(official, refState); + const setObj = computeOfficialVisibilityPatch(official, refState, tournament); if (Object.keys(setObj).length === 0) { return cb(); } @@ -2279,6 +2526,7 @@ function normalize_official_visibility(app, tournament_key, callback) { }); }); }); + }); } async function integrate_now_on_court(app, tkey, callback) { @@ -2470,5 +2718,6 @@ module.exports = { _reconcile_match_officials: reconcile_match_officials, _merge_local_match_into_btp_match: mergeLocalMatchIntoBtpMatch, _sanitize_scoring_format: sanitizeScoringFormat, + _resolve_btp_dependency_link, _set_type_to_end_max: setTypeToEndMax, }; diff --git a/static/js/cmatch.js b/static/js/cmatch.js index d57c48b..2c962f8 100644 --- a/static/js/cmatch.js +++ b/static/js/cmatch.js @@ -751,6 +751,147 @@ function on_match_confirm_button_click(e) { } } +function _get_match_planning_id(match) { + return match && match.btp_match_ids && match.btp_match_ids[0] ? match.btp_match_ids[0].planning : null; +} + +function _same_dependency_pair(links_a, links_b) { + if (!links_a || !links_b) { + return false; + } + return links_a.from1 == links_b.from1 && links_a.from2 == links_b.from2; +} + +function _get_source_planning_for_team(match, team_id) { + const links = match && match.setup ? match.setup.links : null; + if (!links) { + return null; + } + return team_id === 0 ? links.from1 : links.from2; +} + +function _find_direct_predecessor_match(match, team_id, matches) { + const source_planning = _get_source_planning_for_team(match, team_id); + if (source_planning == null) { + return null; + } + const source_candidates = (matches || []).filter((candidate) => { + if (!candidate || candidate._id === match._id || !candidate.setup) { + return false; + } + return _get_match_planning_id(candidate) == source_planning; + }); + + const direct_match = utils.find(source_candidates, (candidate) => candidate.setup.is_match); + if (direct_match) { + return direct_match; + } + + const placeholder = source_candidates[0]; + if (!placeholder || !placeholder.setup || !placeholder.setup.links) { + return null; + } + + return utils.find(matches || [], (candidate) => { + if (!candidate || candidate._id === match._id || !candidate.setup || !candidate.setup.is_match) { + return false; + } + return _same_dependency_pair(candidate.setup.links, placeholder.setup.links); + }) || null; +} + +function _find_matches_feeding_planning(source_planning, matches) { + if (source_planning == null) { + return []; + } + const seen = new Set(); + const incoming = []; + (matches || []).forEach((candidate) => { + if (!candidate || !candidate._id || !candidate.setup || !candidate.setup.is_match || !candidate.setup.links) { + return; + } + let relation = null; + if (candidate.setup.links.winner_to == source_planning) { + relation = 'Winner'; + } else if (candidate.setup.links.loser_to == source_planning) { + relation = 'Loser'; + } + if (!relation || seen.has(candidate._id)) { + return; + } + seen.add(candidate._id); + incoming.push({ match: candidate, relation }); + }); + return incoming; +} + +function _format_dependency_from_incoming_edges(source_planning, matches) { + const incoming = _find_matches_feeding_planning(source_planning, matches); + if (incoming.length === 0) { + return null; + } + if (incoming.length > 1) { + const unique_relations = [...new Set(incoming.map((entry) => entry.relation))]; + const incoming_plannings = incoming + .map((entry) => _get_match_planning_id(entry.match)) + .filter((planning) => planning != null); + if (unique_relations.length === 1 && incoming_plannings.length === incoming.length) { + const consolidation_match = utils.find(matches || [], (candidate) => { + if (!candidate || !candidate.setup || !candidate.setup.is_match || !candidate.setup.links) { + return false; + } + const candidate_sources = [candidate.setup.links.from1, candidate.setup.links.from2]; + return incoming_plannings.every((planning) => candidate_sources.includes(planning)); + }); + if (consolidation_match) { + return ci18n(unique_relations[0]) + " #" + consolidation_match.setup.match_num + " - " + consolidation_match.setup.scheduled_date + " " + consolidation_match.setup.scheduled_time_str; + } + } + } + if (incoming.length === 1) { + const entry = incoming[0]; + return ci18n(entry.relation) + " #" + entry.match.setup.match_num + " - " + entry.match.setup.scheduled_date + " " + entry.match.setup.scheduled_time_str; + } + + const unique_relations = [...new Set(incoming.map((entry) => entry.relation))]; + const match_refs = incoming + .map((entry) => "#" + entry.match.setup.match_num) + .sort((a, b) => cbts_utils.cmp(a, b)); + if (unique_relations.length === 1) { + return ci18n(unique_relations[0]) + " " + match_refs.join(' / '); + } + return match_refs.join(' / '); +} + +function _format_participant_dependency(match, team_id, matches) { + const links = match && match.setup ? match.setup.links : null; + if (!links) { + return '???'; + } + + const direct_link_label = team_id === 0 ? links.from1_link : links.from2_link; + if (direct_link_label) { + return direct_link_label; + } + + const predecessor = _find_direct_predecessor_match(match, team_id, matches); + if (predecessor && predecessor.setup && predecessor.setup.links) { + const current_planning = _get_match_planning_id(match); + if (current_planning != null && predecessor.setup.links.winner_to == current_planning) { + return ci18n('Winner') + " #" + predecessor.setup.match_num + " - " + predecessor.setup.scheduled_date + " " + predecessor.setup.scheduled_time_str; + } + if (current_planning != null && predecessor.setup.links.loser_to == current_planning) { + return ci18n('Loser') + " #" + predecessor.setup.match_num + " - " + predecessor.setup.scheduled_date + " " + predecessor.setup.scheduled_time_str; + } + } + const source_planning = _get_source_planning_for_team(match, team_id); + const incoming_dependency = _format_dependency_from_incoming_edges(source_planning, matches); + if (incoming_dependency) { + return incoming_dependency; + } + return '???'; +} + function render_players_el(parentNode, setup, team_id, match, show_player_status, style) { const team = setup.teams[team_id]; @@ -765,60 +906,8 @@ function render_players_el(parentNode, setup, team_id, match, show_player_status } render_player_el(parentNode, team.players[0], match._id, setup.now_on_court, show_player_status, style, team.players.length > 1 ? true : false); } else { - - let dependency = '???'; - if(team_id == 0 && match.setup.links.from1_link) { - dependency = match.setup.links.from1_link; - } - else if(team_id == 1 && match.setup.links.from2_link) { - dependency = match.setup.links.from2_link; - } - else { - let match_before = curt.matches.filter(m => { - - return (m.setup.event_name === match.setup.event_name && - ( - m.btp_match_ids[0].planning == match.setup.links.from1 || - m.btp_match_ids[0].planning == match.setup.links.from2 - )); - }); - - let resolved_match = []; - - match_before.forEach(t => { - if(t.setup.is_match){ - resolved_match.push(t); - } - else { - const result = curt.matches.find(m => { - return (m.setup.event_name === t.setup.event_name && - m.setup.is_match && - m.setup.links && - t.setup.links && - ( - m.setup.links.from1 == t.setup.links.from1 || - m.setup.links.from2 == t.setup.links.from2 - )); - }); - resolved_match.push(result); - } - }); - - const index = Math.min(resolved_match.length - 1, team_id); - - if(resolved_match.length >= index && resolved_match[index] && resolved_match[index].setup && resolved_match[index].setup.links) { - - if(resolved_match[index].setup.links.winner_to && resolved_match[index].setup.links.winner_to == match.btp_match_ids[0].planning) { - dependency = ci18n('Winner') + " #" + resolved_match[index].setup.match_num + " - " + resolved_match[index].setup.scheduled_date + " " + resolved_match[index].setup.scheduled_time_str; - } - else { - dependency = ci18n('Loser') + " #" + resolved_match[index].setup.match_num + " - " + resolved_match[index].setup.scheduled_date + " " + resolved_match[index].setup.scheduled_time_str; - } - } - } - + const dependency = _format_participant_dependency(match, team_id, curt.matches); uiu.el(parentNode, 'span', {}, dependency); - } if (team.players.length > 1) { @@ -837,7 +926,13 @@ function render_players_el(parentNode, setup, team_id, match, show_player_status } function render_match_participant_el(parentNode, participant, match_id, role, icon_class, show_check_in_status = true) { - const participant_status = show_check_in_status && participant && participant.checked_in ? 'checked_in' : (show_check_in_status ? 'not_checked_in' : 'no_status'); + const technical_official_check_in_locked = + (role === 'umpire' || role === 'service_judge') && + curt && + curt.btp_settings && + curt.btp_settings.check_in_per_match === false; + const participant_checked_in = technical_official_check_in_locked ? true : !!(participant && participant.checked_in); + const participant_status = show_check_in_status && participant_checked_in ? 'checked_in' : (show_check_in_status ? 'not_checked_in' : 'no_status'); const participant_el = uiu.el(parentNode, 'span', { 'class': 'person ' + participant_status, 'data-btp_id': participant.btp_id, @@ -848,7 +943,7 @@ function render_match_participant_el(parentNode, participant, match_id, role, ic uiu.el(participant_el, 'div', icon_class, ''); const name_el = uiu.el(participant_el, 'span', 'name', participant.name || short_name(participant.firstname, participant.lastname || participant.surname, participant.name)); - if (show_check_in_status && participant.btp_id != null && participant.btp_id >= 0) { + if (show_check_in_status && participant.btp_id != null && participant.btp_id >= 0 && !technical_official_check_in_locked) { if (participant_status === 'checked_in') { participant_el.classList.add('can_check_out'); } else { @@ -1154,10 +1249,6 @@ function create_timer(timer_state, parent, default_color, exigent_color) { } else { tobj.timeout = null; el.style.display = "none"; - if(!curt.btp_settings.check_in_per_match && parent.querySelector('div.tablet_inline') === null) { - parent.classList.remove("not_checked_in"); - parent.classList.add("checked_in"); - } } }; @@ -1187,13 +1278,25 @@ function _extract_player_timer_state(player) { } function _extract_preparation_timer_state(match) { + if (!match || !match.setup) { + return null; + } + if (match.setup.state !== 'preparation') { + return null; + } + if (!match.setup.highlight || match.setup.highlight <= 0) { + return null; + } + if (!match.setup.preparation_call_timestamp) { + return null; + } + let s = {}; s.settings = {}; s.settings.negative_timers = false; s.lang = "de"; s.timer = {}; - //s.timer.duration = curt.btp_settings.pause_duration_ms; - s.timer.start = (match.setup.preparation_call_timestamp ? match.setup.preparation_call_timestamp : false); + s.timer.start = match.setup.preparation_call_timestamp; s.timer.upwards = true; s.timer.exigent = false; s.bgColor = "#00000033"; @@ -1208,8 +1311,16 @@ function _extract_match_timer_state(match) { s.settings.negative_timers = true; s.lang = (curt && curt.btp_settings && curt.btp_settings.language && curt.btp_settings.language !== 'auto') ? curt.btp_settings.language : "de"; - var rs = calc.remote_state(s, match.setup, presses); - return rs; + try { + return calc.remote_state(s, match.setup, presses); + } catch (err) { + const label = match && match.setup && match.setup.match_num ? `#${match.setup.match_num}` : (match && match._id ? match._id : ''); + console.error(`[bts] calc.remote_state failed for ${label}`, err); + if (typeof cerror !== 'undefined' && cerror && cerror.silent) { + cerror.silent(`Timer state for ${label} could not be calculated: ${err.message}`); + } + return false; + } } function cmp_scheduled_match_order(m1, m2) { @@ -1552,6 +1663,18 @@ function _update_setup(setup, d) { result.court_id = d.court_id; result.now_on_court = !! d.now_on_court; + if (d.preparation_location_id) { + result.state = 'preparation'; + result.location_id = d.preparation_location_id; + if (!result.preparation_call_timestamp) { + result.preparation_call_timestamp = Date.now(); + } + } else if (result.state === 'preparation') { + result.state = 'scheduled'; + result.highlight = 0; + delete result.location_id; + delete result.preparation_call_timestamp; + } if(!d.umpire_name) { delete result.umpire; @@ -1721,7 +1844,12 @@ function ui_edit(match_id) { cerror.silent(ci18n('match:edit:error:service_judge_requires_umpire')); return; } + const previous_setup = structuredClone(match.setup); match.setup = _update_setup(match.setup, d); + const force_btp_update = + (previous_setup.state || null) !== (match.setup.state || null) || + (previous_setup.location_id || null) !== (match.setup.location_id || null) || + (previous_setup.highlight || 0) !== (match.setup.highlight || 0); btn.setAttribute('disabled', 'disabled'); send({ type: 'match_edit', @@ -1729,7 +1857,7 @@ function ui_edit(match_id) { match, old_court, tournament_key: curt.key, - btp_update: (curt.btp_enabled && !! d.btp_update), + btp_update: (curt.btp_enabled && (!! d.btp_update || force_btp_update)), }, function match_edit_callback(err) { btn.removeAttribute('disabled'); if (err) { @@ -1806,7 +1934,13 @@ function ui_scoresheet(match_id) { i18n.register_lang(i18n_en); const setup = utils.deep_copy(match.setup); setup.tournament_name = curt.name; - const s = calc.remote_state(pseudo_state, setup, match.presses); + let s = null; + try { + s = calc.remote_state(pseudo_state, setup, match.presses); + } catch (err) { + console.error(`[bts] scoresheet remote_state failed for #${setup.match_num || '?'} (${match._id})`, err); + return cerror.silent(`Scoresheet for #${setup.match_num || '?'} cannot be rendered: ${err.message}`); + } s.ui = {}; printing.set_orientation('landscape'); @@ -2126,6 +2260,9 @@ function render_empty_court_row(tr, court, style, is_droppable) { function update_court(court) { const tr = uiu.qs(`tr[data-court_id="${court._id}"]`); + if (!tr) { + return; + } const match_id = tr.getAttribute('data-match_id'); const style = tr.getAttribute("data-style"); tr.innerHTML = ""; @@ -2134,6 +2271,11 @@ function update_court(court) { return; } const m = utils.find(curt.matches, m => m._id === match_id); + if (!m || !m.setup || m.setup.now_on_court !== true || ['finished'].includes(m.setup.state)) { + tr.removeAttribute('data-match_id'); + render_empty_court_row(tr, court, style, true); + return; + } render_match_row(tr, m, court, style); } @@ -2498,6 +2640,28 @@ function render_edit(form, match) { const assigned = uiu.el(edit_match_container, 'div', { style: 'margin-top: 1em', }); + uiu.el(assigned, 'span', 'match_label', ci18n('match:edit:preparation')); + const preparation_select = uiu.el(assigned, 'select', { + name: 'preparation_location_id', + size: 1, + }); + uiu.el(preparation_select, 'option', { + value: '', + selected: setup.state === 'preparation' ? undefined : 'selected', + }, ci18n('match:edit:not_in_preparation')); + if (curt && curt.locations) { + for (const location of curt.locations) { + const attrs = { + value: location._id, + }; + if (setup.state === 'preparation' && location._id === setup.location_id) { + attrs.selected = 'selected'; + } + uiu.el(preparation_select, 'option', attrs, ci18n('match:edit:in_preparation_for', { + location_name: location.name || location._id, + })); + } + } uiu.el(assigned, 'span', 'match_label', 'Court:'); const court_select = uiu.el(assigned, 'select', { 'class': 'court_selector', @@ -2689,6 +2853,8 @@ function build_official_select_entries(tournament, is_service_judge, show_all_of label: `--- ${label} ---` }); }; + const is_waiting_list_label = (label) => + label === ci18n('Waiting list umpire') || label === ci18n('Waiting list service judge'); const append_options = (items, role, secondary_mode = false) => { for (const official of items) { entries.push({ @@ -2718,6 +2884,8 @@ function build_official_select_entries(tournament, is_service_judge, show_all_of const secondary_wait_field = `${secondary_role}_wait`; const primary_pause_field = `${primary_role}_pause`; const secondary_pause_field = `${secondary_role}_pause`; + const primary_manual_pause_field = `${primary_role}_manual_pause`; + const secondary_manual_pause_field = `${secondary_role}_manual_pause`; const preparation_matches = [...(curt.matches || [])] .filter((match) => (match.setup || {}).state === 'preparation') .sort((a, b) => (a.setup?.preparation_call_timestamp || 0) - (b.setup?.preparation_call_timestamp || 0)); @@ -2751,11 +2919,11 @@ function build_official_select_entries(tournament, is_service_judge, show_all_of secondary_wait_field ); const primary_pause_items = sort_by_wait( - all_officials.filter((u) => u[primary_pause_field] != null && should_render_in_lower_lists(u)), + all_officials.filter((u) => (u[primary_pause_field] != null || u[primary_manual_pause_field] != null) && should_render_in_lower_lists(u)), primary_pause_field ); const secondary_pause_items = sort_by_wait( - all_officials.filter((u) => u[secondary_pause_field] != null && should_render_in_lower_lists(u)), + all_officials.filter((u) => (u[secondary_pause_field] != null || u[secondary_manual_pause_field] != null) && should_render_in_lower_lists(u)), secondary_pause_field ); const preparation_primary = sort_by_name(preparation_matches.map((match) => match.setup && match.setup[primary_role]).filter(Boolean)); @@ -2772,7 +2940,7 @@ function build_official_select_entries(tournament, is_service_judge, show_all_of ); const fallback_inactive_officials = all_officials .filter((u) => !visible_official_ids.has(u._id)) - .filter((u) => u.umpire_wait == null && u.service_judge_wait == null && u.umpire_pause == null && u.service_judge_pause == null && u.inactive_list == null) + .filter((u) => u.umpire_wait == null && u.service_judge_wait == null && u.umpire_pause == null && u.service_judge_pause == null && u.umpire_manual_pause == null && u.service_judge_manual_pause == null && u.inactive_list == null) .sort((a, b) => cbts_utils.natcmp(a.name || '', b.name || '')); fallback_inactive_officials.forEach((official) => { if (official.is_umpire && !official.is_service_judge) { @@ -2804,7 +2972,7 @@ function build_official_select_entries(tournament, is_service_judge, show_all_of let rendered_any = false; sections.forEach((section) => { if (!section.items.length) return; - if (rendered_any) { + if (rendered_any || !is_waiting_list_label(section.label)) { append_separator(section.label.replace(/:$/, '')); } append_options(section.items, section.role, section.secondary_mode); @@ -2938,7 +3106,8 @@ return { update_players, create_timer, update_tables, - _build_official_select_entries: build_official_select_entries + _build_official_select_entries: build_official_select_entries, + _format_participant_dependency }; })(); diff --git a/test/test_btp_proto.js b/test/test_btp_proto.js index 9ca50a9..6384f6f 100644 --- a/test/test_btp_proto.js +++ b/test/test_btp_proto.js @@ -1,29 +1,45 @@ 'use strict'; -const assert = require('assert').strict; -const fs = require('fs'); -const path = require('path'); +const assert = require('assert'); +const { _describe, _it } = require('./tutils.js'); +const btp_proto = require('../bts/btp_proto.js'); -const tutils = require('./tutils.js'); -const _describe = tutils._describe; -const _it = tutils._it; +function extract_first_match_status(req) { + return req.Update.Tournament.Matches[0].Match.Status; +} -const {_req2xml: req2xml} = require('../bts/btp_proto'); +_describe('btp_proto update_request', () => { + _it('writes match check-in bits in check-in per match mode', () => { + const req = btp_proto.update_request({ + btp_match_ids: [{ id: 1, draw: 2, planning: 3 }], + setup: { + highlight: 0, + teams: [ + { players: [{ checked_in: true }, { checked_in: false }] }, + { players: [{ checked_in: true }, { checked_in: true }] }, + ] + } + }, 'unicode', null, null, null, null, { + write_match_check_in_status: true, + }); + assert.strictEqual(extract_first_match_status(req), 0b1101); + }); + + _it('does not write match check-in bits in check-in per player mode', () => { + const req = btp_proto.update_request({ + btp_match_ids: [{ id: 1, draw: 2, planning: 3 }], + setup: { + highlight: 0, + teams: [ + { players: [{ checked_in: true }, { checked_in: true }] }, + { players: [{ checked_in: true }, { checked_in: true }] }, + ] + } + }, 'unicode', null, null, null, null, { + write_match_check_in_status: false, + }); -_describe('btp_proto', function() { - _it('Timezone encoding', async function() { - assert.deepStrictEqual( - req2xml({test_date: new Date(1652529397790)}, 'Europe/Berlin'), - ('' + - '' + - '' + - '')); - assert.deepStrictEqual( - req2xml({test_date: new Date(1652529397790)}, 'America/New_York'), - ('' + - '' + - '' + - '')); + assert.strictEqual(extract_first_match_status(req), 0); }); }); diff --git a/test/test_btp_sync.js b/test/test_btp_sync.js index 93e908a..21fdf37 100644 --- a/test/test_btp_sync.js +++ b/test/test_btp_sync.js @@ -126,6 +126,107 @@ _describe('btp_sync', () => { assert.strictEqual(normalized.last_set_points.interval_at, 8); }); + _it('resolves direct visible predecessor links for placement matches without importing extra matches', () => { + const planning_nodes = new Map([ + ['37_4009', { + DrawID: ['37'], + PlanningID: ['4009'], + IsMatch: [true], + MatchNr: ['18'], + WinnerTo: ['3005'], + LoserTo: ['3009'], + PlannedTime: [{ year: 2026, month: 4, day: 18, hour: 11, minute: 30 }], + }], + ]); + + const label = btp_sync._resolve_btp_dependency_link('37', '4009', '3005', [], planning_nodes); + + assert.strictEqual(label, 'Gewinner #18 - 2026-04-18 11:30'); + }); + + _it('resolves hidden loser slots via the visible consolidation match number', () => { + const planning_nodes = new Map([ + ['37_3004', { + DrawID: ['37'], + PlanningID: ['3004'], + IsMatch: [true], + MatchNr: ['17'], + From1: ['4007'], + From2: ['4008'], + WinnerTo: ['2002'], + LoserTo: ['4010'], + PlannedTime: [{ year: 2026, month: 4, day: 18, hour: 11, minute: 0 }], + }], + ['37_4007', { + DrawID: ['37'], + PlanningID: ['4007'], + IsMatch: [true], + MatchNr: ['7'], + LoserTo: ['4010'], + PlannedTime: [{ year: 2026, month: 4, day: 18, hour: 10, minute: 30 }], + }], + ['37_4008', { + DrawID: ['37'], + PlanningID: ['4008'], + IsMatch: [true], + MatchNr: ['8'], + LoserTo: ['4010'], + PlannedTime: [{ year: 2026, month: 4, day: 18, hour: 10, minute: 30 }], + }], + ]); + + const label = btp_sync._resolve_btp_dependency_link('37', '4010', '3005', [], planning_nodes); + + assert.strictEqual(label, 'Verlierer #17 - 2026-04-18 11:00'); + }); + + _it('resolves hidden predecessor nodes that only expose MatchNr without IsMatch', () => { + const planning_nodes = new Map([ + ['37_3013', { + DrawID: ['37'], + PlanningID: ['3013'], + MatchNr: ['18'], + From1: ['5017'], + From2: ['5018'], + WinnerTo: ['2013'], + LoserTo: ['2015'], + }], + ]); + + const label = btp_sync._resolve_btp_dependency_link('37', '3013', '2013', [], planning_nodes); + + assert.strictEqual(label, 'Gewinner #18'); + }); + + _it('derives time from the visible sibling match when a hidden placement node feeds the target', () => { + const planning_nodes = new Map([ + ['39_2003', { + DrawID: ['39'], + PlanningID: ['2003'], + MatchNr: ['49'], + From1: ['3001'], + From2: ['3002'], + WinnerTo: ['1003'], + LoserTo: ['1004'], + }], + ['39_2001', { + DrawID: ['39'], + PlanningID: ['2001'], + IsMatch: [true], + MatchNr: ['49'], + From1: ['3001'], + From2: ['3002'], + WinnerTo: ['1001'], + LoserTo: ['1002'], + PlannedTime: [{ year: 2026, month: 4, day: 19, hour: 14, minute: 0 }], + }], + ]); + + const label = btp_sync._resolve_btp_dependency_link('39', '2003', '1003', [], planning_nodes); + + assert.strictEqual(label, 'Gewinner #49 - 2026-04-19 14:00'); + }); + _it('sanitizes end_points to be at least 1 and max_points to be at least end_points', () => { const sanitized = btp_sync._sanitize_scoring_format({ id: 99, @@ -421,6 +522,11 @@ _describe('btp_sync', () => { }; const app = { db: { + tournaments: { + findOne(query, cb) { + cb(null, { key: 't1', btp_settings: { check_in_per_match: false } }); + } + }, matches: { find(query, cb) { cb(null, state.matches); @@ -490,6 +596,30 @@ _describe('btp_sync', () => { assert.strictEqual(staleMerged.setup.umpire, undefined); }); + _it('drops stale local preparation state when the highlight is already cleared', () => { + const currentMatch = { + btp_needsync: false, + setup: { + state: 'preparation', + highlight: 0, + preparation_call_timestamp: 1775701859486, + teams: [{ players: [] }, { players: [] }], + }, + }; + const btpMatch = { + setup: { + state: 'scheduled', + highlight: 0, + teams: [{ players: [] }, { players: [] }], + }, + }; + + const merged = btp_sync._merge_local_match_into_btp_match(currentMatch, structuredClone(btpMatch)); + + assert.strictEqual(merged.setup.state, 'scheduled'); + assert.strictEqual(merged.setup.preparation_call_timestamp, undefined); + }); + _it('clears stale planned and on-court flags when an official is no longer referenced by matches', () => { const refState = btp_sync._build_official_reference_state([]); const patch = btp_sync._compute_official_visibility_patch({ diff --git a/test/test_cmatch.js b/test/test_cmatch.js index c38f690..c127142 100644 --- a/test/test_cmatch.js +++ b/test/test_cmatch.js @@ -1,11 +1,131 @@ 'use strict'; const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); const {_describe, _it} = require('./tutils.js'); const match_scoring = require('../static/js/match_scoring'); +function createFakeElement(tagName) { + return { + tagName, + children: [], + attributes: {}, + style: {}, + textContent: '', + className: '', + classList: { + _values: new Set(), + add(...names) { + names.filter(Boolean).forEach((name) => this._values.add(name)); + }, + remove(...names) { + names.forEach((name) => this._values.delete(name)); + }, + contains(name) { + return this._values.has(name); + }, + }, + setAttribute(name, value) { + this.attributes[name] = String(value); + }, + getAttribute(name) { + return Object.prototype.hasOwnProperty.call(this.attributes, name) ? this.attributes[name] : null; + }, + removeAttribute(name) { + delete this.attributes[name]; + }, + appendChild(child) { + this.children.push(child); + return child; + }, + addEventListener() {}, + }; +} + +function loadCmatchForUpdateCourtTest(tr, curt) { + const source = fs.readFileSync(path.join(__dirname, '..', 'static', 'js', 'cmatch.js'), 'utf8'); + const context = { + console, + setTimeout() { return 1; }, + clearTimeout() {}, + requestAnimationFrame() {}, + window: { + innerWidth: 1200, + addEventListener() {}, + getComputedStyle() { + return { + getPropertyValue() { + return '16px'; + } + }; + }, + }, + document: { + querySelectorAll() { return []; }, + }, + curt, + crouting: { register() {} }, + change: { default_handler(fn) { return fn; } }, + ci18n: (key) => key, + cerror: { net() {}, silent() {} }, + cflags: { render_flag_el() {} }, + cbts_utils: { cmp(a, b) { return a === b ? 0 : (a < b ? -1 : 1); } }, + ctabletoperator: { add_to_tabletoperator() {} }, + utils: { + find(list, predicate) { + return (list || []).find(predicate); + }, + remove(list, predicate) { + const idx = (list || []).findIndex(predicate); + if (idx === -1) { + return false; + } + list.splice(idx, 1); + return true; + }, + }, + uiu: { + qs(selector) { + if (selector === `tr[data-court_id="${tr.getAttribute('data-court_id')}"]`) { + return tr; + } + return null; + }, + qsEach() {}, + el(parent, tagName, attrsOrClass, text) { + const el = createFakeElement(tagName); + if (typeof attrsOrClass === 'string') { + el.className = attrsOrClass; + if (attrsOrClass) { + el.classList.add(...attrsOrClass.split(/\s+/).filter(Boolean)); + } + } else if (attrsOrClass && typeof attrsOrClass === 'object') { + Object.entries(attrsOrClass).forEach(([key, value]) => { + if (key === 'class') { + el.className = value; + el.classList.add(...String(value).split(/\s+/).filter(Boolean)); + } else { + el.setAttribute(key, value); + } + }); + } + if (text !== undefined) { + el.textContent = text; + } + parent.appendChild(el); + return el; + }, + }, + }; + vm.createContext(context); + vm.runInContext(source, context, { filename: 'cmatch.js' }); + return context.cmatch; +} + _describe('cmatch', () => { _it('detects match end for default 3x21 fallback', () => { assert.strictEqual(match_scoring.is_match_over([[21, 10], [21, 18]], null), true); @@ -33,4 +153,305 @@ _describe('cmatch', () => { assert.strictEqual(match_scoring.is_match_over([[21, 18], [19, 21], [11, 9]], scoringFormat), true); assert.strictEqual(match_scoring.is_match_over([[21, 18], [19, 21], [10, 9]], scoringFormat), false); }); + + _it('clears a stale finished match from a court row when the court is updated', () => { + const tr = createFakeElement('tr'); + tr.setAttribute('data-court_id', 'court-1'); + tr.setAttribute('data-match_id', 'match-1'); + tr.setAttribute('data-style', 'default'); + tr.innerHTML = 'stale'; + + const cmatch = loadCmatchForUpdateCourtTest(tr, { + matches: [{ + _id: 'match-1', + setup: { + is_match: true, + state: 'finished', + now_on_court: false, + }, + }], + }); + + cmatch.update_court({ _id: 'court-1', num: '1', is_active: true }); + + assert.strictEqual(tr.getAttribute('data-match_id'), null); + assert.strictEqual(tr.children.length, 3); + assert.strictEqual(tr.children[1].classList.contains('court_number'), true); + assert.strictEqual(tr.children[2].classList.contains('empty_element'), true); + }); + + _it('keeps the court match row when the referenced match is still on court', () => { + const tr = createFakeElement('tr'); + tr.setAttribute('data-court_id', 'court-1'); + tr.setAttribute('data-match_id', 'match-1'); + tr.setAttribute('data-style', 'public'); + tr.innerHTML = 'old'; + + const cmatch = loadCmatchForUpdateCourtTest(tr, { + key: 'default', + btp_settings: { check_in_per_match: false }, + courts_by_id: { + 'court-1': { _id: 'court-1', num: '1', is_active: true } + }, + matches: [{ + _id: 'match-1', + setup: { + is_match: true, + state: 'oncourt', + now_on_court: true, + court_id: 'court-1', + teams: [ + { players: [{ btp_id: 1, name: 'Alice Example', firstname: 'Alice', lastname: 'Example', checked_in: true }] }, + { players: [{ btp_id: 2, name: 'Bob Example', firstname: 'Bob', lastname: 'Example', checked_in: true }] }, + ], + match_num: 12, + scheduled_date: '2026-04-15', + scheduled_time_str: '10:00', + }, + }], + }); + + cmatch.update_court({ _id: 'court-1', num: '1', is_active: true }); + + assert.strictEqual(tr.getAttribute('data-match_id'), 'match-1'); + assert.notStrictEqual(tr.children.length, 0); + }); + + _it('resolves CP-VF participant dependencies per slot instead of by shared candidate order', () => { + const tr = createFakeElement('tr'); + tr.setAttribute('data-court_id', 'court-1'); + const cmatch = loadCmatchForUpdateCourtTest(tr, { + matches: [], + }); + + const target = { + _id: 'target', + btp_match_ids: [{ planning: 3005 }], + setup: { + links: { + from1: 4009, + from2: 4010, + } + } + }; + const predecessor = { + _id: 'pred-1', + btp_match_ids: [{ planning: 4009 }], + setup: { + is_match: true, + match_num: 18, + scheduled_date: '2026-04-18', + scheduled_time_str: '11:30', + links: { + winner_to: 3005, + loser_to: 3009, + } + } + }; + + assert.strictEqual( + cmatch._format_participant_dependency(target, 0, [predecessor]), + 'Winner #18 - 2026-04-18 11:30' + ); + assert.strictEqual( + cmatch._format_participant_dependency(target, 1, [predecessor]), + '???' + ); + }); + + _it('uses direct link labels before trying to infer a predecessor match', () => { + const tr = createFakeElement('tr'); + tr.setAttribute('data-court_id', 'court-1'); + const cmatch = loadCmatchForUpdateCourtTest(tr, { + matches: [], + }); + + const target = { + _id: 'target', + btp_match_ids: [{ planning: 3005 }], + setup: { + links: { + from1: 4009, + from2: 4010, + from1_link: 'CP-VF (5/12)', + } + } + }; + + assert.strictEqual( + cmatch._format_participant_dependency(target, 0, []), + 'CP-VF (5/12)' + ); + }); + + _it('resolves a predecessor through a placeholder planning node for the correct slot', () => { + const tr = createFakeElement('tr'); + tr.setAttribute('data-court_id', 'court-1'); + const cmatch = loadCmatchForUpdateCourtTest(tr, { + matches: [], + }); + + const target = { + _id: 'target', + btp_match_ids: [{ planning: 3005 }], + setup: { + links: { + from1: 4010, + from2: 4011, + } + } + }; + const placeholder = { + _id: 'placeholder-1', + btp_match_ids: [{ planning: 4010 }], + setup: { + is_match: false, + links: { + from1: 5017, + from2: 5018, + } + } + }; + const predecessor = { + _id: 'pred-1', + btp_match_ids: [{ planning: 4009 }], + setup: { + is_match: true, + match_num: 18, + scheduled_date: '2026-04-18', + scheduled_time_str: '11:30', + links: { + from1: 5017, + from2: 5018, + winner_to: 3005, + loser_to: 3009, + } + } + }; + + assert.strictEqual( + cmatch._format_participant_dependency(target, 0, [placeholder, predecessor]), + 'Winner #18 - 2026-04-18 11:30' + ); + assert.strictEqual( + cmatch._format_participant_dependency(target, 1, [placeholder, predecessor]), + '???' + ); + }); + + _it('falls back to incoming loser edges when a source planning has no local match node', () => { + const tr = createFakeElement('tr'); + tr.setAttribute('data-court_id', 'court-1'); + const cmatch = loadCmatchForUpdateCourtTest(tr, { + matches: [], + }); + + const target = { + _id: 'target', + btp_match_ids: [{ planning: 3005 }], + setup: { + links: { + from1: 4009, + from2: 4010, + } + } + }; + const feederA = { + _id: 'r16-7', + btp_match_ids: [{ planning: 4007 }], + setup: { + is_match: true, + match_num: 7, + scheduled_date: '2026-04-18', + scheduled_time_str: '09:00', + links: { + loser_to: 4010, + } + } + }; + const feederB = { + _id: 'r16-8', + btp_match_ids: [{ planning: 4008 }], + setup: { + is_match: true, + match_num: 8, + scheduled_date: '2026-04-18', + scheduled_time_str: '09:30', + links: { + loser_to: 4010, + } + } + }; + + assert.strictEqual( + cmatch._format_participant_dependency(target, 1, [feederB, feederA]), + 'Loser #7 / #8' + ); + }); + + _it('prefers the visible consolidation match number for hidden loser slots', () => { + const tr = createFakeElement('tr'); + tr.setAttribute('data-court_id', 'court-1'); + const cmatch = loadCmatchForUpdateCourtTest(tr, { + matches: [], + }); + + const target = { + _id: 'target', + btp_match_ids: [{ planning: 3005 }], + setup: { + links: { + from1: 4009, + from2: 4010, + } + } + }; + const feederA = { + _id: 'r16-7', + btp_match_ids: [{ planning: 4007 }], + setup: { + is_match: true, + match_num: 7, + scheduled_date: '2026-04-18', + scheduled_time_str: '09:00', + links: { + loser_to: 4010, + } + } + }; + const feederB = { + _id: 'r16-8', + btp_match_ids: [{ planning: 4008 }], + setup: { + is_match: true, + match_num: 8, + scheduled_date: '2026-04-18', + scheduled_time_str: '09:30', + links: { + loser_to: 4010, + } + } + }; + const visibleConsolidation = { + _id: 'vf-17', + btp_match_ids: [{ planning: 3004 }], + setup: { + is_match: true, + match_num: 17, + scheduled_date: '2026-04-18', + scheduled_time_str: '11:00', + links: { + from1: 4007, + from2: 4008, + winner_to: 2002, + loser_to: 2004, + } + } + }; + + assert.strictEqual( + cmatch._format_participant_dependency(target, 1, [feederA, feederB, visibleConsolidation]), + 'Loser #17 - 2026-04-18 11:00' + ); + }); }); From 8321f98a21dd39ef11f35613e0930db6b5c17afd Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 15 Apr 2026 08:16:23 +0200 Subject: [PATCH 223/224] Add automated preparation and official workflow backend --- bts/admin.js | 641 +++++++--- bts/bts.js | 2 + bts/bupws.js | 204 +++- bts/match_automation.js | 1397 ++++++++++++++++++++++ bts/match_utils.js | 1283 ++++++++++++++++++-- bts/update_queue.js | 25 +- test/test_bupws.js | 94 ++ test/test_match_automation.js | 1786 ++++++++++++++++++++++++++++ test/test_match_utils_officials.js | 645 ++++++++++ 9 files changed, 5796 insertions(+), 281 deletions(-) create mode 100644 bts/match_automation.js create mode 100644 test/test_bupws.js create mode 100644 test/test_match_automation.js create mode 100644 test/test_match_utils_officials.js diff --git a/bts/admin.js b/bts/admin.js index e7e37cd..08d1699 100644 --- a/bts/admin.js +++ b/bts/admin.js @@ -12,6 +12,7 @@ const serror = require('./serror'); const stournament = require('./stournament'); const ticker_manager = require('./ticker_manager'); const utils = require('./utils'); +const match_automation = require('./match_automation'); /** @@ -64,6 +65,7 @@ function handle_tournament_edit_props(app, ws, msg) { const key = msg.key; const props = utils.pluck(msg.props, [ 'name','tguid', + 'automation_enabled', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'btp_ip', 'btp_password','btp_autofetch_timeout_intervall', 'is_team', 'is_nation_competition', @@ -80,7 +82,35 @@ function handle_tournament_edit_props(app, ws, msg) { 'tabletoperator_use_manual_counting_boards_enabled', 'tabletoperator_with_umpire_enabled', 'annoncement_include_event', 'annoncement_include_round','annoncement_include_matchnumber', 'preparation_meetingpoint_enabled', 'preparation_tabletoperator_setup_enabled', - 'call_preparation_matches_automatically_enabled', 'call_next_possible_scheduled_match_in_preparation' , + 'call_preparation_matches_automatically_enabled', 'call_next_possible_scheduled_match_in_preparation', + 'preparation_successor_rally_count', + 'preparation_call_time_limit_before_scheduled_enabled', + 'preparation_call_time_limit_before_scheduled_minutes', + 'preparation_call_block_ahead_limit_enabled', + 'preparation_call_block_ahead_limit', + 'preparation_call_time_ahead_of_frontier_enabled', + 'preparation_call_time_ahead_of_frontier_minutes', + 'preparation_call_matches_ahead_of_frontier_enabled', + 'preparation_call_matches_ahead_of_frontier_limit', + 'preparation_call_player_pause_expired_enabled', + 'preparation_call_technical_officials_available_enabled', + 'call_on_court_time_limit_before_scheduled_enabled', + 'call_on_court_time_limit_before_scheduled_minutes', + 'call_on_court_only_preparation_enabled', + 'call_on_court_only_preparation_minutes', + 'call_on_court_block_ahead_limit_enabled', + 'call_on_court_block_ahead_limit', + 'call_on_court_time_ahead_of_frontier_enabled', + 'call_on_court_time_ahead_of_frontier_minutes', + 'call_on_court_matches_ahead_of_frontier_enabled', + 'call_on_court_matches_ahead_of_frontier_limit', + 'call_on_court_participant_readiness_mode', + 'call_on_court_player_pause_expired_enabled', + 'call_on_court_technical_officials_mode', + 'call_on_court_require_official_space_enabled', + 'official_rotation_mode', + 'technical_official_auto_assignment_mode', + 'technical_official_break_after_assignment_seconds', 'logo_background_color', 'logo_foreground_color', 'scoring_formats']); if (msg.props.btp_timezone) { @@ -103,6 +133,29 @@ function handle_tournament_edit_props(app, ws, msg) { ticker_manager.reconfigure(app, t); } notify_change(app, key, 'props', t); + if (utils.has_key(props, (k) => k === 'technical_official_auto_assignment_mode' || k === 'official_rotation_mode')) { + const match_utils = require('./match_utils'); + match_utils.queue_auto_assign_technical_officials_when_available(app, key); + } + if (utils.has_key(props, (k) => k === 'technical_official_break_after_assignment_seconds')) { + const match_utils = require('./match_utils'); + match_utils.queue_process_expired_technical_official_breaks(app, key); + } + if (props.automation_enabled === true) { + const match_utils = require('./match_utils'); + match_utils.queue_auto_assign_technical_officials_when_available(app, key); + match_utils.queue_auto_execute_preparation_selections(app, key, (selectionErr) => { + if (selectionErr) { + console.warn('[bts] failed to resume preparation automation', selectionErr && (selectionErr.stack || selectionErr.message || String(selectionErr))); + return; + } + match_utils.auto_call_matches_on_free_courts(app, key, (callErr) => { + if (callErr) { + console.warn('[bts] failed to resume on-court automation', callErr && (callErr.stack || callErr.message || String(callErr))); + } + }); + }); + } if (!tournament.displaysettings_general || (tournament.displaysettings_general != t.displaysettings_general)){ @@ -125,6 +178,7 @@ function handle_tournament_edit_prop(app, ws, msg) { const allowed_fields = new Set([ 'name', 'tguid', + 'automation_enabled', 'btp_enabled', 'btp_autofetch_enabled', 'btp_readonly', 'btp_ip', 'btp_password', 'btp_autofetch_timeout_intervall', 'btp_timezone', 'is_team', 'is_nation_competition', @@ -142,6 +196,34 @@ function handle_tournament_edit_prop(app, ws, msg) { 'annoncement_include_event', 'annoncement_include_round', 'annoncement_include_matchnumber', 'preparation_meetingpoint_enabled', 'preparation_tabletoperator_setup_enabled', 'call_preparation_matches_automatically_enabled', 'call_next_possible_scheduled_match_in_preparation', + 'preparation_successor_rally_count', + 'preparation_call_time_limit_before_scheduled_enabled', + 'preparation_call_time_limit_before_scheduled_minutes', + 'preparation_call_block_ahead_limit_enabled', + 'preparation_call_block_ahead_limit', + 'preparation_call_time_ahead_of_frontier_enabled', + 'preparation_call_time_ahead_of_frontier_minutes', + 'preparation_call_matches_ahead_of_frontier_enabled', + 'preparation_call_matches_ahead_of_frontier_limit', + 'preparation_call_player_pause_expired_enabled', + 'preparation_call_technical_officials_available_enabled', + 'call_on_court_time_limit_before_scheduled_enabled', + 'call_on_court_time_limit_before_scheduled_minutes', + 'call_on_court_only_preparation_enabled', + 'call_on_court_only_preparation_minutes', + 'call_on_court_block_ahead_limit_enabled', + 'call_on_court_block_ahead_limit', + 'call_on_court_time_ahead_of_frontier_enabled', + 'call_on_court_time_ahead_of_frontier_minutes', + 'call_on_court_matches_ahead_of_frontier_enabled', + 'call_on_court_matches_ahead_of_frontier_limit', + 'call_on_court_participant_readiness_mode', + 'call_on_court_player_pause_expired_enabled', + 'call_on_court_technical_officials_mode', + 'call_on_court_require_official_space_enabled', + 'official_rotation_mode', + 'technical_official_auto_assignment_mode', + 'technical_official_break_after_assignment_seconds', 'logo_background_color', 'logo_foreground_color', ]); @@ -174,6 +256,21 @@ function handle_tournament_edit_prop(app, ws, msg) { if (/^ticker_/.test(field)) { ticker_manager.reconfigure(app, t); } + if (field === 'automation_enabled' && t[field] === true) { + const match_utils = require('./match_utils'); + match_utils.queue_auto_assign_technical_officials_when_available(app, key); + match_utils.queue_auto_execute_preparation_selections(app, key, (selectionErr) => { + if (selectionErr) { + console.warn('[bts] failed to resume preparation automation', selectionErr && (selectionErr.stack || selectionErr.message || String(selectionErr))); + return; + } + match_utils.auto_call_matches_on_free_courts(app, key, (callErr) => { + if (callErr) { + console.warn('[bts] failed to resume on-court automation', callErr && (callErr.stack || callErr.message || String(callErr))); + } + }); + }); + } notify_change(app, key, 'prop_changed', { field, value: t[field] }); if (!tournament.displaysettings_general || (field === 'displaysettings_general' && tournament.displaysettings_general != t.displaysettings_general)){ @@ -282,6 +379,9 @@ function handle_courts_add(app, ws, msg) { _id: tournament_key + '_' + num, tournament_key, num, + is_active: true, + has_umpire: true, + has_service_judge: true, }; }); app.db.courts.insert(added_courts, function(err) { @@ -316,12 +416,14 @@ function handle_court_edit(app, ws, msg) { return; } const is_active = (msg.is_active != undefined ? msg.is_active : court.is_active); - app.db.courts.update(query, { $set: {is_active} }, {}, (err) => { + const has_umpire = (msg.has_umpire != undefined ? msg.has_umpire : (court.has_umpire != undefined ? court.has_umpire : true)); + const has_service_judge = (msg.has_service_judge != undefined ? msg.has_service_judge : (court.has_service_judge != undefined ? court.has_service_judge : true)); + app.db.courts.update(query, { $set: {is_active, has_umpire, has_service_judge} }, {}, (err) => { if(err) { ws.respond(msg, err); return; } - notify_change(app, msg.tournament_key, 'court_changed', {court_id, is_active}); + notify_change(app, msg.tournament_key, 'court_changed', {court_id, is_active, has_umpire, has_service_judge, match_id: court.match_id ?? null}); ws.respond(msg); return; }); @@ -490,6 +592,65 @@ function handle_tournament_get(app, ws, msg) { }); } +async function async_handle_preparation_selection_get(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key'])) { + return; + } + + const selections = await match_automation.fetch_all_location_preparation_selections(app, msg.tournament_key); + return ws.respond(msg, null, { + selections: selections.map((selection) => ({ + location_id: selection.location_id, + required_preparation_count: selection.required_preparation_count, + current_preparation_count: selection.current_preparation_count, + missing_preparation_count: selection.missing_preparation_count, + candidate_match_nums: selection.candidates.map((match) => match?.setup?.match_num).filter((num) => num != null), + selected_match_nums: selection.selected_matches.map((match) => match?.setup?.match_num).filter((num) => num != null), + })), + }); +} + +async function async_handle_preparation_selection_execute(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'location_id'])) { + return; + } + + const match_utils = require('./match_utils'); + try { + const called_matches = await update_queue.instance().execute(update_queue.named('preparation_selection_execute', async () => { + const tournament = await app.db.tournaments.findOne_async({ key: msg.tournament_key }); + if (!tournament) { + throw new Error('Cannot find tournament ' + msg.tournament_key); + } + + const selection = await match_automation.fetch_location_preparation_selection(app, msg.tournament_key, msg.location_id); + const called_matches = []; + + for (const match of selection.selected_matches) { + await new Promise((resolve, reject) => { + match_utils.call_match_in_preparation(app, tournament, match, msg.location_id, (err) => { + if (err) return reject(err); + called_matches.push({ + _id: match._id, + match_num: match?.setup?.match_num, + }); + resolve(null); + }); + }); + } + + return called_matches; + })); + + return ws.respond(msg, null, { + location_id: msg.location_id, + called_matches, + }); + } catch (err) { + return ws.respond(msg, err); + } +} + function handle_create_tournament(app, ws, msg) { if (! msg.key) { return ws.respond(msg, {message: 'Missing key'}); @@ -1004,6 +1165,8 @@ function _official_wait_set_obj(wait_field, ts) { inactive_list: null, service_judge_pause: null, umpire_pause: null, + service_judge_manual_pause: null, + umpire_manual_pause: null, service_judge_wait: null, umpire_wait: null, service_judge_on_court: null, @@ -1015,6 +1178,20 @@ function _official_wait_set_obj(wait_field, ts) { return setObj; } +function _official_list_target_ts(to_list, base_ts, tournament) { + return base_ts; +} + +function _official_list_target_field(to_list) { + if (to_list === 'umpire_pause') { + return 'umpire_manual_pause'; + } + if (to_list === 'service_judge_pause') { + return 'service_judge_manual_pause'; + } + return to_list; +} + function _apply_wait_releases(app, tournament_key, releases, start_ts, cb) { if (!releases.length) { cb(null, []); @@ -1094,6 +1271,8 @@ function _apply_match_edit_official_state_changes(app, tournament_key, old_setup inactive_list: null, service_judge_pause: null, umpire_pause: null, + service_judge_manual_pause: null, + umpire_manual_pause: null, service_judge_wait: null, umpire_wait: null, service_judge_on_court: null, @@ -1159,7 +1338,7 @@ function handle_match_preparation_call(app, ws, msg) { match_utils.call_match_in_preparation(app, tournament, msg.match, msg.location_id, (err) => { ws.respond(msg, err); return; - }); + }, { force: true }); }); } @@ -1202,6 +1381,14 @@ function handle_match_player_check_in (app, ws, msg) { if (err) { return reject(err); } + console.log('[bts] auto_call_trace:player_check_in_updated', { + ts: Date.now(), + tournament_key: msg.tournament_key, + match_id: msg.match_id, + player_id: msg.player_id, + checked_in: !!msg.checked_in, + }); + trigger_auto_call_after_readiness_change(app, msg.tournament_key); resolve(); }); }); @@ -1209,6 +1396,34 @@ function handle_match_player_check_in (app, ws, msg) { }))).then(() => ws.respond(msg)).catch((err) => ws.respond(msg, err)); } +function trigger_auto_call_after_readiness_change(app, tournament_key) { + const match_utils = require('./match_utils'); + console.log('[bts] auto_call_trace:readiness_trigger_start', { + ts: Date.now(), + tournament_key, + }); + match_utils.queue_auto_execute_preparation_selections(app, tournament_key, (selectionErr) => { + if (selectionErr) { + console.warn('[bts] failed to auto select preparation matches after readiness change', selectionErr && (selectionErr.stack || selectionErr.message || String(selectionErr))); + return; + } + console.log('[bts] auto_call_trace:readiness_trigger_after_preparation_selection', { + ts: Date.now(), + tournament_key, + }); + match_utils.auto_call_matches_on_free_courts(app, tournament_key, (callErr) => { + if (callErr) { + console.warn('[bts] failed to auto call matches on free courts after readiness change', callErr && (callErr.stack || callErr.message || String(callErr))); + return; + } + console.log('[bts] auto_call_trace:readiness_trigger_after_auto_call', { + ts: Date.now(), + tournament_key, + }); + }); + }); +} + function handle_match_participant_check_in(app, ws, msg) { const match_utils = require('./match_utils'); @@ -1217,41 +1432,83 @@ function handle_match_participant_check_in(app, ws, msg) { } update_queue.instance().execute(update_queue.named('handle_match_participant_check_in', () => new Promise((resolve, reject) => { - app.db.matches.findOne({ tournament_key: msg.tournament_key, _id: msg.match_id }, async (err, match) => { - if (err) { - return reject(err); - } - if (!match || !match.setup) { - return reject(new Error('Match not found')); + app.db.tournaments.findOne({ key: msg.tournament_key }, (tournament_err, tournament) => { + if (tournament_err) { + return reject(tournament_err); } + app.db.matches.findOne({ tournament_key: msg.tournament_key, _id: msg.match_id }, async (err, match) => { + if (err) { + return reject(err); + } + if (!match || !match.setup) { + return reject(new Error('Match not found')); + } - let participant_found = false; - const checked_in = !!msg.checked_in; + let participant_found = false; + const checked_in = !!msg.checked_in; - if (msg.role === 'umpire' || msg.role === 'service_judge') { - const participant = match.setup[msg.role]; - if (participant && participant.btp_id == msg.participant_id) { - participant.checked_in = checked_in; - participant_found = true; - } - } else if (msg.role === 'tabletoperator' && Array.isArray(match.setup.tabletoperators)) { - match.setup.tabletoperators.forEach((participant) => { - if (participant.btp_id == msg.participant_id) { + if (msg.role === 'umpire' || msg.role === 'service_judge') { + if (tournament?.btp_settings?.check_in_per_match === false) { + return resolve(); + } + const participant = match.setup[msg.role]; + if (participant && participant.btp_id == msg.participant_id) { participant.checked_in = checked_in; participant_found = true; } - }); - } - - if (!participant_found) { - return reject(new Error('Participant not found in match')); - } + } else if (msg.role === 'tabletoperator' && Array.isArray(match.setup.tabletoperators)) { + match.setup.tabletoperators.forEach((participant) => { + if (participant.btp_id == msg.participant_id) { + participant.checked_in = checked_in; + participant_found = true; + } + }); + } - match_utils.match_update(app, match, undefined, (update_err) => { - if (update_err) { - return reject(update_err); + if (!participant_found) { + return reject(new Error('Participant not found in match')); } - resolve(); + + match_utils.match_update(app, match, undefined, (update_err) => { + if (update_err) { + return reject(update_err); + } + console.log('[bts] auto_call_trace:participant_check_in_updated', { + ts: Date.now(), + tournament_key: msg.tournament_key, + match_id: msg.match_id, + role: msg.role, + participant_id: msg.participant_id, + checked_in, + }); + if ((msg.role === 'umpire' || msg.role === 'service_judge') && tournament?.btp_settings?.check_in_per_match !== false) { + const participant = match.setup[msg.role]; + if (!participant) { + trigger_auto_call_after_readiness_change(app, msg.tournament_key); + return resolve(); + } + const official_query = participant._id + ? { tournament_key: msg.tournament_key, _id: participant._id } + : { tournament_key: msg.tournament_key, btp_id: msg.participant_id }; + return app.db.umpires.update( + official_query, + { $set: { checked_in } }, + { returnUpdatedDocs: true }, + (official_err, numAffected, updated_official) => { + if (official_err) { + return reject(official_err); + } + if (numAffected > 0 && updated_official) { + notify_change(app, msg.tournament_key, 'umpire_updated', updated_official); + } + trigger_auto_call_after_readiness_change(app, msg.tournament_key); + resolve(); + } + ); + } + trigger_auto_call_after_readiness_change(app, msg.tournament_key); + resolve(); + }); }); }); }))).then(() => ws.respond(msg)).catch((err) => ws.respond(msg, err)); @@ -1412,6 +1669,7 @@ function handle_second_preparation_call_team_one(app, ws, msg) { } function handle_official_list_move(app, ws, msg) { + const match_utils = require('./match_utils'); if (!_require_msg(ws, msg, [ 'tournament_key', 'official_id', @@ -1433,15 +1691,19 @@ function handle_official_list_move(app, ws, msg) { to_list } = msg; - if (Array.isArray(ordered_official_ids) && ordered_official_ids.length > 0) { - const unique_ordered_ids = [...new Set(ordered_official_ids.filter(Boolean))]; - if (!unique_ordered_ids.includes(official_id)) { - unique_ordered_ids.push(official_id); - } - return app.db.umpires.find({ - tournament_key, - _id: { $in: unique_ordered_ids } - }, function(err, docs) { + app.db.tournaments.findOne({ key: tournament_key }, function (tournament_err, tournament) { + if (tournament_err) return cerror.ws(ws, tournament_err); + if (!tournament) return cerror.ws(ws, new Error('tournament not found')); + + if (Array.isArray(ordered_official_ids) && ordered_official_ids.length > 0) { + const unique_ordered_ids = [...new Set(ordered_official_ids.filter(Boolean))]; + if (!unique_ordered_ids.includes(official_id)) { + unique_ordered_ids.push(official_id); + } + return app.db.umpires.find({ + tournament_key, + _id: { $in: unique_ordered_ids } + }, function(err, docs) { if (err) return cerror.ws(ws, err); const currentUmpire = docs.find((u) => u._id === official_id); if (!currentUmpire) { @@ -1456,6 +1718,8 @@ function handle_official_list_move(app, ws, msg) { setObj['inactive_list'] = null; setObj['service_judge_pause'] = null; setObj['umpire_pause'] = null; + setObj['service_judge_manual_pause'] = null; + setObj['umpire_manual_pause'] = null; setObj['service_judge_wait'] = null; setObj['umpire_wait'] = null; setObj['service_judge_on_court'] = null; @@ -1463,7 +1727,9 @@ function handle_official_list_move(app, ws, msg) { setObj['is_planed_as_service_judge'] = false; setObj['is_planed_as_umpire'] = false; } - setObj[to_list] = now + index; + setObj[_official_list_target_field(to_list)] = _official_list_target_ts(to_list, now + index, tournament); + const updated_official = { ...(docs.find((u) => u._id === id) || {}), ...setObj }; + setObj.checked_in = match_utils.get_effective_technical_official_checked_in(updated_official, tournament); return { _id: id, setObj }; }); @@ -1490,41 +1756,41 @@ function handle_official_list_move(app, ws, msg) { official_id, from_list, to_list, - new_ts: now + unique_ordered_ids.indexOf(official_id), + new_ts: _official_list_target_ts(to_list, now + unique_ordered_ids.indexOf(official_id), tournament), }); ws.respond(msg); }); } ); }); - }); - } + }); + } - // btp_id sicher normalisieren - const prevId = (prev_btp_id == null) ? null : Number(prev_btp_id); - const nextId = (next_btp_id == null) ? null : Number(next_btp_id); + // btp_id sicher normalisieren + const prevId = (prev_btp_id == null) ? null : Number(prev_btp_id); + const nextId = (next_btp_id == null) ? null : Number(next_btp_id); - const neighborOfficialIds = []; - if (prev_official_id) neighborOfficialIds.push(prev_official_id); - if (next_official_id) neighborOfficialIds.push(next_official_id); + const neighborOfficialIds = []; + if (prev_official_id) neighborOfficialIds.push(prev_official_id); + if (next_official_id) neighborOfficialIds.push(next_official_id); - const neighborBtpIds = []; - if (Number.isFinite(prevId)) neighborBtpIds.push(prevId); - if (Number.isFinite(nextId)) neighborBtpIds.push(nextId); + const neighborBtpIds = []; + if (Number.isFinite(prevId)) neighborBtpIds.push(prevId); + if (Number.isFinite(nextId)) neighborBtpIds.push(nextId); - // Query: current über _id, prev/next primär über _id, fallback über btp_id - const query = { - tournament_key, - $or: [{ _id: official_id }] - }; - if (neighborOfficialIds.length > 0) { - query.$or.push({ _id: { $in: neighborOfficialIds } }); - } - if (neighborBtpIds.length > 0) { - query.$or.push({ btp_id: { $in: neighborBtpIds } }); - } + // Query: current über _id, prev/next primär über _id, fallback über btp_id + const query = { + tournament_key, + $or: [{ _id: official_id }] + }; + if (neighborOfficialIds.length > 0) { + query.$or.push({ _id: { $in: neighborOfficialIds } }); + } + if (neighborBtpIds.length > 0) { + query.$or.push({ btp_id: { $in: neighborBtpIds } }); + } - app.db.umpires.find(query, function (err, docs) { + app.db.umpires.find(query, function (err, docs) { if (err) return cerror.ws(ws, err); let currentUmpire = null; @@ -1580,6 +1846,7 @@ function handle_official_list_move(app, ws, msg) { } else { newTS = (prevTS + nextTS) / 2; } + newTS = _official_list_target_ts(to_list, newTS, tournament); // --- Update vorbereiten --- // Spezifikation: @@ -1591,13 +1858,16 @@ function handle_official_list_move(app, ws, msg) { setObj['inactive_list'] = null; setObj['service_judge_pause'] = null; setObj['umpire_pause'] = null; + setObj['service_judge_manual_pause'] = null; + setObj['umpire_manual_pause'] = null; setObj['service_judge_wait'] = null; setObj['umpire_wait'] = null; setObj['service_judge_on_court'] = null; setObj['umpire_on_court'] = null; setObj['is_planed_as_service_judge'] = false; setObj['is_planed_as_umpire'] = false; - setObj[to_list] = newTS; + setObj[_official_list_target_field(to_list)] = newTS; + setObj.checked_in = match_utils.get_effective_technical_official_checked_in({ ...currentUmpire, ...setObj }, tournament); app.db.umpires.update( { _id: currentUmpire._id, tournament_key }, @@ -1618,12 +1888,14 @@ function handle_official_list_move(app, ws, msg) { to_list, new_ts: newTS, }); + notify_change(app, tournament_key, 'umpire_updated', updated); ws.respond(msg); } ); } ); + }); }); } @@ -1760,31 +2032,12 @@ function handle_official_roles_edit(app, ws, msg) { }); } -function handle_add_officials_to_match(app, ws, msg) { - // 1) Pflichtfelder prüfen - if (!_require_msg(ws, msg, ['tournament_key', 'match_id'])) { - return; - } - - const { tournament_key, match_id } = msg; - - function pack_official(u) { - return { - _id: u._id, - btp_id: u.btp_id, - name: u.name, - firstname: u.firstname, - surname: u.surname, - country: u.country, - is_umpire: !!u.is_umpire, - is_service_judge: !!u.is_service_judge, - checked_in: false - }; - } - - update_queue.instance().execute(update_queue.named('handle_add_officials_to_match', () => new Promise((resolve, reject) => { - // 2) Match laden und prüfen, ob schon ein Umpire gesetzt ist - app.db.matches.findOne({ _id: match_id, tournament_key }, function (err, match) { +function _assign_next_umpire_to_match(app, tournament_key, match_id, options = {}) { + const skip_btp_push = options && options.skip_btp_push === true; + return new Promise((resolve, reject) => { + app.db.tournaments.findOne({ key: tournament_key }, function (tournament_err, tournament) { + if (tournament_err) return reject(tournament_err); + app.db.matches.findOne({ _id: match_id, tournament_key }, function (err, match) { if (err) return reject(err); if (!match) { return reject( @@ -1799,8 +2052,20 @@ function handle_add_officials_to_match(app, ws, msg) { } const setup = match.setup; + if (setup.court_id) { + return app.db.courts.findOne({ tournament_key, _id: setup.court_id }, function(courtErr, court) { + if (courtErr) return reject(courtErr); + if (court && court.has_umpire === false) { + return reject(new Error('Court has no space for an umpire')); + } + return continue_assign(); + }); + } + + return continue_assign(); + + function continue_assign() { - // 3) Ältesten Umpire suchen app.db.umpires .find({ tournament_key, umpire_wait: { $ne: null } }) .sort({ umpire_wait: 1 }) @@ -1813,11 +2078,12 @@ function handle_add_officials_to_match(app, ws, msg) { const umpire = umps[0]; - // 4) Atomar reservieren (Race-Condition-sicher) app.db.umpires.update( { _id: umpire._id, tournament_key, umpire_wait: { $ne: null } }, { $set: { umpire_wait: null, - is_planed_as_umpire: true } }, + service_judge_wait: null, + is_planed_as_umpire: true, + is_planed_as_service_judge: false } }, {}, function (err3, affected1) { if (err3) return reject(err3); @@ -1825,8 +2091,7 @@ function handle_add_officials_to_match(app, ws, msg) { return reject(new Error('Umpire was already taken by another assignment')); } - // 5) Match.setup updaten - setup.umpire = pack_official(umpire); + setup.umpire = _pack_official_for_match(umpire, options.tournament || tournament || null); app.db.matches.update( { _id: match_id, tournament_key, 'setup.umpire': { $exists: false } }, @@ -1836,21 +2101,24 @@ function handle_add_officials_to_match(app, ws, msg) { if (err4 || affectedMatch === 0) { app.db.umpires.update( { _id: umpire._id, tournament_key }, - { $set: { umpire_wait: Date.now()/10, - is_planed_as_umpire: false + { $set: { umpire_wait: umpire.is_umpire ? Date.now()/10 : null, + service_judge_wait: umpire.is_service_judge ? Date.now()/10 : null, + is_planed_as_umpire: false, + is_planed_as_service_judge: false } } ); return reject(err4 || new Error('Match changed during official assignment')); } - // 6) Broadcast app.db.matches.findOne( { _id: match_id, tournament_key }, function (err5, updatedMatch) { if (err5) return reject(err5); - notify_change(app, tournament_key, 'match_edit', {match__id: msg.match_id, match: updatedMatch}); - btp_manager.update_score(app, updatedMatch); + notify_change(app, tournament_key, 'match_edit', {match__id: match_id, match: updatedMatch}); + if (!skip_btp_push) { + btp_manager.update_score(app, updatedMatch); + } app.db.umpires.find( { tournament_key, _id: umpire._id }, @@ -1879,32 +2147,32 @@ function handle_add_officials_to_match(app, ws, msg) { } ); }); + } }); - }))).then(() => ws.respond(msg)).catch((err) => ws.respond(msg, err)); + }); + }); } -function handle_add_service_judge_to_match(app, ws, msg) { +function assign_next_umpire_to_match(app, tournament_key, match_id) { + return update_queue.instance().execute(update_queue.named('handle_add_officials_to_match', () => _assign_next_umpire_to_match(app, tournament_key, match_id))); +} + +function handle_add_officials_to_match(app, ws, msg) { if (!_require_msg(ws, msg, ['tournament_key', 'match_id'])) { return; } const { tournament_key, match_id } = msg; + assign_next_umpire_to_match(app, tournament_key, match_id) + .then(() => ws.respond(msg)) + .catch((err) => ws.respond(msg, err)); +} - function pack_official(u) { - return { - _id: u._id, - btp_id: u.btp_id, - name: u.name, - firstname: u.firstname, - surname: u.surname, - country: u.country, - is_umpire: !!u.is_umpire, - is_service_judge: !!u.is_service_judge, - checked_in: false - }; - } - - update_queue.instance().execute(update_queue.named('handle_add_service_judge_to_match', () => new Promise((resolve, reject) => { +function _assign_next_service_judge_to_match(app, tournament_key, match_id, options = {}) { + const skip_btp_push = options && options.skip_btp_push === true; + return new Promise((resolve, reject) => { + app.db.tournaments.findOne({ key: tournament_key }, function (tournament_err, tournament) { + if (tournament_err) return reject(tournament_err); app.db.matches.findOne({ _id: match_id, tournament_key }, function (err, match) { if (err) return reject(err); if (!match) { @@ -1921,6 +2189,19 @@ function handle_add_service_judge_to_match(app, ws, msg) { } const setup = match.setup; + if (setup.court_id) { + return app.db.courts.findOne({ tournament_key, _id: setup.court_id }, function(courtErr, court) { + if (courtErr) return reject(courtErr); + if (court && court.has_service_judge === false) { + return reject(new Error('Court has no space for a service judge')); + } + return continue_assign(); + }); + } + + return continue_assign(); + + function continue_assign() { app.db.umpires .find({ tournament_key, service_judge_wait: { $ne: null } }) @@ -1936,7 +2217,7 @@ function handle_add_service_judge_to_match(app, ws, msg) { app.db.umpires.update( { _id: service_judge._id, tournament_key, service_judge_wait: { $ne: null } }, - { $set: { service_judge_wait: null, is_planed_as_service_judge: true } }, + { $set: { service_judge_wait: null, umpire_wait: null, is_planed_as_service_judge: true, is_planed_as_umpire: false } }, {}, function (err3, affected) { if (err3) return reject(err3); @@ -1944,7 +2225,7 @@ function handle_add_service_judge_to_match(app, ws, msg) { return reject(new Error('Service judge was already taken')); } - setup.service_judge = pack_official(service_judge); + setup.service_judge = _pack_official_for_match(service_judge, options.tournament || tournament || null); app.db.matches.update( { _id: match_id, tournament_key, 'setup.umpire': { $exists: true }, 'setup.service_judge': { $exists: false } }, @@ -1954,7 +2235,12 @@ function handle_add_service_judge_to_match(app, ws, msg) { if (err4 || affectedMatch === 0) { app.db.umpires.update( { _id: service_judge._id, tournament_key }, - { $set: { service_judge_wait: Date.now() / 10, is_planed_as_service_judge: false } } + { $set: { + service_judge_wait: service_judge.is_service_judge ? Date.now() / 10 : null, + umpire_wait: service_judge.is_umpire ? Date.now() / 10 : null, + is_planed_as_service_judge: false, + is_planed_as_umpire: false + } } ); return reject(err4 || new Error('Match changed during service judge assignment')); } @@ -1964,8 +2250,10 @@ function handle_add_service_judge_to_match(app, ws, msg) { function (err5, updatedMatch) { if (err5) return reject(err5); - notify_change(app, tournament_key, 'match_edit', { match__id: msg.match_id, match: updatedMatch }); - btp_manager.update_score(app, updatedMatch); + notify_change(app, tournament_key, 'match_edit', { match__id: match_id, match: updatedMatch }); + if (!skip_btp_push) { + btp_manager.update_score(app, updatedMatch); + } app.db.umpires.findOne( { tournament_key, _id: service_judge._id }, @@ -1992,11 +2280,29 @@ function handle_add_service_judge_to_match(app, ws, msg) { } ); }); + } }); - }))).then(() => ws.respond(msg)).catch((err) => ws.respond(msg, err)); + }); + }); } -function _pack_official_for_match(u) { +function assign_next_service_judge_to_match(app, tournament_key, match_id) { + return update_queue.instance().execute(update_queue.named('handle_add_service_judge_to_match', () => _assign_next_service_judge_to_match(app, tournament_key, match_id))); +} + +function handle_add_service_judge_to_match(app, ws, msg) { + if (!_require_msg(ws, msg, ['tournament_key', 'match_id'])) { + return; + } + + const { tournament_key, match_id } = msg; + assign_next_service_judge_to_match(app, tournament_key, match_id) + .then(() => ws.respond(msg)) + .catch((err) => ws.respond(msg, err)); +} + +function _pack_official_for_match(u, tournament = null) { + const match_utils = require('./match_utils'); return { _id: u._id, btp_id: u.btp_id, @@ -2006,7 +2312,9 @@ function _pack_official_for_match(u) { country: u.country, is_umpire: !!u.is_umpire, is_service_judge: !!u.is_service_judge, - checked_in: false + umpire_wait: u.umpire_wait ?? null, + service_judge_wait: u.service_judge_wait ?? null, + checked_in: match_utils.get_effective_technical_official_checked_in(u, tournament) }; } @@ -2029,6 +2337,8 @@ function handle_assign_official_to_preparation_match(app, ws, msg) { const role_flag = role === 'umpire' ? 'is_planed_as_umpire' : 'is_planed_as_service_judge'; update_queue.instance().execute(update_queue.named('handle_assign_official_to_preparation_match', () => new Promise((resolve, reject) => { + app.db.tournaments.findOne({ key: tournament_key }, function (tournament_err, tournament) { + if (tournament_err) return reject(tournament_err); app.db.matches.find({ tournament_key, _id: { $in: [...new Set([match_id, source_match_id].filter(Boolean))] } }, function (err, matches) { if (err) return reject(err); const match = matches.find((m) => m._id === match_id); @@ -2071,14 +2381,16 @@ function handle_assign_official_to_preparation_match(app, ws, msg) { } } } - target_setup[role] = _pack_official_for_match(official); + target_setup[role] = _pack_official_for_match(official, tournament || null); delete target_setup[role === 'umpire' ? 'suppressed_umpire_btp_id' : 'suppressed_service_judge_btp_id']; - const officialSetObj = { - inactive_list: null, - service_judge_pause: null, - umpire_pause: null, - service_judge_wait: null, + const officialSetObj = { + inactive_list: null, + service_judge_pause: null, + umpire_pause: null, + service_judge_manual_pause: null, + umpire_manual_pause: null, + service_judge_wait: null, umpire_wait: null, service_judge_on_court: null, umpire_on_court: null, @@ -2162,6 +2474,7 @@ function handle_assign_official_to_preparation_match(app, ws, msg) { ); }); }); + }); }))).then(() => ws.respond(msg)).catch((err) => cerror.ws(ws, err)); } @@ -2184,6 +2497,8 @@ function handle_assign_official_to_match(app, ws, msg) { const role_flag = role === 'umpire' ? 'is_planed_as_umpire' : 'is_planed_as_service_judge'; update_queue.instance().execute(update_queue.named('handle_assign_official_to_match', () => new Promise((resolve, reject) => { + app.db.tournaments.findOne({ key: tournament_key }, function (tournament_err, tournament) { + if (tournament_err) return reject(tournament_err); app.db.matches.find({ tournament_key, _id: { $in: [...new Set([match_id, source_match_id].filter(Boolean))] } }, function (err, matches) { if (err) return reject(err); const match = matches.find((m) => m._id === match_id); @@ -2227,14 +2542,16 @@ function handle_assign_official_to_match(app, ws, msg) { } } } - target_setup[role] = _pack_official_for_match(official); + target_setup[role] = _pack_official_for_match(official, tournament || null); delete target_setup[role === 'umpire' ? 'suppressed_umpire_btp_id' : 'suppressed_service_judge_btp_id']; - const officialSetObj = { - inactive_list: null, - service_judge_pause: null, - umpire_pause: null, - service_judge_wait: null, + const officialSetObj = { + inactive_list: null, + service_judge_pause: null, + umpire_pause: null, + service_judge_manual_pause: null, + umpire_manual_pause: null, + service_judge_wait: null, umpire_wait: null, service_judge_on_court: null, umpire_on_court: null, @@ -2318,6 +2635,7 @@ function handle_assign_official_to_match(app, ws, msg) { ); }); }); + }); }))).then(() => ws.respond(msg)).catch((err) => cerror.ws(ws, err)); } @@ -2347,11 +2665,13 @@ function handle_remove_official_from_preparation_match(app, ws, msg) { const setup = structuredClone(match.setup || {}); const dependent_releases = _remove_official_from_setup(setup, role); - const baseSetObj = { - inactive_list: null, - service_judge_pause: null, - umpire_pause: null, - service_judge_wait: null, + const baseSetObj = { + inactive_list: null, + service_judge_pause: null, + umpire_pause: null, + service_judge_manual_pause: null, + umpire_manual_pause: null, + service_judge_wait: null, umpire_wait: null, service_judge_on_court: null, umpire_on_court: null, @@ -2376,7 +2696,7 @@ function handle_remove_official_from_preparation_match(app, ws, msg) { const now = Date.now(); return async.eachSeries(unique_ordered_ids, (id, next) => { const setObj = (id === official_id) ? { ...baseSetObj } : {}; - setObj[to_list] = now + unique_ordered_ids.indexOf(id); + setObj[_official_list_target_field(to_list)] = _official_list_target_ts(to_list, now + unique_ordered_ids.indexOf(id), null); app.db.umpires.update( { _id: id, tournament_key }, { $set: setObj }, @@ -2390,7 +2710,7 @@ function handle_remove_official_from_preparation_match(app, ws, msg) { } const setObj = { ...baseSetObj }; const now = Date.now(); - setObj[to_list] = now; + setObj[_official_list_target_field(to_list)] = _official_list_target_ts(to_list, now, null); app.db.umpires.update( { _id: official_id, tournament_key }, { $set: setObj }, @@ -2460,11 +2780,13 @@ function handle_remove_official_from_match(app, ws, msg) { const setup = structuredClone(match.setup || {}); const dependent_releases = _remove_official_from_setup(setup, role); - const baseSetObj = { - inactive_list: null, - service_judge_pause: null, - umpire_pause: null, - service_judge_wait: null, + const baseSetObj = { + inactive_list: null, + service_judge_pause: null, + umpire_pause: null, + service_judge_manual_pause: null, + umpire_manual_pause: null, + service_judge_wait: null, umpire_wait: null, service_judge_on_court: null, umpire_on_court: null, @@ -2489,7 +2811,7 @@ function handle_remove_official_from_match(app, ws, msg) { const now = Date.now(); return async.eachSeries(unique_ordered_ids, (id, next) => { const setObj = (id === official_id) ? { ...baseSetObj } : {}; - setObj[to_list] = now + unique_ordered_ids.indexOf(id); + setObj[_official_list_target_field(to_list)] = _official_list_target_ts(to_list, now + unique_ordered_ids.indexOf(id), null); app.db.umpires.update( { _id: id, tournament_key }, { $set: setObj }, @@ -2503,7 +2825,7 @@ function handle_remove_official_from_match(app, ws, msg) { } const setObj = { ...baseSetObj }; const now = Date.now(); - setObj[to_list] = now; + setObj[_official_list_target_field(to_list)] = _official_list_target_ts(to_list, now, null); app.db.umpires.update( { _id: official_id, tournament_key }, { $set: setObj }, @@ -2759,6 +3081,15 @@ function notify_change(app, tournament_key, ctype, val) { _announcement_ts: Date.now(), }; } + if (ctype === 'match_preparation_call' && payload && typeof payload === 'object') { + console.log('[bts] debug:match_preparation_call_sent', { + match_id: payload.match__id || payload.match?._id || null, + announcement_ts: payload._announcement_ts || null, + state: payload.match?.setup?.state || null, + highlight: payload.match?.setup?.highlight || 0, + location_id: payload.match?.setup?.location_id || null, + }); + } for (const admin_ws of all_admins) { admin_ws.sendmsg({ type: 'change', @@ -2925,6 +3256,8 @@ module.exports = { handle_match_edit, handle_match_call_on_court, handle_match_preparation_call, + async_handle_preparation_selection_get, + async_handle_preparation_selection_execute, handle_match_player_check_in, handle_match_participant_check_in, handle_ticker_pushall, @@ -2936,6 +3269,10 @@ module.exports = { handle_official_roles_edit, handle_add_officials_to_match, handle_add_service_judge_to_match, + assign_next_umpire_to_match, + assign_next_service_judge_to_match, + _assign_next_umpire_to_match, + _assign_next_service_judge_to_match, handle_assign_official_to_match, handle_assign_official_to_preparation_match, handle_remove_official_from_match, diff --git a/bts/bts.js b/bts/bts.js index c34f139..c43d69f 100644 --- a/bts/bts.js +++ b/bts/bts.js @@ -15,6 +15,7 @@ const btp_manager = require('./btp_manager'); const bupws = require('./bupws'); const database = require('./database'); const http_api = require('./http_api'); +const match_utils = require('./match_utils'); const serror = require('./serror'); const shortcuts = require('./shortcuts'); const ticker_manager = require('./ticker_manager'); @@ -49,6 +50,7 @@ function main() { }, function (config, db, cb) { const app = create_app(config, db); + match_utils.start_technical_official_pause_manager(app); btp_manager.init(app, (err) => cb(err, app)); }, function(app, cb) { diff --git a/bts/bupws.js b/bts/bupws.js index 61bf948..7295c4d 100644 --- a/bts/bupws.js +++ b/bts/bupws.js @@ -12,6 +12,7 @@ const btp_manager = require('./btp_manager'); const btp_conn = require('./btp_conn'); const ticker_manager = require('./ticker_manager'); const update_queue = require('./update_queue'); +const match_automation = require('./match_automation'); const stournament = require('./stournament'); const all_panels = []; @@ -62,6 +63,32 @@ function notify_change_broadcast(app, tournament_key, ctype, val) { } } +function _clear_court_match_reference_after_finish(app, tournament_key, court_q, court, match_id, finish_confirmed, callback) { + if (!finish_confirmed || !court || court.match_id !== match_id) { + return callback(null, false); + } + app.db.courts.update( + court_q, + { $set: { match_id: null } }, + { returnUpdatedDocs: true }, + (err, _numAffected, updated_court) => { + if (err) { + return callback(err); + } + if (updated_court) { + admin.notify_change(app, tournament_key, 'court_changed', { + court_id: updated_court._id, + is_active: updated_court.is_active, + has_umpire: updated_court.has_umpire, + has_service_judge: updated_court.has_service_judge, + match_id: null, + }); + } + callback(null, !!updated_court); + } + ); +} + function notify_change_ws(app, ws, tournament_key, court_id, ctype, val) { if (ws == null) { notify_change(app, tournament_key, court_id, ctype, val); @@ -161,10 +188,34 @@ async function handle_score_update(app, ws, msg) { (async () => { let match = null; + let tournament = null; + let court = null; try { - match = await match_utils.fetch_match(app, tournament_key, match_id); + const fetch_tournament = new Promise((resolve, reject) => { + app.db.tournaments.findOne({ key: tournament_key }, (err, found_tournament) => { + if (err) { + return reject(err); + } + resolve(found_tournament); + }); + }); + const fetch_court = new Promise((resolve, reject) => { + app.db.courts.findOne({ tournament_key, _id: score_data.court_id }, (err, found_court) => { + if (err) { + return reject(err); + } + resolve(found_court); + }); + }); + [match, tournament, court] = await Promise.all([ + match_utils.fetch_match(app, tournament_key, match_id), + fetch_tournament, + fetch_court, + ]); } catch { match = null; + tournament = null; + court = null; } const finish_confirmed = score_data.finish_confirmed ? score_data.finish_confirmed : false; const allow_finished_confirmation = finish_confirmed && (score_data.team1_won !== undefined && score_data.team1_won !== null); @@ -172,6 +223,25 @@ async function handle_score_update(app, ws, msg) { send_error(ws, tournament_key, "Match not found or not on court actualy."); return finish(); } + if (!court) { + send_error(ws, tournament_key, "Court for score update not found."); + return finish(); + } + if (ws.court_id && score_data.court_id && ws.court_id !== score_data.court_id) { + send_error(ws, tournament_key, "Score update rejected: panel is assigned to a different court."); + return finish(); + } + if (match.setup && match.setup.court_id && score_data.court_id && match.setup.court_id !== score_data.court_id) { + send_error(ws, tournament_key, "Score update rejected: match is assigned to a different court."); + return finish(); + } + const expected_match_for_court = + court.match_id === match_id || + (!court.match_id && match.setup && match.setup.court_id === score_data.court_id && match.setup.now_on_court === true); + if (!expected_match_for_court) { + send_error(ws, tournament_key, "Score update rejected: stale panel state for this court."); + return finish(); + } const update = { network_score: score_data.network_score, @@ -182,6 +252,7 @@ async function handle_score_update(app, ws, msg) { duration_ms:score_data.duration_ms, end_ts:score_data.end_ts, 'setup.now_on_court': true, + 'setup.state': 'oncourt', }; const device_info = score_data.device; @@ -192,6 +263,7 @@ async function handle_score_update(app, ws, msg) { if (finish_confirmed) { update["setup.now_on_court"] = false; + update["setup.state"] = 'finished'; update.team1_won = score_data.team1_won; update.btp_winner = (update.team1_won === true) ? 1 : 2; update.btp_needsync = true; @@ -200,6 +272,21 @@ async function handle_score_update(app, ws, msg) { if (score_data.shuttle_count) { update.shuttle_count = score_data.shuttle_count; } + + const simulated_match = { + ...match, + network_score: update.network_score, + team1_won: update.team1_won, + setup: { + ...match.setup, + now_on_court: update['setup.now_on_court'], + state: update['setup.state'], + }, + }; + const preparation_successor_state = match_automation.calculate_preparation_successor_state(simulated_match, tournament); + update['setup.needs_preparation_successor'] = preparation_successor_state.needs_preparation_successor; + update['setup.needs_preparation_successor_ts'] = preparation_successor_state.needs_preparation_successor_ts; + const match_query = { _id: match_id, tournament_key, @@ -223,6 +310,8 @@ async function handle_score_update(app, ws, msg) { team1_won: update.team1_won, shuttle_count: update.shuttle_count, presses: updated_match.presses, + court_id: updated_match.setup && updated_match.setup.court_id, + now_on_court: updated_match.setup && updated_match.setup.now_on_court, }); } cb(null, updated_match); @@ -241,7 +330,7 @@ async function handle_score_update(app, ws, msg) { cb(null, updated_match); }, (updated_match, cb) => { - db.courts.findOne(court_q, (err, court) => cb(err, updated_match, court)); + cb(null, updated_match, court); }, (updated_match, court, cb) => { if (!court) { @@ -277,6 +366,7 @@ async function handle_score_update(app, ws, msg) { updated_match.network_score[0].length > 1 && (updated_match.network_score[0][0] > 0 || updated_match.network_score[0][1] > 0)) { updated_match.setup.highlight = 0; + match_utils.normalize_preparation_state(updated_match.setup); btp_manager.update_highlight(app, updated_match); } cb(null, updated_match, changed_court); @@ -289,17 +379,38 @@ async function handle_score_update(app, ws, msg) { } cb(null, updated_match, changed_court); }, + (updated_match, changed_court, cb) => { + _clear_court_match_reference_after_finish(app, tournament_key, court_q, court, match_id, finish_confirmed, (err) => { + if (err) { + return cb(err); + } + cb(null, updated_match, changed_court); + }); + }, (updated_match, changed_court, cb) => { if (!updated_match) { return cb(new Error('Cannot find match ' + JSON.stringify(updated_match))); } - if (finish_confirmed) { - match_utils.call_preparation_match_on_court(app, tournament_key, updated_match.setup.court_id) - .then(() => cb(null, updated_match, changed_court)) - .catch((err) => cb(err)); - return; + match_utils.auto_execute_preparation_selection_for_setup(app, tournament, updated_match.setup, (err) => { + if (err) { + return cb(err); + } + return cb(null, updated_match, changed_court); + }); + }, + (updated_match, changed_court, cb) => { + if (!finish_confirmed || !score_data.court_id) { + return cb(null, updated_match, changed_court); } - return cb(null, updated_match, changed_court); + match_utils.call_preparation_match_on_court(app, tournament_key, score_data.court_id) + .then(() => cb(null, updated_match, changed_court)) + .catch((err) => { + const message = err && (err.message || String(err)); + if (/No match found to call on court/.test(message)) { + return cb(null, updated_match, changed_court); + } + return cb(err); + }); }, (updated_match, changed_court, cb) => { if (!device_info) { @@ -671,12 +782,75 @@ function handle_command_done(app, ws, msg) { } function handle_score_change(app, tournament_key, court_id) { + console.log('[bts] auto_call_trace:bup_handle_score_change', { + ts: Date.now(), + tournament_key, + court_id: court_id || null, + all_matches_delivery: !!all_matches_delivery(), + }); matches_handler(app, null, tournament_key, court_id); if (all_matches_delivery()) { matches_handler(app, null, tournament_key, undefined); } } +function get_bup_match_priority(match, prefer_finished_first) { + if (!match || !match.setup) { + return 99; + } + if (prefer_finished_first) { + if (match.setup.state === 'finished') { + return 0; + } + if (match.setup.now_on_court === true) { + return 1; + } + if (match.setup.state === 'oncourt') { + return 2; + } + if (match.setup.state === 'blocked') { + return 3; + } + return 4; + } + if (match.setup.now_on_court === true) { + return 0; + } + if (match.setup.state === 'oncourt') { + return 1; + } + if (match.setup.state === 'blocked') { + return 2; + } + if (match.setup.state === 'finished') { + return 3; + } + return 4; +} + +function cmp_bup_matches(a, b, prefer_finished_first) { + const priority_diff = get_bup_match_priority(a, prefer_finished_first) - get_bup_match_priority(b, prefer_finished_first); + if (priority_diff !== 0) { + return priority_diff; + } + + const a_called = (a && a.setup && a.setup.called_timestamp) || 0; + const b_called = (b && b.setup && b.setup.called_timestamp) || 0; + if (a_called !== b_called) { + return b_called - a_called; + } + + const a_end = a && a.end_ts ? a.end_ts : 0; + const b_end = b && b.end_ts ? b.end_ts : 0; + if (a_end !== b_end) { + return b_end - a_end; + } + + const a_id = (a && a.setup && a.setup.match_id) || ''; + const b_id = (b && b.setup && b.setup.match_id) || ''; + return a_id.localeCompare(b_id); +} + function matches_handler(app, ws, tournament_key, court_id) { const now = Date.now(); const show_still = now - 60000; @@ -735,6 +909,7 @@ function matches_handler(app, ws, tournament_key, court_id) { matches = matches.filter(m => m.setup.now_on_court); } matches = matches.filter(m => m.setup.state == 'oncourt' || m.setup.state == 'finished' || m.setup.state == 'blocked'); + matches.sort((a, b) => cmp_bup_matches(a, b, !!court_id)); db_courts.sort(utils.cmp_key('num')); const courts = db_courts.map(function (dc) { @@ -755,6 +930,18 @@ function matches_handler(app, ws, tournament_key, court_id) { const event = create_event_representation(tournament); event.matches = matches; event.courts = courts; + console.log('[bts] auto_call_trace:bup_score_update_payload', { + ts: Date.now(), + tournament_key, + court_id: court_id || null, + match_states: matches.map((match) => ({ + match_id: match && match.setup && match.setup.match_id, + state: match && match.setup && match.setup.state, + now_on_court: match && match.setup && match.setup.now_on_court, + called_timestamp: match && match.setup && match.setup.called_timestamp, + end_ts: match && match.end_ts, + })), + }); const reply = { status: 'ok', event, @@ -968,4 +1155,5 @@ module.exports = { add_display_status, create_match_representation, create_event_representation, + _clear_court_match_reference_after_finish, }; diff --git a/bts/match_automation.js b/bts/match_automation.js new file mode 100644 index 0000000..793fefa --- /dev/null +++ b/bts/match_automation.js @@ -0,0 +1,1397 @@ +'use strict'; + +const match_scoring = require('../static/js/match_scoring'); +const DEFAULT_PREPARATION_SUCCESSOR_RALLY_COUNT = 11; +const DEFAULT_NOW_FN = () => Date.now(); + +function fallback_scoring_format() { + return { + numSets: 3, + set_points: { + end_points: 21, + max_points: 30, + }, + last_set_points: { + end_points: 21, + max_points: 30, + }, + }; +} + +function normalize_scoring_format(scoring_format) { + const format = scoring_format || fallback_scoring_format(); + const fallback = fallback_scoring_format(); + return { + numSets: Number.isFinite(format.numSets) && format.numSets > 0 ? format.numSets : fallback.numSets, + set_points: { + end_points: Number.isFinite(format?.set_points?.end_points) ? format.set_points.end_points : fallback.set_points.end_points, + max_points: Number.isFinite(format?.set_points?.max_points) ? format.set_points.max_points : fallback.set_points.max_points, + }, + last_set_points: { + end_points: Number.isFinite(format?.last_set_points?.end_points) ? format.last_set_points.end_points : fallback.last_set_points.end_points, + max_points: Number.isFinite(format?.last_set_points?.max_points) ? format.last_set_points.max_points : fallback.last_set_points.max_points, + }, + }; +} + +function sets_needed_to_win(scoring_format) { + const format = normalize_scoring_format(scoring_format); + return Math.floor(format.numSets / 2) + 1; +} + +function get_set_points(scoring_format, set_index) { + const format = normalize_scoring_format(scoring_format); + const use_last_set_points = set_index === (format.numSets - 1); + return use_last_set_points ? format.last_set_points : format.set_points; +} + +function normalize_score_pair(score_pair) { + if (!Array.isArray(score_pair) || score_pair.length < 2) { + return null; + } + const score_a = Number(score_pair[0]); + const score_b = Number(score_pair[1]); + if (!Number.isFinite(score_a) || !Number.isFinite(score_b)) { + return null; + } + return [score_a, score_b]; +} + +function get_current_match_state(match, scoring_format) { + if (!match || !Array.isArray(match.network_score) || match.network_score.length === 0) { + return null; + } + + let wins_a = 0; + let wins_b = 0; + let current_set_score = null; + let current_set_index = 0; + + for (let idx = 0; idx < match.network_score.length; idx++) { + const score = normalize_score_pair(match.network_score[idx]); + if (!score) { + return null; + } + const set_points = get_set_points(scoring_format, idx); + if (match_scoring.is_set_over(score[0], score[1], set_points)) { + if (score[0] > score[1]) { + wins_a += 1; + } else if (score[1] > score[0]) { + wins_b += 1; + } else { + return null; + } + current_set_index = idx + 1; + current_set_score = [0, 0]; + continue; + } + + current_set_index = idx; + current_set_score = score; + return { + wins: [wins_a, wins_b], + current_set_index, + current_set_score, + }; + } + + if (!current_set_score) { + current_set_score = [0, 0]; + } + + return { + wins: [wins_a, wins_b], + current_set_index, + current_set_score, + }; +} + +function get_current_leader(score_pair) { + if (!score_pair) { + return null; + } + if (score_pair[0] > score_pair[1]) { + return 0; + } + if (score_pair[1] > score_pair[0]) { + return 1; + } + return null; +} + +function rallies_needed_for_set_win(score_pair, leader, set_points) { + for (let added = 0; added <= 100; added++) { + const score = [...score_pair]; + score[leader] += added; + if (match_scoring.is_set_over(score[0], score[1], set_points) && score[leader] > score[1 - leader]) { + return added; + } + } + return null; +} + +function can_leader_finish_match_within_rallies(match, scoring_format, rally_count) { + if (!match || !match.setup || !match.setup.now_on_court) { + return false; + } + if (match.team1_won !== undefined && match.team1_won !== null) { + return false; + } + if (!Number.isFinite(rally_count) || rally_count <= 0) { + return false; + } + + const state = get_current_match_state(match, scoring_format); + if (!state) { + return false; + } + + const leader = get_current_leader(state.current_set_score); + if (leader === null) { + return false; + } + + let remaining_rallies = rally_count; + let wins = [...state.wins]; + let current_set_index = state.current_set_index; + let current_set_score = [...state.current_set_score]; + const wins_needed = sets_needed_to_win(scoring_format); + + while (remaining_rallies > 0) { + if (wins[0] >= wins_needed || wins[1] >= wins_needed) { + return true; + } + + const set_points = get_set_points(scoring_format, current_set_index); + const rallies_needed = rallies_needed_for_set_win(current_set_score, leader, set_points); + if (!Number.isFinite(rallies_needed)) { + return false; + } + + if (remaining_rallies < rallies_needed) { + return false; + } + + current_set_score[leader] += rallies_needed; + remaining_rallies -= rallies_needed; + + if (!match_scoring.is_set_over(current_set_score[0], current_set_score[1], set_points)) { + return false; + } + + wins[leader] += 1; + if (wins[leader] >= wins_needed) { + return true; + } + + current_set_index += 1; + current_set_score = [0, 0]; + } + + return wins[0] >= wins_needed || wins[1] >= wins_needed; +} + +function calculate_location_preparation_need(tournament, location_id) { + const courts = Array.isArray(tournament?.courts) ? tournament.courts : []; + const matches = Array.isArray(tournament?.matches) ? tournament.matches : []; + const location_courts = courts.filter((court) => court && court.location_id === location_id); + const active_location_courts = location_courts.filter((court) => court && court.is_active === true); + const active_location_court_ids = new Set(active_location_courts.map((court) => court._id)); + const occupied_court_ids = new Set( + matches + .filter((match) => match && match.setup && match.setup.now_on_court === true && match.setup.court_id) + .map((match) => match.setup.court_id) + ); + + const successor_need_count = matches.filter((match) => { + const setup = match && match.setup; + if (!setup || !setup.now_on_court || !setup.needs_preparation_successor) { + return false; + } + return active_location_court_ids.has(setup.court_id); + }).length; + + const free_court_count = active_location_courts.filter((court) => { + return !occupied_court_ids.has(court._id); + }).length; + const active_court_count = active_location_courts.length; + const required_preparation_count = Math.min(active_court_count, successor_need_count + free_court_count); + + return { + location_id, + active_court_count, + successor_need_count, + free_court_count, + required_preparation_count, + }; +} + +function calculate_location_preparation_status(tournament, location_id) { + const need = calculate_location_preparation_need(tournament, location_id); + const matches = Array.isArray(tournament?.matches) ? tournament.matches : []; + const current_preparation_count = matches.filter((match) => { + const setup = match && match.setup; + return !!setup && setup.state === 'preparation' && setup.location_id === location_id; + }).length; + + return { + ...need, + current_preparation_count, + missing_preparation_count: Math.max(0, need.required_preparation_count - current_preparation_count), + }; +} + +function cmp_values(a, b) { + if (a == null && b == null) return 0; + if (a == null) return -1; + if (b == null) return 1; + if (a < b) return -1; + if (a > b) return 1; + return 0; +} + +function cmp_scheduled_match_order(m1, m2) { + const setup1 = m1?.setup || {}; + const setup2 = m2?.setup || {}; + const time_str1 = setup1.scheduled_time_str; + const time_str2 = setup2.scheduled_time_str; + + if (time_str1 && !time_str2) return -1; + if (time_str2 && !time_str1) return 1; + + const date_cmp = cmp_values(setup1.scheduled_date, setup2.scheduled_date); + if (date_cmp !== 0) return date_cmp; + + if (time_str1 === '00:00' && time_str2 === '00:00') { + return cmp_values(setup1.match_num, setup2.match_num); + } + if (time_str1 === '00:00' && time_str2 !== '00:00') return 1; + if (time_str2 === '00:00' && time_str1 !== '00:00') return -1; + + const time_cmp = cmp_values(time_str1, time_str2); + if (time_cmp !== 0) return time_cmp; + + if (m1?.match_order !== undefined && m2?.match_order !== undefined) { + const match_order_cmp = cmp_values(m1.match_order, m2.match_order); + if (match_order_cmp !== 0) return match_order_cmp; + } + + return cmp_values(setup1.match_num, setup2.match_num); +} + +function get_courts_by_id(tournament) { + const map = new Map(); + const courts = Array.isArray(tournament?.courts) ? tournament.courts : []; + courts.forEach((court) => { + if (court && court._id) { + map.set(court._id, court); + } + }); + return map; +} + +function match_matches_location(match, location_id, courts_by_id) { + const setup = match?.setup || {}; + if (setup.location_id != null) { + return setup.location_id === location_id; + } + if (setup.court_id != null) { + const court = courts_by_id.get(setup.court_id); + if (!court) { + return true; + } + return court.location_id === location_id; + } + return true; +} + +function is_match_completely_initialized(match) { + const teams = match?.setup?.teams; + if (!Array.isArray(teams) || teams.length < 2) { + return false; + } + return teams.every((team) => Array.isArray(team?.players) && team.players.length > 0); +} + +function get_match_players(match) { + const teams = match?.setup?.teams; + if (!Array.isArray(teams)) { + return []; + } + return teams.flatMap((team) => Array.isArray(team?.players) ? team.players : []); +} + +function get_match_player_btp_ids(match) { + return get_match_players(match) + .map((player) => player?.btp_id) + .filter((btp_id) => btp_id != null); +} + +function is_finished_match(match) { + const state = match?.setup?.state; + if (state === 'finished') { + return true; + } + return match?.team1_won !== undefined && match?.team1_won !== null; +} + +function get_match_planning_ids(match) { + if (!Array.isArray(match?.btp_match_ids)) { + return []; + } + return match.btp_match_ids + .map((entry) => entry?.planning) + .filter((planning) => planning != null); +} + +function build_matches_by_planning_id(tournament) { + const map = new Map(); + const matches = Array.isArray(tournament?.matches) ? tournament.matches : []; + matches.forEach((match) => { + get_match_planning_ids(match).forEach((planning_id) => { + if (!map.has(planning_id)) { + map.set(planning_id, []); + } + map.get(planning_id).push(match); + }); + }); + return map; +} + +function get_direct_predecessor_matches(match, tournament, options = {}) { + const links = match?.setup?.links || {}; + const planning_ids = [links.from1, links.from2].filter((planning_id) => planning_id != null); + if (planning_ids.length === 0) { + return []; + } + + const matches_by_planning_id = options.matches_by_planning_id || build_matches_by_planning_id(tournament); + const predecessors = []; + planning_ids.forEach((planning_id) => { + const planning_matches = matches_by_planning_id.get(planning_id) || []; + planning_matches.forEach((candidate) => { + if (candidate && candidate._id !== match._id && !predecessors.includes(candidate)) { + predecessors.push(candidate); + } + }); + }); + return predecessors; +} + +function has_open_participant_dependency(match, tournament, options = {}) { + const participants_are_complete = is_match_completely_initialized(match); + const direct_predecessors = get_direct_predecessor_matches(match, tournament, options); + if (!participants_are_complete && direct_predecessors.some((predecessor) => !is_finished_match(predecessor))) { + return true; + } + if (!participants_are_complete && direct_predecessors.length === 0) { + const links = match?.setup?.links || {}; + if (links.from1 != null || links.from2 != null || links.from1_link != null || links.from2_link != null) { + return true; + } + } + + const current_player_ids = new Set(get_match_player_btp_ids(match)); + if (current_player_ids.size === 0) { + return false; + } + + const matches = Array.isArray(tournament?.matches) ? tournament.matches : []; + return matches.some((other_match) => { + if (!other_match || other_match._id === match._id) { + return false; + } + if (is_finished_match(other_match)) { + return false; + } + if (cmp_scheduled_match_order(other_match, match) >= 0) { + return false; + } + return get_match_player_btp_ids(other_match).some((btp_id) => current_player_ids.has(btp_id)); + }); +} + +function get_time_limit_before_scheduled_minutes(tournament) { + if (tournament?.preparation_call_time_limit_before_scheduled_enabled !== true) { + return null; + } + const minutes = Number(tournament?.preparation_call_time_limit_before_scheduled_minutes); + return Number.isFinite(minutes) && minutes >= 0 ? minutes : null; +} + +function get_time_limit_before_scheduled_minutes_for_prefix(tournament, prefix) { + if (prefix === 'preparation_call') { + return get_time_limit_before_scheduled_minutes(tournament); + } + if (tournament?.[`${prefix}_time_limit_before_scheduled_enabled`] !== true) { + return null; + } + const minutes = Number(tournament?.[`${prefix}_time_limit_before_scheduled_minutes`]); + return Number.isFinite(minutes) && minutes >= 0 ? minutes : null; +} + +function get_scheduled_timestamp(match) { + const setup = match?.setup || {}; + if (!setup.scheduled_date || !setup.scheduled_time_str) { + return null; + } + const timestamp = Date.parse(`${setup.scheduled_date}T${setup.scheduled_time_str}:00`); + return Number.isFinite(timestamp) ? timestamp : null; +} + +function passes_time_limit_before_scheduled(match, tournament, now_ts = DEFAULT_NOW_FN()) { + const limit_minutes = get_time_limit_before_scheduled_minutes(tournament); + if (limit_minutes == null) { + return true; + } + const scheduled_ts = get_scheduled_timestamp(match); + if (!Number.isFinite(scheduled_ts)) { + return false; + } + return now_ts >= (scheduled_ts - (limit_minutes * 60 * 1000)); +} + +function passes_time_limit_before_scheduled_for_prefix(match, tournament, prefix, now_ts = DEFAULT_NOW_FN()) { + const limit_minutes = get_time_limit_before_scheduled_minutes_for_prefix(tournament, prefix); + if (limit_minutes == null) { + return true; + } + const scheduled_ts = get_scheduled_timestamp(match); + if (!Number.isFinite(scheduled_ts)) { + return false; + } + return now_ts >= (scheduled_ts - (limit_minutes * 60 * 1000)); +} + +function get_participant_readiness_mode(tournament, prefix = 'preparation_call') { + const field = `${prefix}_participant_readiness_mode`; + const value = tournament?.[field]; + if (value === 'checked_in' || value === 'pause_expired' || value === 'disabled') { + return value; + } + if (prefix === 'preparation_call' && tournament?.preparation_call_player_pause_expired_enabled === true) { + return 'pause_expired'; + } + if (prefix === 'call_on_court' && tournament?.call_on_court_player_pause_expired_enabled === true) { + return 'pause_expired'; + } + return 'disabled'; +} + +function passes_player_pause_expired_rule(match, tournament, now_ts = DEFAULT_NOW_FN()) { + return passes_player_pause_expired_rule_for_prefix(match, tournament, 'preparation_call', now_ts); +} + +function passes_player_pause_expired_rule_for_prefix(match, tournament, prefix, now_ts = DEFAULT_NOW_FN()) { + if (get_participant_readiness_mode(tournament, prefix) !== 'pause_expired') { + return true; + } + + const pause_duration_ms = Number(tournament?.btp_settings?.pause_duration_ms); + if (!Number.isFinite(pause_duration_ms) || pause_duration_ms <= 0) { + return true; + } + + return get_match_players(match).every((player) => { + if (!player) { + return true; + } + if (player.now_playing_on_court || player.now_tablet_on_court) { + return false; + } + if (!player.last_time_on_court_ts) { + return true; + } + return (now_ts - Number(player.last_time_on_court_ts)) >= pause_duration_ms; + }); +} + +function passes_players_checked_in_rule(match, tournament) { + return passes_players_checked_in_rule_for_prefix(match, tournament, 'preparation_call'); +} + +function passes_players_checked_in_rule_for_prefix(match, tournament, prefix) { + if (get_participant_readiness_mode(tournament, prefix) !== 'checked_in') { + return true; + } + return get_match_players(match).every((player) => !player || player.checked_in === true); +} + +function get_waiting_technical_official_ids(tournament) { + const officials = Array.isArray(tournament?.umpires) ? tournament.umpires : []; + const umpire_ids = new Set(); + const service_judge_ids = new Set(); + + officials.forEach((official) => { + if (!official || !official._id) { + return; + } + if (official.umpire_wait != null) { + umpire_ids.add(official._id); + } + if (official.service_judge_wait != null) { + service_judge_ids.add(official._id); + } + }); + + return { umpire_ids, service_judge_ids }; +} + +function passes_technical_officials_available_rule(match, tournament) { + if (tournament?.preparation_call_technical_officials_available_enabled !== true) { + return true; + } + return passes_technical_officials_available_rule_for_prefix(match, tournament, 'preparation_call'); +} + +function passes_technical_officials_available_rule_for_prefix(match, tournament, prefix) { + if (prefix !== 'preparation_call' && prefix !== 'call_on_court') { + return true; + } + if (prefix === 'call_on_court' && tournament?.call_on_court_technical_officials_mode !== 'available') { + return true; + } + + const official_rotation_mode = tournament?.official_rotation_mode || 'umpire_and_service_judge'; + if (official_rotation_mode === 'disabled') { + return true; + } + const technical_official_auto_assignment_mode = tournament?.technical_official_auto_assignment_mode || 'manual_only'; + if (technical_official_auto_assignment_mode !== 'when_available' && technical_official_auto_assignment_mode !== 'on_preparation_call') { + return true; + } + + const setup = match?.setup || {}; + const has_assigned_umpire = !!setup.umpire; + const has_assigned_service_judge = !!setup.service_judge; + const assigned_official_ids = new Set( + [setup.umpire?._id, setup.service_judge?._id].filter((value) => !!value) + ); + + const { umpire_ids, service_judge_ids } = get_waiting_technical_official_ids(tournament); + const available_umpire_ids = new Set([...umpire_ids].filter((official_id) => !assigned_official_ids.has(official_id))); + const available_service_judge_ids = new Set([...service_judge_ids].filter((official_id) => !assigned_official_ids.has(official_id))); + if (official_rotation_mode === 'umpire_only') { + return has_assigned_umpire || available_umpire_ids.size > 0; + } + if (has_assigned_umpire && has_assigned_service_judge) { + return true; + } + + const needs_umpire = !has_assigned_umpire; + const needs_service_judge = !has_assigned_service_judge; + + if (needs_umpire && !needs_service_judge) { + return available_umpire_ids.size > 0; + } + if (!needs_umpire && needs_service_judge) { + return available_service_judge_ids.size > 0; + } + if (!needs_umpire && !needs_service_judge) { + return true; + } + + if (available_umpire_ids.size === 0 || available_service_judge_ids.size === 0) { + return false; + } + + for (const umpire_id of available_umpire_ids) { + for (const service_judge_id of available_service_judge_ids) { + if (umpire_id !== service_judge_id) { + return true; + } + } + } + + return false; +} + +function get_call_on_court_required_technical_official_roles(tournament, court) { + const official_rotation_mode = tournament?.official_rotation_mode || 'umpire_and_service_judge'; + const court_supports_umpire = !!court && court.has_umpire !== false; + const court_supports_service_judge = court_supports_umpire && !!court && court.has_service_judge !== false; + + if (official_rotation_mode === 'disabled') { + return { + needs_umpire: false, + needs_service_judge: false, + }; + } + + if (official_rotation_mode === 'umpire_only') { + return { + needs_umpire: true, + needs_service_judge: false, + }; + } + + return { + needs_umpire: true, + needs_service_judge: court_supports_service_judge, + }; +} + +function passes_call_on_court_technical_officials_available_rule(match, court, tournament) { + if (tournament?.call_on_court_technical_officials_mode !== 'available') { + return true; + } + if (!court) { + return false; + } + + const technical_official_auto_assignment_mode = tournament?.technical_official_auto_assignment_mode || 'manual_only'; + if ( + technical_official_auto_assignment_mode !== 'when_available' && + technical_official_auto_assignment_mode !== 'on_preparation_call' && + technical_official_auto_assignment_mode !== 'on_match_call_if_possible' + ) { + return true; + } + + const { needs_umpire, needs_service_judge } = get_call_on_court_required_technical_official_roles(tournament, court); + const setup = match?.setup || {}; + const assigned_official_ids = new Set( + [setup.umpire?._id, setup.service_judge?._id].filter((value) => !!value) + ); + const { umpire_ids, service_judge_ids } = get_waiting_technical_official_ids(tournament); + const available_umpire_ids = new Set([...umpire_ids].filter((official_id) => !assigned_official_ids.has(official_id))); + const available_service_judge_ids = new Set([...service_judge_ids].filter((official_id) => !assigned_official_ids.has(official_id))); + + if (!needs_umpire && !needs_service_judge) { + return true; + } + + const has_assigned_umpire = !needs_umpire || !!setup.umpire; + const has_assigned_service_judge = !needs_service_judge || !!setup.service_judge; + + if (has_assigned_umpire && has_assigned_service_judge) { + return true; + } + if (!has_assigned_umpire && has_assigned_service_judge) { + return available_umpire_ids.size > 0; + } + if (has_assigned_umpire && !has_assigned_service_judge) { + return available_service_judge_ids.size > 0; + } + + if (available_umpire_ids.size === 0) { + return false; + } + if (!needs_service_judge) { + return true; + } + if (available_service_judge_ids.size === 0) { + return false; + } + + for (const umpire_id of available_umpire_ids) { + for (const service_judge_id of available_service_judge_ids) { + if (umpire_id !== service_judge_id) { + return true; + } + } + } + + return false; +} + +function passes_call_on_court_technical_official_assignment_possible_rule(match, court, tournament) { + if (tournament?.call_on_court_technical_officials_mode !== 'available') { + return true; + } + if (!court) { + return false; + } + const official_rotation_mode = tournament?.official_rotation_mode || 'umpire_and_service_judge'; + if (official_rotation_mode === 'disabled') { + return true; + } + const setup = match?.setup || {}; + const { needs_umpire: role_needs_umpire, needs_service_judge: role_needs_service_judge } = + get_call_on_court_required_technical_official_roles(tournament, court); + const needs_umpire = role_needs_umpire && !setup.umpire; + const needs_service_judge = role_needs_service_judge && !setup.service_judge; + const requires_umpire_space = !!setup.umpire || needs_umpire; + const requires_service_judge_space = !!setup.service_judge || needs_service_judge; + + if (requires_umpire_space && court.has_umpire === false) { + return false; + } + if (requires_service_judge_space && court.has_service_judge === false) { + return false; + } + return true; +} + +function passes_base_preparation_rules(match, location_id, tournament, options = {}) { + const setup = match?.setup || {}; + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + const matches_by_planning_id = options.matches_by_planning_id || build_matches_by_planning_id(tournament); + const now_ts = options.now_ts != null ? options.now_ts : DEFAULT_NOW_FN(); + const ignore_location = options.ignore_location === true; + const ignore_technical_officials_available_rule = options.ignore_technical_officials_available_rule === true; + + if (setup.state !== 'scheduled') return false; + if (setup.is_match !== true) return false; + if (setup.incomplete === true) return false; + if (!is_match_completely_initialized(match)) return false; + if (match?.team1_won !== undefined && match?.team1_won !== null) return false; + if (setup.state === 'preparation' || setup.state === 'oncourt' || setup.state === 'blocked' || setup.state === 'finished') return false; + if (!ignore_location && !match_matches_location(match, location_id, courts_by_id)) return false; + if (has_open_participant_dependency(match, tournament, { matches_by_planning_id })) return false; + if (!passes_time_limit_before_scheduled(match, tournament, now_ts)) return false; + if (!passes_player_pause_expired_rule(match, tournament, now_ts)) return false; + if (!ignore_technical_officials_available_rule && !passes_technical_officials_available_rule(match, tournament)) return false; + + return true; +} + +function get_frontier_block_limit(tournament) { + if (tournament?.preparation_call_block_ahead_limit_enabled !== true) { + return null; + } + const limit = Number(tournament?.preparation_call_block_ahead_limit); + return Number.isFinite(limit) && limit >= 0 ? limit : null; +} + +function get_frontier_block_limit_for_prefix(tournament, prefix) { + if (prefix === 'preparation_call') { + return get_frontier_block_limit(tournament); + } + if (tournament?.[`${prefix}_block_ahead_limit_enabled`] !== true) { + return null; + } + const limit = Number(tournament?.[`${prefix}_block_ahead_limit`]); + return Number.isFinite(limit) && limit >= 0 ? limit : null; +} + +function get_frontier_time_limit_minutes(tournament) { + if (tournament?.preparation_call_time_ahead_of_frontier_enabled !== true) { + return null; + } + const minutes = Number(tournament?.preparation_call_time_ahead_of_frontier_minutes); + return Number.isFinite(minutes) && minutes >= 0 ? minutes : null; +} + +function get_frontier_time_limit_minutes_for_prefix(tournament, prefix) { + if (prefix === 'preparation_call') { + return get_frontier_time_limit_minutes(tournament); + } + if (tournament?.[`${prefix}_time_ahead_of_frontier_enabled`] !== true) { + return null; + } + const minutes = Number(tournament?.[`${prefix}_time_ahead_of_frontier_minutes`]); + return Number.isFinite(minutes) && minutes >= 0 ? minutes : null; +} + +function get_frontier_match_limit(tournament) { + if (tournament?.preparation_call_matches_ahead_of_frontier_enabled !== true) { + return null; + } + const limit = Number(tournament?.preparation_call_matches_ahead_of_frontier_limit); + return Number.isFinite(limit) && limit >= 0 ? limit : null; +} + +function get_frontier_match_limit_for_prefix(tournament, prefix) { + if (prefix === 'preparation_call') { + return get_frontier_match_limit(tournament); + } + if (tournament?.[`${prefix}_matches_ahead_of_frontier_enabled`] !== true) { + return null; + } + const limit = Number(tournament?.[`${prefix}_matches_ahead_of_frontier_limit`]); + return Number.isFinite(limit) && limit >= 0 ? limit : null; +} + +function get_location_relevant_matches(tournament, location_id, options = {}) { + const matches = Array.isArray(tournament?.matches) ? tournament.matches : []; + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + return matches + .filter((match) => match?.setup?.state === 'scheduled') + .filter((match) => match_matches_location(match, location_id, courts_by_id)) + .sort(cmp_scheduled_match_order); +} + +function find_preparation_frontier_match(tournament, location_id, options = {}) { + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + const matches_by_planning_id = options.matches_by_planning_id || build_matches_by_planning_id(tournament); + const now_ts = options.now_ts != null ? options.now_ts : DEFAULT_NOW_FN(); + const relevant_matches = options.relevant_matches || get_location_relevant_matches(tournament, location_id, { courts_by_id }); + + return relevant_matches.find((match) => !passes_base_preparation_rules(match, location_id, tournament, { + courts_by_id, + matches_by_planning_id, + now_ts, + ignore_technical_officials_available_rule: options.ignore_technical_officials_available_rule === true, + })) || null; +} + +function build_frontier_block_sequence(matches) { + const block_sequence = []; + let last_block_key = null; + matches.forEach((match) => { + const setup = match?.setup || {}; + const block_key = `${setup.event_name || ''}|${setup.phase_block_key || 'UNKNOWN'}`; + if (block_key !== last_block_key) { + block_sequence.push(block_key); + last_block_key = block_key; + } + }); + return block_sequence; +} + +function get_frontier_block_distance(match, frontier, relevant_matches) { + const sequence = build_frontier_block_sequence(relevant_matches); + const match_key = `${match?.setup?.event_name || ''}|${match?.setup?.phase_block_key || 'UNKNOWN'}`; + const frontier_key = `${frontier?.setup?.event_name || ''}|${frontier?.setup?.phase_block_key || 'UNKNOWN'}`; + const match_index = sequence.indexOf(match_key); + const frontier_index = sequence.indexOf(frontier_key); + if (match_index < 0 || frontier_index < 0) { + return Number.POSITIVE_INFINITY; + } + return match_index - frontier_index; +} + +function passes_frontier_block_limit(match, frontier, tournament, relevant_matches) { + const limit = get_frontier_block_limit(tournament); + if (limit == null || !frontier) { + return true; + } + return get_frontier_block_distance(match, frontier, relevant_matches) <= limit; +} + +function passes_frontier_block_limit_for_prefix(match, frontier, tournament, relevant_matches, prefix) { + const limit = get_frontier_block_limit_for_prefix(tournament, prefix); + if (limit == null || !frontier) { + return true; + } + return get_frontier_block_distance(match, frontier, relevant_matches) <= limit; +} + +function passes_frontier_time_limit(match, frontier, tournament) { + const limit_minutes = get_frontier_time_limit_minutes(tournament); + if (limit_minutes == null || !frontier) { + return true; + } + const match_ts = get_scheduled_timestamp(match); + const frontier_ts = get_scheduled_timestamp(frontier); + if (!Number.isFinite(match_ts) || !Number.isFinite(frontier_ts)) { + return false; + } + return (match_ts - frontier_ts) <= (limit_minutes * 60 * 1000); +} + +function passes_frontier_time_limit_for_prefix(match, frontier, tournament, prefix) { + const limit_minutes = get_frontier_time_limit_minutes_for_prefix(tournament, prefix); + if (limit_minutes == null || !frontier) { + return true; + } + const match_ts = get_scheduled_timestamp(match); + const frontier_ts = get_scheduled_timestamp(frontier); + if (!Number.isFinite(match_ts) || !Number.isFinite(frontier_ts)) { + return false; + } + return (match_ts - frontier_ts) <= (limit_minutes * 60 * 1000); +} + +function passes_frontier_match_limit(match, frontier, tournament, relevant_matches) { + const limit = get_frontier_match_limit(tournament); + if (limit == null || !frontier) { + return true; + } + const match_index = relevant_matches.findIndex((candidate) => candidate?._id === match?._id); + const frontier_index = relevant_matches.findIndex((candidate) => candidate?._id === frontier?._id); + if (match_index < 0 || frontier_index < 0) { + return false; + } + return (match_index - frontier_index) <= limit; +} + +function passes_frontier_match_limit_for_prefix(match, frontier, tournament, relevant_matches, prefix) { + const limit = get_frontier_match_limit_for_prefix(tournament, prefix); + if (limit == null || !frontier) { + return true; + } + const match_index = relevant_matches.findIndex((candidate) => candidate?._id === match?._id); + const frontier_index = relevant_matches.findIndex((candidate) => candidate?._id === frontier?._id); + if (match_index < 0 || frontier_index < 0) { + return false; + } + return (match_index - frontier_index) <= limit; +} + +function get_call_on_court_relevant_matches(tournament, location_id, options = {}) { + const matches = Array.isArray(tournament?.matches) ? tournament.matches : []; + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + const only_preparation = tournament?.call_on_court_only_preparation_enabled === true; + return matches + .filter((match) => { + const state = match?.setup?.state; + if (only_preparation) { + return state === 'preparation'; + } + return state === 'scheduled' || state === 'preparation'; + }) + .filter((match) => match_matches_location(match, location_id, courts_by_id)) + .sort(cmp_scheduled_match_order); +} + +function get_call_on_court_preparation_age_minutes(tournament) { + if (tournament?.call_on_court_only_preparation_enabled !== true) { + return null; + } + const minutes = Number(tournament?.call_on_court_only_preparation_minutes); + return Number.isFinite(minutes) && minutes >= 0 ? minutes : 0; +} + +function passes_call_on_court_preparation_rule(match, tournament, now_ts = DEFAULT_NOW_FN()) { + const minimum_minutes = get_call_on_court_preparation_age_minutes(tournament); + if (minimum_minutes == null) { + return true; + } + if (match?.setup?.state !== 'preparation') { + return false; + } + const preparation_ts = Number(match?.setup?.preparation_call_timestamp); + if (!Number.isFinite(preparation_ts)) { + return false; + } + return (now_ts - preparation_ts) >= (minimum_minutes * 60 * 1000); +} + +function get_required_technical_official_roles(tournament) { + const official_rotation_mode = tournament?.official_rotation_mode || 'umpire_and_service_judge'; + return { + needs_umpire: official_rotation_mode !== 'disabled', + needs_service_judge: official_rotation_mode === 'umpire_and_service_judge', + }; +} + +function passes_call_on_court_technical_officials_checked_in_rule(match, court, tournament) { + const mode = tournament?.call_on_court_technical_officials_mode || 'disabled'; + if (mode !== 'checked_in') { + return true; + } + const { needs_umpire, needs_service_judge } = get_call_on_court_required_technical_official_roles(tournament, court); + const setup = match?.setup || {}; + if (needs_umpire && setup.umpire?.checked_in !== true) { + return false; + } + if (needs_service_judge && setup.service_judge?.checked_in !== true) { + return false; + } + return true; +} + +function passes_call_on_court_assigned_official_space_rule(match, court, tournament) { + if (tournament?.call_on_court_require_official_space_enabled !== true) { + return true; + } + if (!court) { + return false; + } + const setup = match?.setup || {}; + if (setup.umpire && court.has_umpire === false) { + return false; + } + if (setup.service_judge && court.has_service_judge === false) { + return false; + } + return true; +} + +function passes_base_call_on_court_rules(match, court_id, tournament, options = {}) { + const setup = match?.setup || {}; + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + const matches_by_planning_id = options.matches_by_planning_id || build_matches_by_planning_id(tournament); + const now_ts = options.now_ts != null ? options.now_ts : DEFAULT_NOW_FN(); + const court = courts_by_id.get(court_id) || null; + const location_id = court?.location_id || null; + + if (!court || court.is_active !== true) return false; + if (setup.state !== 'scheduled' && setup.state !== 'preparation') return false; + if (setup.is_match !== true) return false; + if (setup.incomplete === true) return false; + if (!is_match_completely_initialized(match)) return false; + if (setup.now_on_court === true) return false; + if (match?.team1_won !== undefined && match?.team1_won !== null) return false; + if (!match_matches_location(match, location_id, courts_by_id)) return false; + if (has_open_participant_dependency(match, tournament, { matches_by_planning_id })) return false; + if (!passes_call_on_court_preparation_rule(match, tournament, now_ts)) return false; + if (!passes_time_limit_before_scheduled_for_prefix(match, tournament, 'call_on_court', now_ts)) return false; + if (!passes_players_checked_in_rule_for_prefix(match, tournament, 'call_on_court')) return false; + if (!passes_player_pause_expired_rule_for_prefix(match, tournament, 'call_on_court', now_ts)) return false; + if (!passes_call_on_court_technical_officials_checked_in_rule(match, court, tournament)) return false; + if (!passes_call_on_court_technical_officials_available_rule(match, court, tournament)) return false; + if (!passes_call_on_court_technical_official_assignment_possible_rule(match, court, tournament)) return false; + if (!passes_call_on_court_assigned_official_space_rule(match, court, tournament)) return false; + + return true; +} + +function find_call_on_court_frontier_match(tournament, court_id, options = {}) { + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + const court = courts_by_id.get(court_id) || null; + const location_id = court?.location_id || null; + const matches_by_planning_id = options.matches_by_planning_id || build_matches_by_planning_id(tournament); + const now_ts = options.now_ts != null ? options.now_ts : DEFAULT_NOW_FN(); + const relevant_matches = options.relevant_matches || get_call_on_court_relevant_matches(tournament, location_id, { courts_by_id }); + + return relevant_matches.find((match) => !passes_base_call_on_court_rules(match, court_id, tournament, { + courts_by_id, + matches_by_planning_id, + now_ts, + })) || null; +} + +function is_match_eligible_for_on_court_call(match, court_id, tournament, options = {}) { + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + const court = courts_by_id.get(court_id) || null; + const location_id = court?.location_id || null; + const matches_by_planning_id = options.matches_by_planning_id || build_matches_by_planning_id(tournament); + const now_ts = options.now_ts != null ? options.now_ts : DEFAULT_NOW_FN(); + const relevant_matches = options.relevant_matches || get_call_on_court_relevant_matches(tournament, location_id, { courts_by_id }); + const frontier = options.frontier !== undefined + ? options.frontier + : find_call_on_court_frontier_match(tournament, court_id, { + courts_by_id, + matches_by_planning_id, + now_ts, + relevant_matches, + }); + + if (!passes_base_call_on_court_rules(match, court_id, tournament, { + courts_by_id, + matches_by_planning_id, + now_ts, + })) { + return false; + } + if (frontier && cmp_scheduled_match_order(match, frontier) < 0) { + return true; + } + if (!passes_frontier_block_limit_for_prefix(match, frontier, tournament, relevant_matches, 'call_on_court')) return false; + if (!passes_frontier_time_limit_for_prefix(match, frontier, tournament, 'call_on_court')) return false; + if (!passes_frontier_match_limit_for_prefix(match, frontier, tournament, relevant_matches, 'call_on_court')) return false; + + return true; +} + +function cmp_call_on_court_candidate_order(m1, m2) { + const state1 = m1?.setup?.state; + const state2 = m2?.setup?.state; + if (state1 === 'preparation' && state2 !== 'preparation') return -1; + if (state2 === 'preparation' && state1 !== 'preparation') return 1; + const preparation_age_cmp = cmp_values( + Number(m1?.setup?.preparation_call_timestamp) || 0, + Number(m2?.setup?.preparation_call_timestamp) || 0 + ); + if (state1 === 'preparation' && state2 === 'preparation' && preparation_age_cmp !== 0) { + return preparation_age_cmp; + } + return cmp_scheduled_match_order(m1, m2); +} + +function find_call_on_court_candidates(tournament, court_id, options = {}) { + const matches = Array.isArray(tournament?.matches) ? tournament.matches : []; + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + const court = courts_by_id.get(court_id) || null; + const location_id = court?.location_id || null; + const matches_by_planning_id = options.matches_by_planning_id || build_matches_by_planning_id(tournament); + const now_ts = options.now_ts != null ? options.now_ts : DEFAULT_NOW_FN(); + const relevant_matches = options.relevant_matches || get_call_on_court_relevant_matches(tournament, location_id, { courts_by_id }); + const frontier = options.frontier !== undefined + ? options.frontier + : find_call_on_court_frontier_match(tournament, court_id, { + courts_by_id, + matches_by_planning_id, + now_ts, + relevant_matches, + }); + + return matches + .filter((match) => is_match_eligible_for_on_court_call(match, court_id, tournament, { + courts_by_id, + matches_by_planning_id, + now_ts, + relevant_matches, + frontier, + })) + .sort(cmp_call_on_court_candidate_order); +} + +function is_match_eligible_for_preparation(match, location_id, tournament, options = {}) { + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + const matches_by_planning_id = options.matches_by_planning_id || build_matches_by_planning_id(tournament); + const now_ts = options.now_ts != null ? options.now_ts : DEFAULT_NOW_FN(); + const relevant_matches = options.relevant_matches || get_location_relevant_matches(tournament, location_id, { courts_by_id }); + const frontier = options.frontier !== undefined + ? options.frontier + : find_preparation_frontier_match(tournament, location_id, { + courts_by_id, + matches_by_planning_id, + now_ts, + relevant_matches, + ignore_technical_officials_available_rule: options.ignore_technical_officials_available_rule === true, + }); + + if (!passes_base_preparation_rules(match, location_id, tournament, { + courts_by_id, + matches_by_planning_id, + now_ts, + ignore_technical_officials_available_rule: options.ignore_technical_officials_available_rule === true, + })) { + return false; + } + if (frontier && cmp_scheduled_match_order(match, frontier) < 0) { + return true; + } + if (!passes_frontier_block_limit(match, frontier, tournament, relevant_matches)) return false; + if (!passes_frontier_time_limit(match, frontier, tournament)) return false; + if (!passes_frontier_match_limit(match, frontier, tournament, relevant_matches)) return false; + + return true; +} + +function find_location_preparation_candidates(tournament, location_id, options = {}) { + const matches = Array.isArray(tournament?.matches) ? tournament.matches : []; + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + const matches_by_planning_id = options.matches_by_planning_id || build_matches_by_planning_id(tournament); + const now_ts = options.now_ts != null ? options.now_ts : DEFAULT_NOW_FN(); + const relevant_matches = options.relevant_matches || get_location_relevant_matches(tournament, location_id, { courts_by_id }); + const frontier = options.frontier !== undefined + ? options.frontier + : find_preparation_frontier_match(tournament, location_id, { + courts_by_id, + matches_by_planning_id, + now_ts, + relevant_matches, + ignore_technical_officials_available_rule: options.ignore_technical_officials_available_rule === true, + }); + + return matches + .filter((match) => is_match_eligible_for_preparation(match, location_id, tournament, { + courts_by_id, + matches_by_planning_id, + now_ts, + relevant_matches, + frontier, + ignore_technical_officials_available_rule: options.ignore_technical_officials_available_rule === true, + })) + .sort(cmp_scheduled_match_order); +} + +function get_global_relevant_matches(tournament) { + const matches = Array.isArray(tournament?.matches) ? tournament.matches : []; + return matches + .filter((match) => match?.setup?.state === 'scheduled') + .sort(cmp_scheduled_match_order); +} + +function find_global_preparation_frontier_match(tournament, options = {}) { + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + const matches_by_planning_id = options.matches_by_planning_id || build_matches_by_planning_id(tournament); + const now_ts = options.now_ts != null ? options.now_ts : DEFAULT_NOW_FN(); + const relevant_matches = options.relevant_matches || get_global_relevant_matches(tournament); + + return relevant_matches.find((match) => !passes_base_preparation_rules(match, null, tournament, { + courts_by_id, + matches_by_planning_id, + now_ts, + ignore_location: true, + ignore_technical_officials_available_rule: options.ignore_technical_officials_available_rule === true, + })) || null; +} + +function find_global_preparation_candidates(tournament, options = {}) { + const matches = Array.isArray(tournament?.matches) ? tournament.matches : []; + const courts_by_id = options.courts_by_id || get_courts_by_id(tournament); + const matches_by_planning_id = options.matches_by_planning_id || build_matches_by_planning_id(tournament); + const now_ts = options.now_ts != null ? options.now_ts : DEFAULT_NOW_FN(); + const relevant_matches = options.relevant_matches || get_global_relevant_matches(tournament); + const frontier = options.frontier !== undefined + ? options.frontier + : find_global_preparation_frontier_match(tournament, { + courts_by_id, + matches_by_planning_id, + now_ts, + relevant_matches, + ignore_technical_officials_available_rule: options.ignore_technical_officials_available_rule === true, + }); + + return matches + .filter((match) => { + if (!passes_base_preparation_rules(match, null, tournament, { + courts_by_id, + matches_by_planning_id, + now_ts, + ignore_location: true, + ignore_technical_officials_available_rule: options.ignore_technical_officials_available_rule === true, + })) { + return false; + } + if (frontier && cmp_scheduled_match_order(match, frontier) < 0) { + return true; + } + if (!passes_frontier_block_limit(match, frontier, tournament, relevant_matches)) return false; + if (!passes_frontier_time_limit(match, frontier, tournament)) return false; + if (!passes_frontier_match_limit(match, frontier, tournament, relevant_matches)) return false; + return true; + }) + .sort(cmp_scheduled_match_order); +} + +function calculate_location_preparation_selection(tournament, location_id, options = {}) { + const status = calculate_location_preparation_status(tournament, location_id); + const candidates = find_location_preparation_candidates(tournament, location_id, options); + const effective_required_preparation_count = + (tournament?.call_preparation_matches_automatically_enabled ? status.successor_need_count : 0); + const effective_missing_preparation_count = Math.max(0, effective_required_preparation_count - status.current_preparation_count); + const selected_matches = candidates.slice(0, status.missing_preparation_count); + const auto_selected_matches = candidates.slice(0, effective_missing_preparation_count); + + return { + location_id, + ...status, + effective_required_preparation_count, + effective_missing_preparation_count, + candidates, + selected_matches, + auto_selected_matches, + }; +} + +function get_preparation_successor_rally_count(tournament) { + const rally_count = Number(tournament && tournament.preparation_successor_rally_count); + if (Number.isFinite(rally_count) && rally_count > 0) { + return Math.floor(rally_count); + } + return DEFAULT_PREPARATION_SUCCESSOR_RALLY_COUNT; +} + +function calculate_preparation_successor_state(match, tournament) { + const rally_count = get_preparation_successor_rally_count(tournament); + const needs_preparation_successor = can_leader_finish_match_within_rallies( + match, + match && match.setup ? match.setup.scoring_format : null, + rally_count + ); + return { + needs_preparation_successor, + needs_preparation_successor_ts: needs_preparation_successor + ? (match?.setup?.needs_preparation_successor_ts || Date.now()) + : null, + rally_count, + }; +} + +async function fetch_location_preparation_status(app, tournament_key, location_id) { + const [courts, matches] = await Promise.all([ + app.db.courts.find_async({ tournament_key }), + app.db.matches.find_async({ tournament_key }), + ]); + + return calculate_location_preparation_status({ + courts, + matches, + }, location_id); +} + +async function fetch_location_preparation_selection(app, tournament_key, location_id, options = {}) { + const [tournament, locations, courts, matches, umpires] = await Promise.all([ + app.db.tournaments.findOne_async({ key: tournament_key }), + app.db.locations.find_async({ tournament_key }), + app.db.courts.find_async({ tournament_key }), + app.db.matches.find_async({ tournament_key }), + app.db.umpires.find_async({ tournament_key }), + ]); + + const location = (locations || []).find((entry) => entry && entry._id === location_id) || null; + const selection = calculate_location_preparation_selection({ + ...(tournament || {}), + locations, + courts, + matches, + umpires, + }, location_id, options); + + return { + ...selection, + location, + }; +} + +async function fetch_all_location_preparation_selections(app, tournament_key, options = {}) { + const [tournament, locations, courts, matches, umpires] = await Promise.all([ + app.db.tournaments.findOne_async({ key: tournament_key }), + app.db.locations.find_async({ tournament_key }), + app.db.courts.find_async({ tournament_key }), + app.db.matches.find_async({ tournament_key }), + app.db.umpires.find_async({ tournament_key }), + ]); + + return (locations || []).map((location) => { + const selection = calculate_location_preparation_selection({ + ...(tournament || {}), + locations, + courts, + matches, + umpires, + }, location._id, options); + + return { + ...selection, + location, + }; + }); +} + +async function fetch_global_preparation_candidates(app, tournament_key, options = {}) { + const [tournament, locations, courts, matches, umpires] = await Promise.all([ + app.db.tournaments.findOne_async({ key: tournament_key }), + app.db.locations.find_async({ tournament_key }), + app.db.courts.find_async({ tournament_key }), + app.db.matches.find_async({ tournament_key }), + app.db.umpires.find_async({ tournament_key }), + ]); + + return find_global_preparation_candidates({ + ...(tournament || {}), + locations, + courts, + matches, + umpires, + }, options); +} + +module.exports = { + can_leader_finish_match_within_rallies, + calculate_location_preparation_need, + calculate_location_preparation_selection, + calculate_location_preparation_status, + calculate_preparation_successor_state, + fetch_all_location_preparation_selections, + fetch_global_preparation_candidates, + fetch_location_preparation_selection, + fetch_location_preparation_status, + find_call_on_court_candidates, + find_global_preparation_candidates, + find_location_preparation_candidates, + find_call_on_court_frontier_match, + get_preparation_successor_rally_count, + get_current_match_state, + get_current_leader, + has_open_participant_dependency, + find_preparation_frontier_match, + get_participant_readiness_mode, + is_match_eligible_for_on_court_call, + is_match_eligible_for_preparation, + passes_call_on_court_assigned_official_space_rule, + passes_call_on_court_technical_official_assignment_possible_rule, + passes_call_on_court_preparation_rule, + passes_call_on_court_technical_officials_checked_in_rule, + passes_players_checked_in_rule, + passes_player_pause_expired_rule, + passes_technical_officials_available_rule, + rallies_needed_for_set_win, +}; diff --git a/bts/match_utils.js b/bts/match_utils.js index 16f1da0..b461bbb 100644 --- a/bts/match_utils.js +++ b/bts/match_utils.js @@ -2,6 +2,117 @@ const assert = require('assert'); const async = require('async'); +const update_queue = require('./update_queue'); + +const pending_preparation_selection_runs = new Map(); +const pending_technical_official_assignment_runs = new Map(); +const pending_technical_official_pause_runs = new Map(); +let technical_official_pause_interval = null; + +function is_tournament_automation_enabled(tournament) { + return tournament?.automation_enabled !== false; +} + +function get_court_sort_value(court) { + const numeric_num = Number(court?.num); + if (Number.isFinite(numeric_num)) { + return { numeric: true, value: numeric_num }; + } + return { numeric: false, value: String(court?.num || '') }; +} + +function cmp_court_order(c1, c2) { + const a = get_court_sort_value(c1); + const b = get_court_sort_value(c2); + if (a.numeric && b.numeric) { + return a.value - b.value; + } + return String(a.value).localeCompare(String(b.value), 'de', { numeric: true, sensitivity: 'base' }); +} + +function sort_free_courts_for_auto_call(courts, tabletoperators, tournament) { + const free_courts = Array.isArray(courts) ? [...courts] : []; + if (tournament?.tabletoperator_enabled !== true) { + return free_courts.sort(cmp_court_order); + } + + const free_court_ids = new Set(free_courts.map((court) => court?._id).filter(Boolean)); + const preferred_court_index = new Map(); + + (Array.isArray(tabletoperators) ? [...tabletoperators] : []) + .filter((tabletoperator) => tabletoperator && tabletoperator.court == null) + .sort((a, b) => Number(a.start_ts || 0) - Number(b.start_ts || 0)) + .forEach((tabletoperator, index) => { + const played_on_court = tabletoperator?.played_on_court; + if (!played_on_court || !free_court_ids.has(played_on_court) || preferred_court_index.has(played_on_court)) { + return; + } + preferred_court_index.set(played_on_court, index); + }); + + return free_courts.sort((c1, c2) => { + const preference1 = preferred_court_index.has(c1?._id) ? preferred_court_index.get(c1._id) : Number.POSITIVE_INFINITY; + const preference2 = preferred_court_index.has(c2?._id) ? preferred_court_index.get(c2._id) : Number.POSITIVE_INFINITY; + if (preference1 !== preference2) { + return preference1 - preference2; + } + return cmp_court_order(c1, c2); + }); +} + +function get_technical_official_break_after_assignment_ms(tournament) { + const seconds = Number(tournament?.technical_official_break_after_assignment_seconds); + if (!Number.isFinite(seconds) || seconds <= 0) { + return 0; + } + return Math.round(seconds * 1000); +} + +function is_technical_official_unavailable(official) { + if (!official) { + return true; + } + return official.umpire_pause != null || + official.service_judge_pause != null || + official.umpire_manual_pause != null || + official.service_judge_manual_pause != null || + official.inactive_list != null; +} + +function get_effective_technical_official_checked_in(official, tournament_or_btp_settings) { + if (!official) { + return false; + } + const btp_settings = tournament_or_btp_settings?.btp_settings || tournament_or_btp_settings || {}; + if (btp_settings.check_in_per_match === false) { + return !is_technical_official_unavailable(official); + } + return !!official.checked_in; +} + +function sync_technical_official_checked_in(official, tournament_or_btp_settings) { + if (!official) { + return official; + } + official.checked_in = get_effective_technical_official_checked_in(official, tournament_or_btp_settings); + return official; +} + +function match_needs_technical_official_assignment(match, tournament) { + if (!match || !match.setup) { + return false; + } + if ((tournament?.official_rotation_mode || 'umpire_and_service_judge') === 'disabled') { + return false; + } + if (!match.setup.umpire) { + return true; + } + if ((tournament?.official_rotation_mode || 'umpire_and_service_judge') !== 'umpire_and_service_judge') { + return false; + } + return !match.setup.service_judge; +} async function match_update(app, match, old_court, callback) { async.waterfall([ @@ -45,7 +156,37 @@ async function call_match(app, tournament, match, old_court, callback) { if (match_completly_initialized(match.setup) == false) { return callback("Match cannot be called one or more Teams are not set."); } + console.log('[bts] auto_call_trace:call_match_start', { + ts: Date.now(), + tournament_key: tournament && tournament.key, + match_id: match._id, + court_id: match.setup && match.setup.court_id, + old_court, + state: match.setup && match.setup.state, + now_on_court: match.setup && match.setup.now_on_court, + }); async.waterfall([ (wcb) => add_called_timestamp(match, wcb), + (wcb) => auto_assign_technical_officials_for_match(app, tournament, match._id, (assignErr) => { + if (assignErr) { + return wcb(assignErr); + } + app.db.matches.findOne({ _id: match._id, tournament_key: tournament.key }, (reloadErr, refreshed_match) => { + if (reloadErr) { + return wcb(reloadErr); + } + if (refreshed_match) { + // Preserve transient on-court fields that are only persisted later in this waterfall. + refreshed_match.setup = refreshed_match.setup || {}; + refreshed_match.setup.court_id = match.setup.court_id; + refreshed_match.setup.now_on_court = match.setup.now_on_court; + refreshed_match.setup.called_timestamp = match.setup.called_timestamp; + refreshed_match.setup.state = match.setup.state; + match = refreshed_match; + } + return wcb(null); + }); + }, { allow_on_match_call: true }), + (wcb) => drop_unsupported_court_officials(app, tournament, match, wcb), (wcb) => add_tabletoperators(app, tournament, match, wcb), (wcb) => set_umpires_on_court(app, tournament, match, wcb), (wcb) => remove_highlight_preparation(match, wcb), @@ -53,13 +194,21 @@ async function call_match(app, tournament, match, old_court, callback) { (wcb) => update_match_db(app, match, wcb), (wcb) => update_court_db(app, match, wcb), (wcb) => notify_change_match_edit(app, match, wcb), - (wcb) => notify_change_match_called_on_court(app, match, wcb), (wcb) => notify_bupws(app, match, old_court, wcb), + (wcb) => notify_change_match_called_on_court(app, match, wcb), (wcb) => set_player_on_court(app, tournament.key, match.setup, wcb), (wcb) => set_player_on_tablet(app, tournament.key, match.setup, wcb), (wcb) => update_btp_courts(app, tournament.key, match, wcb), - (wcb) => call_next_possible_match_for_preparation(app, tournament.key, wcb)], + (wcb) => auto_execute_preparation_selection_for_setup(app, tournament, match.setup, wcb)], (err) => { + console.log('[bts] auto_call_trace:call_match_end', { + ts: Date.now(), + tournament_key: tournament && tournament.key, + match_id: match && match._id, + court_id: match && match.setup && match.setup.court_id, + called_timestamp: match && match.setup && match.setup.called_timestamp, + error: err ? (err.message || String(err)) : null, + }); return callback(err, match); } ); @@ -80,8 +229,8 @@ async function switch_court(app, tournament, match, old_court, callback) { (wcb) => update_match_db(app, match, wcb), (wcb) => update_court_db(app, match, wcb), (wcb) => notify_change_match_edit(app, match, wcb), - (wcb) => notify_change_match_called_on_court(app, match, wcb), (wcb) => notify_bupws(app, match, old_court, wcb), + (wcb) => notify_change_match_called_on_court(app, match, wcb), (wcb) => set_player_on_court(app, tournament.key, match.setup, wcb), (wcb) => set_player_on_tablet(app, tournament.key, match.setup, wcb), (wcb) => update_btp_courts(app, tournament.key, match, wcb) @@ -115,6 +264,22 @@ function remove_called_timestamp(match, callback) { return callback(null); } +function normalize_preparation_state(setup) { + if (!setup) { + return setup; + } + if (Number(setup.highlight) > 0) { + return setup; + } + if (setup.state === 'preparation') { + setup.state = 'scheduled'; + } + if (setup.preparation_call_timestamp != null) { + setup.preparation_call_timestamp = undefined; + } + return setup; +} + function add_preparation_call_timestamp(db, tournament_key, setup, location_id) { return new Promise((resolve) => { const stournament = require('./stournament'); @@ -137,6 +302,490 @@ function add_preparation_call_timestamp(db, tournament_key, setup, location_id) }); } +function resolve_location_id_for_setup(app, tournament_key, setup, callback) { + if (setup && setup.location_id) { + return callback(null, setup.location_id); + } + if (!(setup && setup.court_id)) { + return callback(null, null); + } + app.db.courts.findOne({ tournament_key, _id: setup.court_id }, (err, court) => { + if (err) return callback(err); + return callback(null, court ? court.location_id : null); + }); +} + +function auto_execute_preparation_selection(app, tournament, location_id, callback) { + if (!tournament || !tournament.key || !location_id) { + return callback(null); + } + if (!is_tournament_automation_enabled(tournament)) { + return callback(null); + } + if (!tournament.call_preparation_matches_automatically_enabled) { + return callback(null); + } + + const match_automation = require('./match_automation'); + match_automation.fetch_location_preparation_selection(app, tournament.key, location_id) + .then((selection) => { + async.eachSeries(selection.selected_matches || [], (match, cb) => { + call_match_in_preparation(app, tournament, match, location_id, cb); + }, callback); + }) + .catch((err) => callback(err)); +} + +function auto_execute_preparation_selections(app, tournament, callback) { + if (!tournament || !tournament.key) { + return callback(null); + } + if (!is_tournament_automation_enabled(tournament)) { + return callback(null); + } + if (!tournament.call_preparation_matches_automatically_enabled) { + return callback(null); + } + + app.db.locations.find({ tournament_key: tournament.key }).sort({ name: 1 }).exec((err, locations) => { + if (err) { + return callback(err); + } + + async.eachSeries(locations || [], (location, cb) => { + if (!location || !location._id) { + return cb(null); + } + return auto_execute_preparation_selection(app, tournament, location._id, cb); + }, callback); + }); +} + +function auto_execute_preparation_selection_for_setup(app, tournament, setup, callback) { + if (!tournament || !tournament.key) { + return callback(null); + } + return auto_execute_preparation_selections(app, tournament, (err) => { + if (err) { + return callback(err); + } + return auto_assign_technical_officials_for_preparation_matches(app, tournament, callback); + }); +} + +function queue_auto_execute_preparation_selections(app, tournament_key, callback) { + if (!app || !app.db || !tournament_key) { + if (callback) { + callback(null); + } + return; + } + + const pending = pending_preparation_selection_runs.get(tournament_key); + if (pending) { + if (callback) { + pending.callbacks.push(callback); + } + return; + } + + const state = { + callbacks: callback ? [callback] : [], + }; + pending_preparation_selection_runs.set(tournament_key, state); + + update_queue.instance().execute(update_queue.named('auto_execute_preparation_selections', () => new Promise((resolve, reject) => { + app.db.tournaments.findOne({ key: tournament_key }, (err, tournament) => { + if (err) { + return reject(err); + } + if (!tournament) { + return reject(new Error('Cannot find tournament ' + tournament_key)); + } + return auto_execute_preparation_selections(app, tournament, (execErr) => { + if (execErr) { + return reject(execErr); + } + return resolve(true); + }); + }); + }))).then(() => { + const current = pending_preparation_selection_runs.get(tournament_key); + pending_preparation_selection_runs.delete(tournament_key); + queue_auto_assign_technical_officials_when_available(app, tournament_key); + (current?.callbacks || []).forEach((cb) => cb(null)); + }).catch((err) => { + const current = pending_preparation_selection_runs.get(tournament_key); + pending_preparation_selection_runs.delete(tournament_key); + (current?.callbacks || []).forEach((cb) => cb(err)); + console.warn('[bts] failed to queue auto_execute_preparation_selections', err && (err.stack || err.message || String(err))); + }); +} + +function technical_official_auto_assignment_mode_supports_preparation(mode) { + return mode === 'on_preparation_call' || mode === 'when_available'; +} + +function technical_official_auto_assignment_mode_supports_match_call(mode) { + return mode === 'on_match_call_if_possible' || mode === 'on_preparation_call' || mode === 'when_available'; +} + +function build_official_wait_reentry_set_obj(role, ts) { + const setObj = { + inactive_list: null, + service_judge_pause: null, + umpire_pause: null, + service_judge_manual_pause: null, + umpire_manual_pause: null, + service_judge_wait: null, + umpire_wait: null, + service_judge_on_court: null, + umpire_on_court: null, + is_planed_as_service_judge: false, + is_planed_as_umpire: false, + }; + if (role === 'umpire') { + setObj.umpire_wait = ts; + } else if (role === 'service_judge') { + setObj.service_judge_wait = ts; + } + return setObj; +} + +function drop_unsupported_court_officials(app, tournament, match, callback) { + if (!match?.setup?.court_id) { + return callback(null); + } + + app.db.courts.findOne({ tournament_key: tournament.key, _id: match.setup.court_id }, (courtErr, court) => { + if (courtErr) { + return callback(courtErr); + } + if (!court) { + return callback(null); + } + + const admin = require('./admin'); + const setup = match.setup || {}; + const releases = []; + if (court.has_umpire === false && setup.umpire) { + const official = setup.umpire; + admin._remove_official_from_setup(setup, 'umpire'); + if (official && official._id) { + releases.push({ + official_id: official._id, + role: 'umpire', + wait_ts: Number.isFinite(Number(official.umpire_wait)) ? Number(official.umpire_wait) : (Date.now() / 10), + }); + } + } + if (court.has_service_judge === false && setup.service_judge) { + const official = setup.service_judge; + admin._remove_official_from_setup(setup, 'service_judge'); + if (official && official._id) { + releases.push({ + official_id: official._id, + role: 'service_judge', + wait_ts: Number.isFinite(Number(official.service_judge_wait)) ? Number(official.service_judge_wait) : (Date.now() / 10), + }); + } + } + + if (releases.length === 0) { + return callback(null); + } + + match.btp_needsync = true; + async.eachSeries(releases, (release, cb) => { + app.db.umpires.update( + { _id: release.official_id, tournament_key: tournament.key }, + { $set: build_official_wait_reentry_set_obj(release.role, release.wait_ts) }, + {}, + cb + ); + }, (updateErr) => { + if (updateErr) { + return callback(updateErr); + } + app.db.umpires.find( + { tournament_key: tournament.key, _id: { $in: releases.map((release) => release.official_id) } }, + (findErr, updatedOfficials) => { + if (findErr) { + return callback(findErr); + } + app.db.umpires.find({ tournament_key: tournament.key }, (allErr, all_umpires) => { + if (allErr) { + return callback(allErr); + } + (updatedOfficials || []).forEach((official) => { + admin.notify_change(app, tournament.key, 'umpire_updated', official); + }); + admin.notify_change(app, tournament.key, 'umpires_changed', { all_umpires }); + return callback(null); + }); + } + ); + }); + }); +} + +function is_nonblocking_auto_assign_error(err) { + return !!(err && ( + /Match already has assigned umpire/.test(err.message) || + /No umpire available/.test(err.message) || + /Match has no assigned umpire/.test(err.message) || + /Match already has assigned service judge/.test(err.message) || + /No service judge available/.test(err.message) + )); +} + +function auto_assign_technical_officials_for_match(app, tournament, match_id, callback, options = {}) { + if (!is_tournament_automation_enabled(tournament)) { + return callback(null, false); + } + const mode = tournament.technical_official_auto_assignment_mode || 'manual_only'; + if (!technical_official_auto_assignment_mode_supports_match_call(mode)) { + return callback(null, false); + } + if ((tournament.official_rotation_mode || 'umpire_and_service_judge') === 'disabled') { + return callback(null, false); + } + if (mode === 'on_match_call_if_possible' && options.allow_on_match_call !== true) { + return callback(null, false); + } + + const admin = require('./admin'); + let changed = false; + const load_target_court = () => new Promise((resolve, reject) => { + if (!app?.db?.matches || typeof app.db.matches.findOne !== 'function') { + return resolve(null); + } + app.db.matches.findOne({ _id: match_id, tournament_key: tournament.key }, (matchErr, match) => { + if (matchErr) { + return reject(matchErr); + } + const court_id = match?.setup?.court_id; + if (!court_id) { + return resolve(null); + } + if (!app?.db?.courts || typeof app.db.courts.findOne !== 'function') { + return resolve(null); + } + app.db.courts.findOne({ tournament_key: tournament.key, _id: court_id }, (courtErr, court) => { + if (courtErr) { + return reject(courtErr); + } + return resolve(court || null); + }); + }); + }); + const try_auto_assign = (assign_fn) => { + return assign_fn(app, tournament.key, match_id, { skip_btp_push: true }) + .then(() => { + changed = true; + }) + .catch((err) => { + if (is_nonblocking_auto_assign_error(err)) { + return null; + } + throw err; + }); + }; + + Promise.resolve() + .then(() => load_target_court()) + .then((court) => { + if (court && court.has_umpire === false) { + return null; + } + return try_auto_assign(admin._assign_next_umpire_to_match).then(() => court); + }) + .then((court) => { + if ((tournament.official_rotation_mode || 'umpire_and_service_judge') !== 'umpire_and_service_judge') { + return null; + } + if (court && court.has_service_judge === false) { + return null; + } + return try_auto_assign(admin._assign_next_service_judge_to_match); + }) + .then(() => callback(null, changed)) + .catch((err) => callback(err)); +} + +function fetch_technical_official_assignment_targets(app, tournament, callback) { + if (!tournament || !tournament.key) { + return callback(null, []); + } + + app.db.matches + .find({ tournament_key: tournament.key, 'setup.state': 'preparation' }) + .sort({ 'setup.preparation_call_timestamp': 1 }) + .exec((err, preparation_matches) => { + if (err) { + return callback(err); + } + + const targets = []; + const seen_match_ids = new Set(); + (preparation_matches || []).forEach((match) => { + if (!match || !match._id || seen_match_ids.has(match._id)) { + return; + } + if (!match_needs_technical_official_assignment(match, tournament)) { + return; + } + seen_match_ids.add(match._id); + targets.push(match); + }); + + if ((tournament.technical_official_auto_assignment_mode || 'manual_only') !== 'when_available') { + return callback(null, targets); + } + + const match_automation = require('./match_automation'); + match_automation.fetch_all_location_preparation_selections(app, tournament.key, { + ignore_technical_officials_available_rule: true, + }) + .then((selections) => { + (selections || []).forEach((selection) => { + (selection.selected_matches || []).forEach((match) => { + if (!match || !match._id || seen_match_ids.has(match._id)) { + return; + } + if (!match_needs_technical_official_assignment(match, tournament)) { + return; + } + seen_match_ids.add(match._id); + targets.push(match); + }); + }); + + app.db.umpires.find({ tournament_key: tournament.key, umpire_wait: { $ne: null } }, (umpireErr, waiting_umpires) => { + if (umpireErr) { + return callback(umpireErr); + } + const available_umpire_count = Array.isArray(waiting_umpires) ? waiting_umpires.length : 0; + if (available_umpire_count <= targets.length) { + return callback(null, targets); + } + + match_automation.fetch_global_preparation_candidates(app, tournament.key, { + ignore_technical_officials_available_rule: true, + }) + .then((global_candidates) => { + (global_candidates || []).forEach((match) => { + if (!match || !match._id || seen_match_ids.has(match._id)) { + return; + } + if (targets.length >= available_umpire_count) { + return; + } + if (!match_needs_technical_official_assignment(match, tournament)) { + return; + } + seen_match_ids.add(match._id); + targets.push(match); + }); + callback(null, targets); + }) + .catch((globalErr) => callback(globalErr)); + }); + }) + .catch((selectionErr) => callback(selectionErr)); + }); +} + +function auto_assign_technical_officials_for_preparation_matches(app, tournament, callback) { + if (!tournament || !tournament.key) { + return callback(null); + } + if (!is_tournament_automation_enabled(tournament)) { + return callback(null); + } + if ((tournament.technical_official_auto_assignment_mode || 'manual_only') !== 'when_available') { + return callback(null); + } + if ((tournament.official_rotation_mode || 'umpire_and_service_judge') === 'disabled') { + return callback(null); + } + + fetch_technical_official_assignment_targets(app, tournament, (targetErr, matches) => { + if (targetErr) { + return callback(targetErr); + } + + const btp_manager = require('./btp_manager'); + async.eachSeries(matches || [], (match, cb) => { + if (!match || !match._id) { + return cb(null); + } + return auto_assign_technical_officials_for_match(app, tournament, match._id, (assignErr, changed) => { + if (assignErr) { + return cb(assignErr); + } + if (!changed) { + return cb(null); + } + fetch_match(app, tournament.key, match._id) + .then((updatedMatch) => { + btp_manager.update_highlight(app, updatedMatch); + cb(null); + }) + .catch(cb); + }); + }, callback); + }); +} + +function queue_auto_assign_technical_officials_when_available(app, tournament_key, callback) { + if (!app || !app.db || !tournament_key) { + if (callback) { + callback(null); + } + return; + } + + const pending = pending_technical_official_assignment_runs.get(tournament_key); + if (pending) { + if (callback) { + pending.callbacks.push(callback); + } + return; + } + + const state = { + callbacks: callback ? [callback] : [], + }; + pending_technical_official_assignment_runs.set(tournament_key, state); + + update_queue.instance().execute(update_queue.named('auto_assign_technical_officials_when_available', () => new Promise((resolve, reject) => { + app.db.tournaments.findOne({ key: tournament_key }, (err, tournament) => { + if (err) { + return reject(err); + } + if (!tournament) { + return reject(new Error('Cannot find tournament ' + tournament_key)); + } + return auto_assign_technical_officials_for_preparation_matches(app, tournament, (execErr) => { + if (execErr) { + return reject(execErr); + } + return resolve(true); + }); + }); + }))).then(() => { + const current = pending_technical_official_assignment_runs.get(tournament_key); + pending_technical_official_assignment_runs.delete(tournament_key); + (current?.callbacks || []).forEach((cb) => cb(null)); + }).catch((err) => { + const current = pending_technical_official_assignment_runs.get(tournament_key); + pending_technical_official_assignment_runs.delete(tournament_key); + (current?.callbacks || []).forEach((cb) => cb(err)); + console.warn('[bts] failed to queue auto_assign_technical_officials_when_available', err && (err.stack || err.message || String(err))); + }); +} + function remove_preparation_call_timestamp(setup) { setup.preparation_call_timestamp = undefined; } @@ -163,7 +812,7 @@ async function add_tabletoperators(app, tournament, match, callback) { const setup = match.setup; try { - if ((tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { + if (is_tournament_automation_enabled(tournament) && (tournament.tabletoperator_enabled && tournament.tabletoperator_enabled == true)) { if (!setup.tabletoperators || setup.tabletoperators == null) { const fetch_result = await fetch_tabletoperator(admin, app, tournament.key, court_id); @@ -218,6 +867,7 @@ async function set_umpires_on_court(app, tournament, match, callback) { umpire.last_time_on_court_ts = setup.called_timestamp; umpire.status = 'oncourt'; umpire.court_id = court_id; + sync_technical_official_checked_in(umpire, tournament); update_umpire(app, tournament.key, umpire); } @@ -236,6 +886,7 @@ async function set_umpires_on_court(app, tournament, match, callback) { service_judge.last_time_on_court_ts = setup.called_timestamp; service_judge.status = 'oncourt'; service_judge.court_id = court_id; + sync_technical_official_checked_in(service_judge, tournament); update_umpire(app, tournament.key, service_judge); } @@ -245,6 +896,7 @@ async function set_umpires_on_court(app, tournament, match, callback) { function remove_highlight_preparation(match, callback){ const setup = match.setup; setup.highlight = 0; + normalize_preparation_state(setup); return callback(null); } @@ -308,6 +960,16 @@ function notify_change_match_called_on_court (app, match, callback) { function notify_bupws(app, match, old_court, callback) { const bupws = require('./bupws'); + console.log('[bts] auto_call_trace:notify_bupws', { + ts: Date.now(), + tournament_key: match && match.tournament_key, + match_id: match && match._id, + court_id: match && match.setup && match.setup.court_id, + old_court, + state: match && match.setup && match.setup.state, + now_on_court: match && match.setup && match.setup.now_on_court, + called_timestamp: match && match.setup && match.setup.called_timestamp, + }); bupws.handle_score_change(app, match.tournament_key, match.setup.court_id); @@ -884,78 +1546,172 @@ function reset_tabletoperator_settings_at_player(app, tkey, tournament, player, } } +function apply_official_on_court_release(official, role, end_ts, options = {}) { + if (!official) { + return official; + } + const official_rotation_mode = options.official_rotation_mode || 'umpire_and_service_judge'; + const technical_break_ms = Number(options.technical_official_break_after_assignment_ms) || 0; + const use_break = technical_break_ms > 0; + const set_pause_or_wait = (pause_field, wait_field, ts, pause_ts) => { + official.status = use_break ? 'pause' : 'ready'; + official[pause_field] = use_break ? pause_ts : null; + official[wait_field] = use_break ? null : ts; + }; + + official.umpire_on_court = null; + official.service_judge_on_court = null; + official.is_planed_as_umpire = false; + official.is_planed_as_service_judge = false; + official.last_time_on_court_ts = end_ts; + official.checked_in = false; + official.status = 'ready'; + official.court_id = null; + official.umpire_wait = null; + official.service_judge_wait = null; + official.umpire_pause = null; + official.service_judge_pause = null; + official.umpire_manual_pause = null; + official.service_judge_manual_pause = null; + official.inactive_list = null; + + if (official_rotation_mode === 'umpire_only') { + if (official.is_umpire === true || official.is_service_judge === true) { + set_pause_or_wait('umpire_pause', 'umpire_wait', end_ts, end_ts + technical_break_ms); + } else { + // Be tolerant for legacy/inconsistent role flags and keep the official in rotation. + set_pause_or_wait('umpire_pause', 'umpire_wait', end_ts, end_ts + technical_break_ms); + } + return official; + } + + if (role === 'umpire') { + if (official.is_service_judge === true) { + set_pause_or_wait('service_judge_pause', 'service_judge_wait', end_ts, end_ts + technical_break_ms); + } else if (official.is_umpire === true) { + set_pause_or_wait('umpire_pause', 'umpire_wait', end_ts + 100, end_ts + technical_break_ms + 100); + } else { + // Fall back to the role actually performed if role flags are missing. + set_pause_or_wait('umpire_pause', 'umpire_wait', end_ts + 100, end_ts + technical_break_ms + 100); + } + } else if (role === 'service_judge') { + if (official.is_umpire === true) { + set_pause_or_wait('umpire_pause', 'umpire_wait', end_ts, end_ts + technical_break_ms); + } else if (official.is_service_judge === true) { + set_pause_or_wait('service_judge_pause', 'service_judge_wait', end_ts + 100, end_ts + technical_break_ms + 100); + } else { + // Fall back to the role actually performed if role flags are missing. + set_pause_or_wait('service_judge_pause', 'service_judge_wait', end_ts + 100, end_ts + technical_break_ms + 100); + } + } + + sync_technical_official_checked_in(official, options.tournament || options.btp_settings); + return official; +} + +function apply_official_pause_expiry(official, options = {}) { + if (!official) { + return official; + } + const has_umpire_pause = official.umpire_pause != null; + const has_service_judge_pause = official.service_judge_pause != null; + const umpire_pause = has_umpire_pause ? Number(official.umpire_pause) : null; + const service_judge_pause = has_service_judge_pause ? Number(official.service_judge_pause) : null; + + if (!has_umpire_pause && !has_service_judge_pause) { + return official; + } + + official.status = 'ready'; + official.umpire_wait = null; + official.service_judge_wait = null; + official.umpire_pause = null; + official.service_judge_pause = null; + official.umpire_manual_pause = null; + official.service_judge_manual_pause = null; + official.inactive_list = null; + + if (has_umpire_pause && (!has_service_judge_pause || umpire_pause <= service_judge_pause)) { + official.umpire_wait = umpire_pause; + } else { + official.service_judge_wait = service_judge_pause; + } + + sync_technical_official_checked_in(official, options.tournament || options.btp_settings); + return official; +} + +function apply_official_standby_state(official, options = {}) { + if (!official) { + return official; + } + + official.umpire_on_court = null; + official.service_judge_on_court = null; + official.is_planed_as_umpire = false; + official.is_planed_as_service_judge = false; + official.umpire_wait = null; + official.service_judge_wait = null; + official.umpire_pause = null; + official.service_judge_pause = null; + official.umpire_manual_pause = null; + official.service_judge_manual_pause = null; + official.inactive_list = null; + official.last_time_on_court_ts = null; + official.status = 'standby'; + official.court_id = null; + + sync_technical_official_checked_in(official, options.tournament || options.btp_settings); + return official; +} + async function remove_umpire_on_court(app, tournament_key, cur_match_id, end_ts, callback) { + app.db.tournaments.findOne({ key: tournament_key }, (tournament_err, tournament) => { + if (tournament_err) { + return callback(tournament_err); + } + const official_rotation_mode = tournament?.official_rotation_mode || 'umpire_and_service_judge'; + const technical_official_break_after_assignment_ms = get_technical_official_break_after_assignment_ms(tournament); + app.db.matches.findOne({ 'tournament_key': tournament_key, '_id': cur_match_id }, (err, cur_match) => { if (err) { return reject(err); } if (cur_match.setup.umpire) { - const umpire = cur_match.setup.umpire; - umpire.umpire_on_court = null; - umpire.service_judge_on_court = null; - umpire.is_planed_as_umpire = false; - umpire.last_time_on_court_ts = end_ts; - umpire.checked_in = false; - umpire.status = 'ready'; - umpire.court_id = null; - - if(umpire.is_service_judge === true) { - umpire.service_judge_wait = end_ts; - } else if(umpire.is_umpire === true) { - umpire.umpire_wait = end_ts + 100; - } else { - umpire.inactive_list = end_ts; - } - + const umpire = apply_official_on_court_release(cur_match.setup.umpire, 'umpire', end_ts, { + tournament, + official_rotation_mode, + technical_official_break_after_assignment_ms, + }); update_umpire(app, tournament_key, umpire, 'ready', end_ts, null); } if (cur_match.setup.service_judge) { - const service_judge = cur_match.setup.service_judge; - service_judge.umpire_on_court = null; - service_judge.service_judge_on_court = null; - service_judge.is_planed_as_umpire = false; - service_judge.last_time_on_court_ts = end_ts; - service_judge.checked_in = false; - service_judge.status = 'ready'; - service_judge.court_id = null; - - if(service_judge.is_umpire === true) { - service_judge.umpire_wait = end_ts; - } else if(service_judge.is_service_judge === true) { - service_judge.service_judge_wait = end_ts + 100; - } else { - service_judge.inactive_list = end_ts + 50; - } - + const service_judge = apply_official_on_court_release(cur_match.setup.service_judge, 'service_judge', end_ts, { + tournament, + official_rotation_mode, + technical_official_break_after_assignment_ms, + }); update_umpire(app, tournament_key, service_judge); } return callback(null); }); + }); } function set_umpire_to_standby(app, tournament_key, setup) { - if (setup.umpire) { - const umpire = setup.umpire; - umpire.umpire_on_court = null; - umpire.service_judge_on_court = null; - umpire.is_planed_as_umpire = false; - umpire.last_time_on_court_ts = null; - umpire.status = 'standby'; - umpire.court_id = null; - update_umpire(app, tournament_key, umpire); - } + app.db.tournaments.findOne({ key: tournament_key }, (tournament_err, tournament) => { + const standby_options = tournament_err || !tournament ? {} : { tournament }; + if (setup.umpire) { + const umpire = apply_official_standby_state(setup.umpire, standby_options); + update_umpire(app, tournament_key, umpire); + } - if (setup.service_judge) { - const service_judge = setup.service_judge; - service_judge.umpire_on_court = null; - service_judge.service_judge_on_court = null; - service_judge.is_planed_as_umpire = false; - service_judge.last_time_on_court_ts = null; - service_judge.status = 'standby'; - service_judge.court_id = null; - update_umpire(app, tournament_key, service_judge); - } + if (setup.service_judge) { + const service_judge = apply_official_standby_state(setup.service_judge, standby_options); + update_umpire(app, tournament_key, service_judge); + } + }); } @@ -981,45 +1737,265 @@ function update_umpire(app, tkey, umpire) { const admin = require('./admin'); admin.notify_change(app, tkey, 'umpire_updated', changed_umpire); + const official_is_waiting = + changed_umpire && + (changed_umpire.umpire_wait != null || changed_umpire.service_judge_wait != null); + + if (official_is_waiting) { + queue_auto_execute_preparation_selections(app, tkey); + } else { + queue_auto_assign_technical_officials_when_available(app, tkey); + } } ); } +function process_expired_technical_official_breaks_for_tournament(app, tournament, callback) { + if (!app || !app.db || !tournament || !tournament.key) { + return callback(null); + } + if ((tournament.official_rotation_mode || 'umpire_and_service_judge') === 'disabled') { + return callback(null); + } + const pause_ms = get_technical_official_break_after_assignment_ms(tournament); + const now = Date.now(); + const query = pause_ms > 0 + ? { + tournament_key: tournament.key, + $or: [ + { umpire_pause: { $ne: null, $lte: now } }, + { service_judge_pause: { $ne: null, $lte: now } }, + ] + } + : { + tournament_key: tournament.key, + $or: [ + { umpire_pause: { $ne: null } }, + { service_judge_pause: { $ne: null } }, + ] + }; + app.db.umpires.find(query, (err, officials) => { + if (err) { + return callback(err); + } + const sorted_officials = (officials || []).sort((a, b) => { + const a_ts = Math.min( + Number.isFinite(Number(a?.umpire_pause)) ? Number(a.umpire_pause) : Number.POSITIVE_INFINITY, + Number.isFinite(Number(a?.service_judge_pause)) ? Number(a.service_judge_pause) : Number.POSITIVE_INFINITY + ); + const b_ts = Math.min( + Number.isFinite(Number(b?.umpire_pause)) ? Number(b.umpire_pause) : Number.POSITIVE_INFINITY, + Number.isFinite(Number(b?.service_judge_pause)) ? Number(b.service_judge_pause) : Number.POSITIVE_INFINITY + ); + return a_ts - b_ts; + }); + + async.eachSeries(sorted_officials, (official, cb) => { + update_umpire(app, tournament.key, apply_official_pause_expiry(official, { tournament })); + cb(null); + }, callback); + }); +} + +function queue_process_expired_technical_official_breaks(app, tournament_key, callback) { + if (!app || !app.db || !tournament_key) { + if (callback) callback(null); + return; + } + + const pending = pending_technical_official_pause_runs.get(tournament_key); + if (pending) { + if (callback) { + pending.callbacks.push(callback); + } + return; + } + + const state = { + callbacks: callback ? [callback] : [], + }; + pending_technical_official_pause_runs.set(tournament_key, state); + + update_queue.instance().execute( + update_queue.named(`technical_official_pause_expiry_${tournament_key}`, () => new Promise((resolve, reject) => { + app.db.tournaments.findOne({ key: tournament_key }, (err, tournament) => { + if (err || !tournament) { + reject(err || new Error('tournament not found')); + return; + } + process_expired_technical_official_breaks_for_tournament(app, tournament, (process_err) => { + if (process_err) { + reject(process_err); + return; + } + resolve(null); + }); + }); + })) + ).then(() => { + const current = pending_technical_official_pause_runs.get(tournament_key); + pending_technical_official_pause_runs.delete(tournament_key); + (current?.callbacks || []).forEach((cb) => cb(null)); + }).catch((err) => { + const current = pending_technical_official_pause_runs.get(tournament_key); + pending_technical_official_pause_runs.delete(tournament_key); + (current?.callbacks || []).forEach((cb) => cb(err)); + }); +} + +function start_technical_official_pause_manager(app) { + if (technical_official_pause_interval || !app || !app.db) { + return; + } + technical_official_pause_interval = setInterval(() => { + app.db.tournaments.find({ + official_rotation_mode: { $ne: 'disabled' }, + technical_official_break_after_assignment_seconds: { $gt: 0 }, + }, (err, tournaments) => { + if (err) { + return; + } + (tournaments || []).forEach((tournament) => { + queue_process_expired_technical_official_breaks(app, tournament.key); + }); + }); + }, 1000); +} + function call_preparation_match_on_court(app, tournament_key, court_id) { return new Promise((resolve, reject) => { + console.log('[bts] auto_call_trace:call_preparation_match_on_court_start', { + ts: Date.now(), + tournament_key, + court_id, + }); app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { if (err) { return reject("No tournament found for "); } - if (tournament.call_preparation_matches_automatically_enabled) { - const match_querry = { 'tournament_key': tournament_key, 'setup.highlight': 6 }; - app.db.matches.find(match_querry).sort({ 'setup.preparation_call_timestamp': 1 }).exec((err, matches) => { - if (err) { - return reject(msg, err); - } - if (matches && matches.length > 0) { - const next_match = matches[0]; - next_match.setup.court_id = court_id; - next_match.setup.now_on_court = true; - call_match(app, tournament, next_match, undefined, (err) => { - if (err) { - return reject(err); - } else { - return resolve(next_match); - } + if (!is_tournament_automation_enabled(tournament)) { + return resolve("Global automation disabled"); + } + if (tournament.call_next_possible_scheduled_match_in_preparation) { + const match_automation = require('./match_automation'); + Promise.all([ + app.db.courts.find_async({ tournament_key }), + app.db.matches.find_async({ tournament_key }), + app.db.umpires.find_async({ tournament_key }), + ]).then(([courts, matches, umpires]) => { + const current_tournament = { + ...(tournament || {}), + courts, + matches, + umpires, + }; + const candidates = match_automation.find_call_on_court_candidates(current_tournament, court_id); + if (!candidates || candidates.length === 0) { + console.log('[bts] auto_call_trace:call_preparation_match_on_court_no_candidate', { + ts: Date.now(), + tournament_key, + court_id, }); - - } else { return reject("No match found to call on court."); } - }); + const next_match = candidates[0]; + console.log('[bts] auto_call_trace:call_preparation_match_on_court_candidate', { + ts: Date.now(), + tournament_key, + court_id, + match_id: next_match && next_match._id, + state: next_match && next_match.setup && next_match.setup.state, + highlight: next_match && next_match.setup && next_match.setup.highlight, + location_id: next_match && next_match.setup && next_match.setup.location_id, + candidate_count: candidates.length, + }); + next_match.setup.court_id = court_id; + next_match.setup.now_on_court = true; + call_match(app, tournament, next_match, undefined, (callErr) => { + if (callErr) { + return reject(callErr); + } + return resolve(next_match); + }); + }).catch((queryErr) => reject(queryErr)); } else { - return resolve("Function call_preparation_matches_automatically_enabled disabled"); + return resolve("Function call_next_possible_scheduled_match_in_preparation disabled"); } }); }); } +function auto_call_matches_on_free_courts(app, tournament_key, callback) { + if (!app || !app.db || !tournament_key) { + return callback ? callback(null) : null; + } + + app.db.tournaments.findOne({ key: tournament_key }, (tournamentErr, tournament) => { + if (tournamentErr) { + return callback ? callback(tournamentErr) : null; + } + + app.db.courts.find({ tournament_key, is_active: true }, (err, courts) => { + if (err) { + return callback ? callback(err) : null; + } + + app.db.matches.find({ tournament_key, 'setup.now_on_court': true }, (matchesErr, on_court_matches) => { + if (matchesErr) { + return callback ? callback(matchesErr) : null; + } + + const continue_with_tabletoperators = (tabletoperators) => { + const occupied_court_ids = new Set( + (on_court_matches || []) + .filter((match) => match?.setup?.court_id) + .map((match) => match.setup.court_id) + ); + const free_active_courts = sort_free_courts_for_auto_call( + (courts || []).filter((court) => court && !occupied_court_ids.has(court._id)), + tabletoperators, + tournament + ); + console.log('[bts] auto_call_trace:auto_call_matches_on_free_courts', { + ts: Date.now(), + tournament_key, + active_court_ids: (courts || []).filter((court) => court && court.is_active).map((court) => court._id), + occupied_court_ids: Array.from(occupied_court_ids), + free_active_court_ids: free_active_courts.map((court) => court._id), + }); + + async.eachSeries(free_active_courts, (court, cb) => { + call_preparation_match_on_court(app, tournament_key, court._id) + .then(() => cb(null)) + .catch((callErr) => { + const message = callErr && (callErr.message || String(callErr)); + if (/No match found to call on court/.test(message)) { + return cb(null); + } + return cb(callErr); + }); + }, (finalErr) => { + if (callback) { + callback(finalErr); + } + }); + }; + + if (tournament?.tabletoperator_enabled !== true) { + return continue_with_tabletoperators([]); + } + + app.db.tabletoperators.find({ tournament_key, court: null }, (tabletErr, tabletoperators) => { + if (tabletErr) { + return callback ? callback(tabletErr) : null; + } + return continue_with_tabletoperators(tabletoperators || []); + }); + }); + }); + }); +} + async function call_next_possible_match_for_preparation(app, tournament_key, callback) { app.db.tournaments.findOne({ key: tournament_key }, async (err, tournament) => { if (err) { @@ -1085,55 +2061,105 @@ async function call_next_possible_match_for_preparation(app, tournament_key, cal } -async function call_match_in_preparation(app, tournament, match, location_id, callback) { +async function call_match_in_preparation(app, tournament, match, location_id, callback, options = {}) { const tournament_key = tournament.key; const admin = require('./admin'); const setup = match.setup; const match_id = match._id; + const force = options && options.force === true; - await add_preparation_call_timestamp(app.db, tournament_key, setup, location_id); + app.db.matches.findOne({ _id: match_id, tournament_key }, async (findErr, current_match) => { + if (findErr) { + return callback(findErr); + } + if (!current_match) { + return callback(new Error('Cannot find match ' + match_id + ' of tournament ' + tournament_key + ' in database')); + } + if ( + current_match.setup && + current_match.setup.state === 'preparation' && + Number(current_match.setup.highlight) > 0 + ) { + return callback(null); + } - if (tournament.preparation_tabletoperator_setup_enabled) { - if (!setup.umpire || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { - if (!setup.tabletoperators || setup.tabletoperators == null) { - const fetch_result = await fetch_tabletoperator(admin, app, tournament.key, "prep_call"); - let value = []; - if (tournament.tabletoperator_with_state_from_match_enabled && typeof(fetch_result) == "undefined") { - value.push({ - asian_name: false, - name: setup.teams[0].players[0].state, - firstname: "", - lastname: "", - btp_id: -1}); - } else { - value = fetch_result; + if (!force) { + try { + const match_automation = require('./match_automation'); + const [courts, matches, umpires] = await Promise.all([ + app.db.courts.find_async({ tournament_key }), + app.db.matches.find_async({ tournament_key }), + app.db.umpires.find_async({ tournament_key }), + ]); + const current_tournament = { + ...(tournament || {}), + courts, + matches, + umpires, + }; + if (!match_automation.is_match_eligible_for_preparation(current_match, location_id, current_tournament)) { + return callback(null); } - - if (!setup.umpire || !setup.umpire.name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { - setup.tabletoperators = value; - } + } catch (eligibilityErr) { + return callback(eligibilityErr); } } - } - set_umpire_to_standby(app, tournament_key, setup); - app.db.matches.update({ _id: match_id, tournament_key }, { $set: { setup } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_match) { - if (err) { - return callback(err); - } - if (numAffected !== 1) { - return callback(new Error('Cannot find match ' + match_id + ' of tournament ' + tournament_key + ' in database')); - } - if (changed_match._id !== match_id) { - const errmsg = 'Match ' + changed_match._id + ' changed by accident, intended to change ' + match_id + ' (old nedb version?)'; - serror.silent(errmsg); - - return callback(new Error(errmsg)); + await add_preparation_call_timestamp(app.db, tournament_key, setup, location_id); + + if (is_tournament_automation_enabled(tournament) && tournament.preparation_tabletoperator_setup_enabled) { + if (!setup.umpire || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { + if (!setup.tabletoperators || setup.tabletoperators == null) { + const fetch_result = await fetch_tabletoperator(admin, app, tournament.key, "prep_call"); + let value = []; + if (tournament.tabletoperator_with_state_from_match_enabled && typeof(fetch_result) == "undefined") { + value.push({ + asian_name: false, + name: setup.teams[0].players[0].state, + firstname: "", + lastname: "", + btp_id: -1}); + } else { + value = fetch_result; + } + + if (!setup.umpire || !setup.umpire.name || (tournament.tabletoperator_with_umpire_enabled && tournament.tabletoperator_with_umpire_enabled == true)) { + setup.tabletoperators = value; + } + } + } } - admin.notify_change(app, tournament_key, 'match_preparation_call', { match__id: match_id, match: changed_match}); - const btp_manager = require('./btp_manager'); - btp_manager.update_highlight(app, changed_match); - return callback (null); + set_umpire_to_standby(app, tournament_key, setup); + + app.db.matches.update({ _id: match_id, tournament_key }, { $set: { setup } }, { returnUpdatedDocs: true }, function (err, numAffected, changed_match) { + if (err) { + return callback(err); + } + if (numAffected !== 1) { + return callback(new Error('Cannot find match ' + match_id + ' of tournament ' + tournament_key + ' in database')); + } + if (changed_match._id !== match_id) { + const errmsg = 'Match ' + changed_match._id + ' changed by accident, intended to change ' + match_id + ' (old nedb version?)'; + serror.silent(errmsg); + + return callback(new Error(errmsg)); + } + return auto_assign_technical_officials_for_match(app, tournament, match_id, (assignErr) => { + if (assignErr) { + return callback(assignErr); + } + app.db.matches.findOne({ _id: match_id, tournament_key }, (latestErr, latest_match) => { + if (latestErr) { + return callback(latestErr); + } + const final_match = latest_match || changed_match; + admin.notify_change(app, tournament_key, 'match_preparation_call', { match__id: match_id, match: final_match}); + const btp_manager = require('./btp_manager'); + btp_manager.update_highlight(app, final_match); + return callback(null); + }); + }); + }); }); } @@ -1217,8 +2243,31 @@ module.exports ={ set_umpire_to_standby, add_preparation_call_timestamp, remove_preparation_call_timestamp, + normalize_preparation_state, reset_player_tabletoperator, + apply_official_on_court_release, + apply_official_pause_expiry, + apply_official_standby_state, + auto_execute_preparation_selection, + auto_execute_preparation_selections, + auto_execute_preparation_selection_for_setup, + queue_auto_execute_preparation_selections, + technical_official_auto_assignment_mode_supports_preparation, + auto_assign_technical_officials_for_match, + fetch_technical_official_assignment_targets, + auto_assign_technical_officials_for_preparation_matches, + queue_auto_assign_technical_officials_when_available, + get_technical_official_break_after_assignment_ms, + process_expired_technical_official_breaks_for_tournament, + queue_process_expired_technical_official_breaks, + start_technical_official_pause_manager, + auto_call_matches_on_free_courts, call_preparation_match_on_court, call_next_possible_match_for_preparation, - call_match_in_preparation + call_match_in_preparation, + sort_free_courts_for_auto_call, + is_tournament_automation_enabled, + is_technical_official_unavailable, + get_effective_technical_official_checked_in, + sync_technical_official_checked_in }; diff --git a/bts/update_queue.js b/bts/update_queue.js index eb7def0..6977dc4 100644 --- a/bts/update_queue.js +++ b/bts/update_queue.js @@ -20,8 +20,19 @@ class update_queue { return task.name && task.name.length > 0 ? task.name : ''; } - _start_watchdog(task_name) { + _task_hang_after_ms(task) { + if (!task) { + return 5000; + } + if (typeof task._queue_hang_after_ms === 'number' && task._queue_hang_after_ms > 0) { + return task._queue_hang_after_ms; + } + return 5000; + } + + _start_watchdog(task, task_name) { this._clear_watchdog(); + const hang_after_ms = this._task_hang_after_ms(task); this.current_task_watchdog = setTimeout(() => { const runtime_ms = this.current_task_started_at ? (Date.now() - this.current_task_started_at) : null; const payload = { @@ -37,7 +48,7 @@ class update_queue { console.warn('[bts] update_queue:hang_reporter_error', err && (err.stack || err.message || String(err))); } } - }, 5000); + }, hang_after_ms); } _clear_watchdog() { @@ -57,7 +68,7 @@ class update_queue { const task_name = this._task_name(task); this.current_task = task_name; this.current_task_started_at = Date.now(); - this._start_watchdog(task_name); + this._start_watchdog(task, task_name); try { const res = await task(...args); this._clear_watchdog(); @@ -96,7 +107,13 @@ function named(name, task) { return task; } +function hang_after(ms, task) { + task._queue_hang_after_ms = ms; + return task; +} + module.exports = { instance, - named + named, + hang_after }; diff --git a/test/test_bupws.js b/test/test_bupws.js new file mode 100644 index 0000000..2df9715 --- /dev/null +++ b/test/test_bupws.js @@ -0,0 +1,94 @@ +'use strict'; + +const assert = require('assert'); + +const {_describe, _it} = require('./tutils.js'); + +const admin = require('../bts/admin.js'); +const bupws = require('../bts/bupws.js'); + +_describe('bupws', () => { + _it('clears the stored court match reference when the finished match still owns the court', (done) => { + const notifications = []; + const original_notify_change = admin.notify_change; + admin.notify_change = (_app, _tournament_key, ctype, payload) => { + notifications.push({ ctype, payload }); + }; + + const app = { + db: { + courts: { + update(query, update, options, cb) { + assert.deepStrictEqual(query, { tournament_key: 'default', _id: 'court-1' }); + assert.deepStrictEqual(update, { $set: { match_id: null } }); + assert.deepStrictEqual(options, { returnUpdatedDocs: true }); + cb(null, 1, { + _id: 'court-1', + is_active: true, + has_umpire: true, + has_service_judge: false, + match_id: null, + }); + } + } + } + }; + + bupws._clear_court_match_reference_after_finish( + app, + 'default', + { tournament_key: 'default', _id: 'court-1' }, + { _id: 'court-1', match_id: 'match-1', is_active: true, has_umpire: true, has_service_judge: false }, + 'match-1', + true, + (err, changed) => { + admin.notify_change = original_notify_change; + assert.ifError(err); + assert.strictEqual(changed, true); + assert.deepStrictEqual(notifications, [{ + ctype: 'court_changed', + payload: { + court_id: 'court-1', + is_active: true, + has_umpire: true, + has_service_judge: false, + match_id: null, + } + }]); + done(); + } + ); + }); + + _it('does not clear the court match reference for unrelated finishes', (done) => { + const original_notify_change = admin.notify_change; + admin.notify_change = () => { + throw new Error('notify_change must not be called'); + }; + + const app = { + db: { + courts: { + update() { + throw new Error('court update must not be called'); + } + } + } + }; + + bupws._clear_court_match_reference_after_finish( + app, + 'default', + { tournament_key: 'default', _id: 'court-1' }, + { _id: 'court-1', match_id: 'other-match' }, + 'match-1', + true, + (err, changed) => { + admin.notify_change = original_notify_change; + assert.ifError(err); + assert.strictEqual(changed, false); + done(); + } + ); + }); +}); diff --git a/test/test_match_automation.js b/test/test_match_automation.js new file mode 100644 index 0000000..db22416 --- /dev/null +++ b/test/test_match_automation.js @@ -0,0 +1,1786 @@ +'use strict'; + +const assert = require('assert'); + +const {_describe, _it} = require('./tutils.js'); + +const match_automation = require('../bts/match_automation.js'); + +function make_scoring_format(overrides = {}) { + return { + numSets: 3, + set_points: { + end_points: 21, + max_points: 30, + }, + last_set_points: { + end_points: 21, + max_points: 30, + }, + ...overrides, + }; +} + +function make_match({ network_score, now_on_court = true, team1_won = null } = {}) { + return { + network_score, + team1_won, + setup: { + now_on_court, + }, + }; +} + +function make_preparation_match(overrides = {}) { + const setup_overrides = overrides.setup || {}; + const match = { + _id: overrides._id || 'm1', + match_order: overrides.match_order, + team1_won: overrides.team1_won ?? null, + setup: { + state: 'scheduled', + is_match: true, + incomplete: false, + match_num: 1, + scheduled_date: '2026-04-07', + scheduled_time_str: '10:00', + location_id: null, + court_id: null, + teams: [ + { players: [{ _id: 'p1' }] }, + { players: [{ _id: 'p2' }] }, + ], + ...setup_overrides, + }, + }; + if (overrides.btp_match_ids) { + match.btp_match_ids = overrides.btp_match_ids; + } + if (overrides.btp_player_ids) { + match.btp_player_ids = overrides.btp_player_ids; + } + return match; +} + +function make_court(overrides = {}) { + return { + _id: overrides._id || 'c1', + location_id: overrides.location_id || 'l1', + is_active: overrides.is_active !== false, + match_id: overrides.match_id ?? null, + ...overrides, + }; +} + +function make_location(overrides = {}) { + return { + _id: overrides._id || 'l1', + name: overrides.name || 'Location 1', + ...overrides, + }; +} + +function make_tournament(overrides = {}) { + return { + key: overrides.key || 't1', + courts: overrides.courts || [], + matches: overrides.matches || [], + locations: overrides.locations || [], + umpires: overrides.umpires || [], + btp_settings: overrides.btp_settings || {}, + ...overrides, + }; +} + +function make_official(overrides = {}) { + return { + _id: overrides._id || 'o1', + umpire_wait: overrides.umpire_wait ?? null, + service_judge_wait: overrides.service_judge_wait ?? null, + ...overrides, + }; +} + +function make_realistic_location_fixture() { + const location = make_location({ _id: 'l-hall', name: 'BTS Halle' }); + const courts = [ + make_court({ _id: 'c-free', location_id: location._id, is_active: true, match_id: null }), + make_court({ _id: 'c-on', location_id: location._id, is_active: true, match_id: 'm-live' }), + make_court({ _id: 'c-off', location_id: location._id, is_active: false, match_id: 'm-disabled-live' }), + ]; + const matches = [ + make_preparation_match({ + _id: 'm-live', + setup: { + state: 'oncourt', + now_on_court: true, + court_id: 'c-on', + match_num: 10, + scheduled_time_str: '10:00', + needs_preparation_successor: true, + }, + }), + make_preparation_match({ + _id: 'm-disabled-live', + setup: { + state: 'oncourt', + now_on_court: true, + court_id: 'c-off', + match_num: 11, + scheduled_time_str: '10:00', + needs_preparation_successor: true, + }, + }), + make_preparation_match({ + _id: 'm-prepared', + setup: { + state: 'preparation', + location_id: location._id, + match_num: 12, + scheduled_time_str: '10:15', + }, + }), + make_preparation_match({ + _id: 'm-eligible-1', + match_order: 1, + setup: { + match_num: 13, + location_id: location._id, + scheduled_time_str: '10:30', + }, + }), + make_preparation_match({ + _id: 'm-eligible-2', + match_order: 2, + setup: { + match_num: 14, + location_id: location._id, + scheduled_time_str: '10:45', + }, + }), + ]; + + return make_tournament({ + call_preparation_matches_automatically_enabled: true, + locations: [location], + courts, + matches, + }); +} + +_describe('match automation', () => { + _it('returns false when no side leads in the current set', () => { + const match = make_match({ + network_score: [[10, 10]], + }); + + assert.strictEqual( + match_automation.can_leader_finish_match_within_rallies(match, make_scoring_format(), 5), + false + ); + }); + + _it('returns false when the leader cannot finish the match within n rallies', () => { + const match = make_match({ + network_score: [[21, 18], [9, 7]], + }); + + assert.strictEqual( + match_automation.can_leader_finish_match_within_rallies(match, make_scoring_format(), 11), + false + ); + }); + + _it('returns true when the leader can finish the match within n rallies', () => { + const match = make_match({ + network_score: [[21, 18], [10, 7]], + }); + + assert.strictEqual( + match_automation.can_leader_finish_match_within_rallies(match, make_scoring_format(), 11), + true + ); + }); + + _it('allows rallies to carry over into the next set', () => { + const match = make_match({ + network_score: [[18, 15]], + }); + + assert.strictEqual( + match_automation.can_leader_finish_match_within_rallies(match, make_scoring_format(), 25), + true + ); + }); + + _it('returns false when carried-over rallies still do not finish the match', () => { + const match = make_match({ + network_score: [[18, 15]], + }); + + assert.strictEqual( + match_automation.can_leader_finish_match_within_rallies(match, make_scoring_format(), 20), + false + ); + }); + + _it('returns false in deuce when no side leads', () => { + const match = make_match({ + network_score: [[21, 18], [20, 20]], + }); + + assert.strictEqual( + match_automation.can_leader_finish_match_within_rallies(match, make_scoring_format(), 1), + false + ); + }); + + _it('returns zero preparation need when no successors and no free courts exist', () => { + const result = match_automation.calculate_location_preparation_need({ + courts: [ + { _id: 'c1', location_id: 'l1', is_active: true, match_id: 'm1' }, + ], + matches: [ + { + _id: 'm1', + setup: { + now_on_court: true, + court_id: 'c1', + }, + }, + ], + }, 'l1'); + + assert.deepStrictEqual(result, { + location_id: 'l1', + active_court_count: 1, + successor_need_count: 0, + free_court_count: 0, + required_preparation_count: 0, + }); + }); + + _it('tracks a free active court separately from preparation successor need', () => { + const result = match_automation.calculate_location_preparation_need({ + courts: [ + { _id: 'c1', location_id: 'l1', is_active: true, match_id: null }, + ], + matches: [], + }, 'l1'); + + assert.strictEqual(result.successor_need_count, 0); + assert.strictEqual(result.free_court_count, 1); + assert.strictEqual(result.required_preparation_count, 1); + }); + + _it('counts a triggered on-court match towards preparation need', () => { + const result = match_automation.calculate_location_preparation_need({ + courts: [ + { _id: 'c1', location_id: 'l1', is_active: true, match_id: 'm1' }, + ], + matches: [ + { + _id: 'm1', + setup: { + now_on_court: true, + court_id: 'c1', + needs_preparation_successor: true, + }, + }, + ], + }, 'l1'); + + assert.strictEqual(result.successor_need_count, 1); + assert.strictEqual(result.free_court_count, 0); + assert.strictEqual(result.required_preparation_count, 1); + }); + + _it('does not count successor need on inactive courts', () => { + const result = match_automation.calculate_location_preparation_need({ + courts: [ + { _id: 'c1', location_id: 'l1', is_active: false, match_id: 'm1' }, + ], + matches: [ + { + _id: 'm1', + setup: { + now_on_court: true, + court_id: 'c1', + needs_preparation_successor: true, + }, + }, + ], + }, 'l1'); + + assert.strictEqual(result.active_court_count, 0); + assert.strictEqual(result.successor_need_count, 0); + assert.strictEqual(result.free_court_count, 0); + assert.strictEqual(result.required_preparation_count, 0); + }); + + _it('uses only successor need as required preparation count', () => { + const result = match_automation.calculate_location_preparation_need({ + courts: [ + { _id: 'c1', location_id: 'l1', is_active: true, match_id: 'm1' }, + { _id: 'c2', location_id: 'l1', is_active: true, match_id: 'm2' }, + { _id: 'c3', location_id: 'l1', is_active: true, match_id: null }, + ], + matches: [ + { + _id: 'm1', + setup: { + now_on_court: true, + court_id: 'c1', + needs_preparation_successor: true, + }, + }, + { + _id: 'm2', + setup: { + now_on_court: true, + court_id: 'c2', + needs_preparation_successor: true, + }, + }, + ], + }, 'l1'); + + assert.strictEqual(result.successor_need_count, 2); + assert.strictEqual(result.free_court_count, 1); + assert.strictEqual(result.required_preparation_count, 3); + }); + + _it('ignores inactive or occupied courts', () => { + const result = match_automation.calculate_location_preparation_need({ + courts: [ + { _id: 'c1', location_id: 'l1', is_active: false, match_id: null }, + { _id: 'c2', location_id: 'l1', is_active: true, match_id: 'm2' }, + { _id: 'c3', location_id: 'l1', is_active: true, match_id: null }, + ], + matches: [ + { + _id: 'm2', + setup: { + now_on_court: true, + court_id: 'c2', + }, + }, + ], + }, 'l1'); + + assert.strictEqual(result.free_court_count, 1); + assert.strictEqual(result.required_preparation_count, 1); + }); + + _it('treats stale court match references as free when no match is actually on court', () => { + const result = match_automation.calculate_location_preparation_need({ + courts: [ + { _id: 'c1', location_id: 'l1', is_active: true, match_id: 'old-match' }, + ], + matches: [ + { + _id: 'old-match', + setup: { + now_on_court: false, + court_id: 'c1', + }, + }, + ], + }, 'l1'); + + assert.strictEqual(result.free_court_count, 1); + assert.strictEqual(result.required_preparation_count, 1); + }); + + _it('only counts matches and courts of the requested location', () => { + const result = match_automation.calculate_location_preparation_need({ + courts: [ + { _id: 'c1', location_id: 'l1', is_active: true, match_id: 'm1' }, + { _id: 'c2', location_id: 'l1', is_active: true, match_id: null }, + { _id: 'c3', location_id: 'l2', is_active: true, match_id: null }, + ], + matches: [ + { + _id: 'm1', + setup: { + now_on_court: true, + court_id: 'c1', + needs_preparation_successor: true, + }, + }, + { + _id: 'm2', + setup: { + now_on_court: true, + court_id: 'c3', + needs_preparation_successor: true, + }, + }, + ], + }, 'l1'); + + assert.strictEqual(result.successor_need_count, 1); + assert.strictEqual(result.free_court_count, 1); + assert.strictEqual(result.required_preparation_count, 2); + }); + + _it('caps required preparation count at the number of active courts', () => { + const result = match_automation.calculate_location_preparation_need({ + courts: [ + { _id: 'c1', location_id: 'l1', is_active: true, match_id: 'm1' }, + ], + matches: [ + { + _id: 'm1', + setup: { + now_on_court: true, + court_id: 'c1', + needs_preparation_successor: true, + }, + }, + { + _id: 'm2', + setup: { + state: 'preparation', + location_id: 'l1', + }, + }, + ], + }, 'l1'); + + assert.strictEqual(result.active_court_count, 1); + assert.strictEqual(result.successor_need_count, 1); + assert.strictEqual(result.free_court_count, 0); + assert.strictEqual(result.required_preparation_count, 1); + }); + + _it('uses the default rally threshold for preparation successor state', () => { + const match = make_match({ + network_score: [[21, 18], [10, 7]], + }); + match.setup.scoring_format = make_scoring_format(); + + const result = match_automation.calculate_preparation_successor_state(match, {}); + + assert.strictEqual(result.rally_count, 11); + assert.strictEqual(result.needs_preparation_successor, true); + assert.ok(Number.isFinite(result.needs_preparation_successor_ts)); + }); + + _it('preserves an existing preparation successor timestamp while the flag stays active', () => { + const match = make_match({ + network_score: [[21, 18], [10, 7]], + }); + match.setup.scoring_format = make_scoring_format(); + match.setup.needs_preparation_successor_ts = 12345; + + const result = match_automation.calculate_preparation_successor_state(match, { + preparation_successor_rally_count: 11, + }); + + assert.strictEqual(result.needs_preparation_successor, true); + assert.strictEqual(result.needs_preparation_successor_ts, 12345); + }); + + _it('subtracts already prepared matches for the same location', () => { + const result = match_automation.calculate_location_preparation_status({ + courts: [ + { _id: 'c1', location_id: 'l1', is_active: true, match_id: 'm1' }, + { _id: 'c2', location_id: 'l1', is_active: true, match_id: null }, + ], + matches: [ + { + _id: 'm1', + setup: { + now_on_court: true, + court_id: 'c1', + needs_preparation_successor: true, + }, + }, + { + _id: 'm2', + setup: { + state: 'preparation', + location_id: 'l1', + }, + }, + ], + }, 'l1'); + + assert.strictEqual(result.required_preparation_count, 2); + assert.strictEqual(result.current_preparation_count, 1); + assert.strictEqual(result.missing_preparation_count, 1); + }); + + _it('accepts a scheduled match explicitly assigned to the location', () => { + const tournament = { courts: [], matches: [] }; + const match = make_preparation_match({ + setup: { + location_id: 'l1', + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + true + ); + }); + + _it('accepts a scheduled match via a court assigned to the location', () => { + const tournament = { + courts: [ + { _id: 'c1', location_id: 'l1' }, + ], + matches: [], + }; + const match = make_preparation_match({ + setup: { + court_id: 'c1', + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + true + ); + }); + + _it('accepts an unassigned match for any location', () => { + const tournament = { courts: [], matches: [] }; + const match = make_preparation_match(); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + true + ); + }); + + _it('rejects a match assigned to another location', () => { + const tournament = { courts: [], matches: [] }; + const match = make_preparation_match({ + setup: { + location_id: 'l2', + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + false + ); + }); + + _it('rejects a match whose court belongs to another location', () => { + const tournament = { + courts: [ + { _id: 'c2', location_id: 'l2' }, + ], + matches: [], + }; + const match = make_preparation_match({ + setup: { + court_id: 'c2', + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + false + ); + }); + + _it('rejects matches that are not clean scheduled candidates', () => { + const tournament = { courts: [], matches: [] }; + const cases = [ + make_preparation_match({ setup: { state: 'preparation' } }), + make_preparation_match({ setup: { state: 'oncourt' } }), + make_preparation_match({ setup: { state: 'blocked' } }), + make_preparation_match({ setup: { state: 'finished' } }), + make_preparation_match({ setup: { state: 'draft' } }), + make_preparation_match({ setup: { is_match: false } }), + make_preparation_match({ setup: { incomplete: true } }), + make_preparation_match({ team1_won: true }), + make_preparation_match({ + setup: { + teams: [ + { players: [{ _id: 'p1' }] }, + { players: [] }, + ], + }, + }), + ]; + + cases.forEach((match) => { + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + false + ); + }); + }); + + _it('respects the optional time limit before scheduled time', () => { + const tournament = { + preparation_call_time_limit_before_scheduled_enabled: true, + preparation_call_time_limit_before_scheduled_minutes: 15, + courts: [], + matches: [], + }; + const match = make_preparation_match({ + setup: { + scheduled_date: '2026-04-07', + scheduled_time_str: '10:00', + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament, { + now_ts: Date.parse('2026-04-07T09:44:00'), + }), + false + ); + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament, { + now_ts: Date.parse('2026-04-07T09:45:00'), + }), + true + ); + }); + + _it('rejects matches without usable scheduling when the time limit is enabled', () => { + const tournament = { + preparation_call_time_limit_before_scheduled_enabled: true, + preparation_call_time_limit_before_scheduled_minutes: 15, + courts: [], + matches: [], + }; + const match = make_preparation_match({ + setup: { + scheduled_time_str: null, + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + false + ); + }); + + _it('respects the optional player pause rule', () => { + const tournament = { + preparation_call_player_pause_expired_enabled: true, + btp_settings: { + pause_duration_ms: 10 * 60 * 1000, + }, + courts: [], + matches: [], + }; + const match = make_preparation_match({ + setup: { + teams: [ + { players: [{ _id: 'p1', last_time_on_court_ts: Date.parse('2026-04-07T09:55:01') }] }, + { players: [{ _id: 'p2', last_time_on_court_ts: Date.parse('2026-04-07T09:40:00') }] }, + ], + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament, { + now_ts: Date.parse('2026-04-07T10:00:00'), + }), + false + ); + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament, { + now_ts: Date.parse('2026-04-07T10:06:00'), + }), + true + ); + }); + + _it('treats players currently on court or on tablet as not pause-cleared when the rule is enabled', () => { + const tournament = { + preparation_call_player_pause_expired_enabled: true, + btp_settings: { + pause_duration_ms: 10 * 60 * 1000, + }, + courts: [], + matches: [], + }; + const now = Date.parse('2026-04-07T10:30:00'); + + const playing_match = make_preparation_match({ + setup: { + teams: [ + { players: [{ _id: 'p1', now_playing_on_court: 'c1' }] }, + { players: [{ _id: 'p2' }] }, + ], + }, + }); + const tablet_match = make_preparation_match({ + setup: { + teams: [ + { players: [{ _id: 'p1', now_tablet_on_court: 'c1' }] }, + { players: [{ _id: 'p2' }] }, + ], + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(playing_match, 'l1', tournament, { now_ts: now }), + false + ); + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(tablet_match, 'l1', tournament, { now_ts: now }), + false + ); + }); + + _it('does not enforce player pause timing when the rule is disabled', () => { + const tournament = { + preparation_call_player_pause_expired_enabled: false, + btp_settings: { + pause_duration_ms: 10 * 60 * 1000, + }, + courts: [], + matches: [], + }; + const match = make_preparation_match({ + setup: { + teams: [ + { players: [{ _id: 'p1', last_time_on_court_ts: Date.parse('2026-04-07T09:59:30') }] }, + { players: [{ _id: 'p2' }] }, + ], + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament, { + now_ts: Date.parse('2026-04-07T10:00:00'), + }), + true + ); + }); + + _it('requires matches to already be in preparation for the configured minimum time on court-call automation', () => { + const court = make_court({ _id: 'c1', location_id: 'l1', is_active: true }); + const tournament = make_tournament({ + courts: [court], + matches: [], + call_on_court_only_preparation_enabled: true, + call_on_court_only_preparation_minutes: 5, + }); + const scheduled_match = make_preparation_match({ + _id: 'scheduled', + setup: { + state: 'scheduled', + location_id: 'l1', + }, + }); + const recent_preparation_match = make_preparation_match({ + _id: 'prep-recent', + setup: { + state: 'preparation', + location_id: 'l1', + preparation_call_timestamp: Date.parse('2026-04-07T09:57:00'), + }, + }); + const old_preparation_match = make_preparation_match({ + _id: 'prep-old', + setup: { + state: 'preparation', + location_id: 'l1', + preparation_call_timestamp: Date.parse('2026-04-07T09:54:00'), + }, + }); + tournament.matches = [scheduled_match, recent_preparation_match, old_preparation_match]; + + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(scheduled_match, 'c1', tournament, { + now_ts: Date.parse('2026-04-07T10:00:00'), + }), + false + ); + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(recent_preparation_match, 'c1', tournament, { + now_ts: Date.parse('2026-04-07T10:00:00'), + }), + false + ); + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(old_preparation_match, 'c1', tournament, { + now_ts: Date.parse('2026-04-07T10:00:00'), + }), + true + ); + }); + + _it('ignores earlier scheduled matches as frontier blockers when only preparation matches may be called', () => { + const court = make_court({ _id: 'c1', location_id: 'l1', is_active: true }); + const scheduled_match = make_preparation_match({ + _id: 'scheduled-earlier', + match_order: 1, + setup: { + state: 'scheduled', + location_id: 'l1', + match_num: 10, + scheduled_time_str: '10:00', + }, + }); + const preparation_match = make_preparation_match({ + _id: 'prepared-later', + match_order: 2, + setup: { + state: 'preparation', + location_id: 'l1', + match_num: 11, + scheduled_time_str: '10:15', + preparation_call_timestamp: Date.parse('2026-04-07T09:59:00'), + }, + }); + const tournament = make_tournament({ + courts: [court], + matches: [scheduled_match, preparation_match], + call_on_court_only_preparation_enabled: true, + call_on_court_only_preparation_minutes: 0, + }); + + const candidates = match_automation.find_call_on_court_candidates(tournament, 'c1', { + now_ts: Date.parse('2026-04-07T10:00:00'), + }); + + assert.deepStrictEqual(candidates.map((match) => match._id), ['prepared-later']); + }); + + _it('prefers the oldest preparation call when multiple prepared matches are eligible for on-court calls', () => { + const court = make_court({ _id: 'c1', location_id: 'l1', is_active: true }); + const newer_preparation_match = make_preparation_match({ + _id: 'prepared-newer', + match_order: 2, + setup: { + state: 'preparation', + location_id: 'l1', + match_num: 11, + scheduled_time_str: '10:15', + preparation_call_timestamp: Date.parse('2026-04-07T09:58:00'), + }, + }); + const older_preparation_match = make_preparation_match({ + _id: 'prepared-older', + match_order: 3, + setup: { + state: 'preparation', + location_id: 'l1', + match_num: 12, + scheduled_time_str: '10:30', + preparation_call_timestamp: Date.parse('2026-04-07T09:52:00'), + }, + }); + const tournament = make_tournament({ + courts: [court], + matches: [newer_preparation_match, older_preparation_match], + call_on_court_only_preparation_enabled: true, + call_on_court_only_preparation_minutes: 0, + }); + + const candidates = match_automation.find_call_on_court_candidates(tournament, 'c1', { + now_ts: Date.parse('2026-04-07T10:00:00'), + }); + + assert.deepStrictEqual(candidates.map((match) => match._id), ['prepared-older', 'prepared-newer']); + }); + + _it('requires all players to be checked in when the on-court participant rule is set to checked_in', () => { + const tournament = make_tournament({ + courts: [make_court({ _id: 'c1', location_id: 'l1', is_active: true })], + call_on_court_participant_readiness_mode: 'checked_in', + }); + const match = make_preparation_match({ + setup: { + location_id: 'l1', + teams: [ + { players: [{ _id: 'p1', checked_in: true }] }, + { players: [{ _id: 'p2', checked_in: false }] }, + ], + }, + }); + tournament.matches = [match]; + + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(match, 'c1', tournament), + false + ); + + match.setup.teams[1].players[0].checked_in = true; + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(match, 'c1', tournament), + true + ); + }); + + _it('requires assigned technical officials to be checked in when the on-court official rule is set to checked_in', () => { + const tournament = make_tournament({ + courts: [make_court({ _id: 'c1', location_id: 'l1', is_active: true })], + official_rotation_mode: 'umpire_and_service_judge', + call_on_court_technical_officials_mode: 'checked_in', + }); + const match = make_preparation_match({ + setup: { + location_id: 'l1', + umpire: { _id: 'u1', checked_in: true }, + service_judge: { _id: 'u2', checked_in: false }, + }, + }); + tournament.matches = [match]; + + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(match, 'c1', tournament), + false + ); + + match.setup.service_judge.checked_in = true; + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(match, 'c1', tournament), + true + ); + }); + + _it('requires technical officials to be available when the on-court official rule is set to available', () => { + const tournament = make_tournament({ + courts: [make_court({ _id: 'c1', location_id: 'l1', is_active: true })], + official_rotation_mode: 'umpire_only', + technical_official_auto_assignment_mode: 'when_available', + call_on_court_technical_officials_mode: 'available', + umpires: [], + }); + const match = make_preparation_match({ + setup: { + location_id: 'l1', + }, + }); + tournament.matches = [match]; + + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(match, 'c1', tournament), + false + ); + + tournament.umpires = [make_official({ _id: 'u1', umpire_wait: 111, checked_in: true })]; + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(match, 'c1', tournament), + true + ); + }); + + _it('requires enough official space on the target court when availability is checked for on-match-call assignment', () => { + const tournament = make_tournament({ + courts: [make_court({ _id: 'c1', location_id: 'l1', is_active: true, has_umpire: false })], + official_rotation_mode: 'umpire_only', + technical_official_auto_assignment_mode: 'on_match_call_if_possible', + call_on_court_technical_officials_mode: 'available', + umpires: [make_official({ _id: 'u1', umpire_wait: 111, checked_in: true })], + }); + const match = make_preparation_match({ + setup: { + location_id: 'l1', + }, + }); + tournament.matches = [match]; + + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(match, 'c1', tournament), + false + ); + }); + + _it('allows on-court call with only an umpire when the target court has no service judge space', () => { + const tournament = make_tournament({ + courts: [make_court({ _id: 'c1', location_id: 'l1', is_active: true, has_umpire: true, has_service_judge: false })], + official_rotation_mode: 'umpire_and_service_judge', + technical_official_auto_assignment_mode: 'on_match_call_if_possible', + call_on_court_technical_officials_mode: 'available', + umpires: [make_official({ _id: 'u1', umpire_wait: 111, checked_in: true })], + }); + const match = make_preparation_match({ + setup: { + state: 'preparation', + location_id: 'l1', + }, + }); + tournament.matches = [match]; + + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(match, 'c1', tournament), + true + ); + }); + + _it('does not require a service judge to be checked in on a court without service judge space', () => { + const tournament = make_tournament({ + courts: [make_court({ _id: 'c1', location_id: 'l1', is_active: true, has_umpire: true, has_service_judge: false })], + official_rotation_mode: 'umpire_and_service_judge', + call_on_court_technical_officials_mode: 'checked_in', + }); + const match = make_preparation_match({ + setup: { + location_id: 'l1', + umpire: { _id: 'u1', checked_in: true }, + }, + }); + tournament.matches = [match]; + + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(match, 'c1', tournament), + true + ); + }); + + _it('can require enough court space for already assigned technical officials', () => { + const tournament = make_tournament({ + courts: [ + make_court({ _id: 'c1', location_id: 'l1', is_active: true, has_umpire: true, has_service_judge: false }), + make_court({ _id: 'c2', location_id: 'l1', is_active: true, has_umpire: true, has_service_judge: true }), + ], + call_on_court_require_official_space_enabled: true, + }); + const match = make_preparation_match({ + setup: { + location_id: 'l1', + umpire: { _id: 'u1', checked_in: true }, + service_judge: { _id: 'u2', checked_in: true }, + }, + }); + tournament.matches = [match]; + + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(match, 'c1', tournament), + false + ); + assert.strictEqual( + match_automation.is_match_eligible_for_on_court_call(match, 'c2', tournament), + true + ); + }); + + _it('requires a waiting umpire when the technical officials rule is enabled in umpire_only mode', () => { + const tournament = make_tournament({ + preparation_call_technical_officials_available_enabled: true, + official_rotation_mode: 'umpire_only', + technical_official_auto_assignment_mode: 'on_preparation_call', + umpires: [], + }); + const match = make_preparation_match(); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + false + ); + + tournament.umpires = [ + make_official({ _id: 'u1', umpire_wait: 111 }), + ]; + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + true + ); + }); + + _it('requires distinct waiting umpire and service judge capacity in full rotation mode', () => { + const tournament = make_tournament({ + preparation_call_technical_officials_available_enabled: true, + official_rotation_mode: 'umpire_and_service_judge', + technical_official_auto_assignment_mode: 'on_preparation_call', + umpires: [ + make_official({ _id: 'dual-only', umpire_wait: 111, service_judge_wait: 222 }), + ], + }); + const match = make_preparation_match(); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + false + ); + + tournament.umpires = [ + make_official({ _id: 'u1', umpire_wait: 111 }), + make_official({ _id: 's1', service_judge_wait: 222 }), + ]; + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + true + ); + }); + + _it('accepts matches that already have the required technical officials assigned', () => { + const tournament = make_tournament({ + preparation_call_technical_officials_available_enabled: true, + official_rotation_mode: 'umpire_and_service_judge', + technical_official_auto_assignment_mode: 'on_preparation_call', + umpires: [], + }); + const match = make_preparation_match({ + setup: { + umpire: { _id: 'u1' }, + service_judge: { _id: 's1' }, + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + true + ); + }); + + _it('accepts matches with one assigned technical official when the missing role is still available', () => { + const tournament = make_tournament({ + preparation_call_technical_officials_available_enabled: true, + official_rotation_mode: 'umpire_and_service_judge', + technical_official_auto_assignment_mode: 'on_preparation_call', + umpires: [ + make_official({ _id: 's1', service_judge_wait: 222 }), + ], + }); + const match = make_preparation_match({ + setup: { + umpire: { _id: 'u1' }, + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + true + ); + }); + + _it('does not count an already assigned official as available for the missing second role', () => { + const tournament = make_tournament({ + preparation_call_technical_officials_available_enabled: true, + official_rotation_mode: 'umpire_and_service_judge', + technical_official_auto_assignment_mode: 'on_preparation_call', + umpires: [ + make_official({ _id: 'dual1', service_judge_wait: 222 }), + ], + }); + const match = make_preparation_match({ + setup: { + umpire: { _id: 'dual1' }, + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + false + ); + }); + + _it('can ignore the technical officials availability rule when searching likely future candidates', () => { + const tournament = make_tournament({ + preparation_call_technical_officials_available_enabled: true, + official_rotation_mode: 'umpire_and_service_judge', + technical_official_auto_assignment_mode: 'on_preparation_call', + umpires: [ + make_official({ _id: 'u1', umpire_wait: 111 }), + ], + matches: [ + make_preparation_match({ + _id: 'm1', + setup: { + match_num: 1, + scheduled_time_str: '10:00', + }, + }), + ], + }); + + assert.deepStrictEqual( + match_automation.find_global_preparation_candidates(tournament).map((match) => match._id), + [] + ); + + assert.deepStrictEqual( + match_automation.find_global_preparation_candidates(tournament, { + ignore_technical_officials_available_rule: true, + }).map((match) => match._id), + ['m1'] + ); + }); + + _it('ignores the technical officials rule when it is disabled', () => { + const tournament = make_tournament({ + preparation_call_technical_officials_available_enabled: false, + official_rotation_mode: 'umpire_and_service_judge', + umpires: [], + }); + const match = make_preparation_match(); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + true + ); + }); + + _it('rejects matches with an unfinished direct predecessor', () => { + const predecessor = make_preparation_match({ + _id: 'm-pre', + btp_match_ids: [{ planning: 100 }], + setup: { + match_num: 1, + scheduled_time_str: '09:30', + }, + }); + const match = make_preparation_match({ + _id: 'm-target', + setup: { + match_num: 2, + scheduled_time_str: '10:00', + teams: [ + { players: [{ _id: 'p1' }] }, + { players: [] }, + ], + links: { + from1: 100, + }, + }, + }); + const tournament = { + courts: [], + matches: [predecessor, match], + }; + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + false + ); + + predecessor.team1_won = true; + match.setup.teams[1].players = [{ _id: 'p2' }]; + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + true + ); + }); + + _it('rejects matches with unresolved predecessor references when participants are not yet complete', () => { + const tournament = { courts: [], matches: [] }; + const match = make_preparation_match({ + setup: { + teams: [ + { players: [{ _id: 'p1' }] }, + { players: [] }, + ], + links: { + from1: 999, + }, + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + false + ); + }); + + _it('does not reject complete matches only because raw predecessor links exist', () => { + const tournament = { courts: [], matches: [] }; + const match = make_preparation_match({ + setup: { + links: { + from1: 999, + }, + }, + }); + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(match, 'l1', tournament), + true + ); + }); + + _it('rejects matches whose players are still in an earlier unfinished match', () => { + const earlier_match = make_preparation_match({ + _id: 'm-earlier', + setup: { + match_num: 1, + scheduled_time_str: '09:30', + teams: [ + { players: [{ _id: 'p1', btp_id: 10 }] }, + { players: [{ _id: 'p2', btp_id: 20 }] }, + ], + }, + }); + const later_match = make_preparation_match({ + _id: 'm-later', + setup: { + match_num: 2, + scheduled_time_str: '10:00', + teams: [ + { players: [{ _id: 'p1', btp_id: 10 }] }, + { players: [{ _id: 'p3', btp_id: 30 }] }, + ], + }, + }); + const tournament = { + courts: [], + matches: [earlier_match, later_match], + }; + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(later_match, 'l1', tournament), + false + ); + + earlier_match.team1_won = true; + + assert.strictEqual( + match_automation.is_match_eligible_for_preparation(later_match, 'l1', tournament), + true + ); + }); + + _it('limits candidates by block distance from the first unusable match', () => { + const tournament = { + preparation_call_block_ahead_limit_enabled: true, + preparation_call_block_ahead_limit: 1, + courts: [], + matches: [ + make_preparation_match({ + _id: 'm1', + setup: { + match_num: 1, + scheduled_time_str: '09:00', + event_name: 'HE', + phase_block_key: 'G1', + }, + }), + make_preparation_match({ + _id: 'm2', + setup: { + match_num: 2, + scheduled_time_str: '09:05', + event_name: 'HE', + phase_block_key: 'G1', + teams: [ + { players: [{ _id: 'p1' }] }, + { players: [] }, + ], + links: { from1: 999 }, + }, + }), + make_preparation_match({ + _id: 'm3', + setup: { + match_num: 3, + scheduled_time_str: '09:10', + event_name: 'HE', + phase_block_key: 'G1', + }, + }), + make_preparation_match({ + _id: 'm4', + setup: { + match_num: 4, + scheduled_time_str: '09:15', + event_name: 'HE', + phase_block_key: 'G2', + }, + }), + make_preparation_match({ + _id: 'm5', + setup: { + match_num: 5, + scheduled_time_str: '09:20', + event_name: 'HE', + phase_block_key: 'VF', + }, + }), + ], + }; + + const candidates = match_automation.find_location_preparation_candidates(tournament, 'l1'); + + assert.deepStrictEqual(candidates.map((match) => match._id), ['m1', 'm3', 'm4']); + }); + + _it('limits candidates by scheduled time distance from the first unusable match', () => { + const tournament = { + preparation_call_time_ahead_of_frontier_enabled: true, + preparation_call_time_ahead_of_frontier_minutes: 30, + courts: [], + matches: [ + make_preparation_match({ + _id: 'm1', + setup: { + match_num: 1, + scheduled_time_str: '09:00', + }, + }), + make_preparation_match({ + _id: 'm2', + setup: { + match_num: 2, + scheduled_time_str: '10:00', + teams: [ + { players: [{ _id: 'p1' }] }, + { players: [] }, + ], + links: { from1: 999 }, + }, + }), + make_preparation_match({ + _id: 'm3', + setup: { + match_num: 3, + scheduled_time_str: '10:20', + }, + }), + make_preparation_match({ + _id: 'm4', + setup: { + match_num: 4, + scheduled_time_str: '10:40', + }, + }), + ], + }; + + const candidates = match_automation.find_location_preparation_candidates(tournament, 'l1'); + + assert.deepStrictEqual(candidates.map((match) => match._id), ['m1', 'm3']); + }); + + _it('limits candidates by match count distance from the first unusable match', () => { + const tournament = { + preparation_call_matches_ahead_of_frontier_enabled: true, + preparation_call_matches_ahead_of_frontier_limit: 1, + courts: [], + matches: [ + make_preparation_match({ + _id: 'm1', + setup: { + match_num: 1, + scheduled_time_str: '09:00', + }, + }), + make_preparation_match({ + _id: 'm2', + setup: { + match_num: 2, + scheduled_time_str: '09:05', + teams: [ + { players: [{ _id: 'p1' }] }, + { players: [] }, + ], + links: { from1: 999 }, + }, + }), + make_preparation_match({ + _id: 'm3', + setup: { + match_num: 3, + scheduled_time_str: '09:10', + }, + }), + make_preparation_match({ + _id: 'm4', + setup: { + match_num: 4, + scheduled_time_str: '09:15', + }, + }), + ], + }; + + const candidates = match_automation.find_location_preparation_candidates(tournament, 'l1'); + + assert.deepStrictEqual(candidates.map((match) => match._id), ['m1', 'm3']); + }); + + _it('ignores incomplete matches when determining the frontier sequence', () => { + const tournament = { + preparation_call_matches_ahead_of_frontier_enabled: true, + preparation_call_matches_ahead_of_frontier_limit: 1, + courts: [], + matches: [ + make_preparation_match({ + _id: 'ko_future', + setup: { + match_num: 100, + state: 'incomplete', + phase_block_key: 'VF', + teams: [ + { players: [] }, + { players: [] }, + ], + }, + }), + make_preparation_match({ + _id: 'm1', + setup: { + match_num: 1, + scheduled_time_str: '09:00', + }, + }), + make_preparation_match({ + _id: 'frontier', + setup: { + match_num: 2, + scheduled_time_str: '09:05', + teams: [ + { players: [{ _id: 'p1' }] }, + { players: [] }, + ], + links: { from1: 999 }, + }, + }), + make_preparation_match({ + _id: 'm3', + setup: { + match_num: 3, + scheduled_time_str: '09:10', + }, + }), + ], + }; + + const candidates = match_automation.find_location_preparation_candidates(tournament, 'l1'); + + assert.deepStrictEqual(candidates.map((match) => match._id), ['m1', 'm3']); + }); + + _it('selects only as many preparation candidates as are currently missing', () => { + const tournament = { + call_preparation_matches_automatically_enabled: true, + courts: [ + { _id: 'c1', location_id: 'l1', is_active: true, match_id: null }, + { _id: 'c2', location_id: 'l1', is_active: true, match_id: 'm-oncourt' }, + ], + matches: [ + { + _id: 'm-oncourt', + setup: { + now_on_court: true, + court_id: 'c2', + needs_preparation_successor: true, + }, + }, + { + _id: 'm-prepared', + setup: { + state: 'preparation', + location_id: 'l1', + }, + }, + make_preparation_match({ + _id: 'm1', + setup: { + match_num: 1, + scheduled_time_str: '09:30', + location_id: 'l1', + }, + }), + make_preparation_match({ + _id: 'm2', + setup: { + match_num: 2, + scheduled_time_str: '10:00', + location_id: 'l1', + }, + }), + make_preparation_match({ + _id: 'm3', + setup: { + match_num: 3, + scheduled_time_str: '10:30', + location_id: 'l1', + }, + }), + ], + }; + + const selection = match_automation.calculate_location_preparation_selection(tournament, 'l1'); + + assert.strictEqual(selection.required_preparation_count, 2); + assert.strictEqual(selection.current_preparation_count, 1); + assert.strictEqual(selection.missing_preparation_count, 1); + assert.deepStrictEqual(selection.candidates.map((match) => match._id), ['m1', 'm2', 'm3']); + assert.deepStrictEqual(selection.selected_matches.map((match) => match._id), ['m1']); + assert.deepStrictEqual(selection.auto_selected_matches.map((match) => match._id), []); + }); + + _it('returns no selected matches when the location already has enough preparation matches', () => { + const tournament = { + courts: [ + { _id: 'c1', location_id: 'l1', is_active: true, match_id: null }, + ], + matches: [ + { + _id: 'm-prepared', + setup: { + state: 'preparation', + location_id: 'l1', + }, + }, + make_preparation_match({ + _id: 'm1', + setup: { + match_num: 1, + scheduled_time_str: '09:30', + location_id: 'l1', + }, + }), + ], + }; + + const selection = match_automation.calculate_location_preparation_selection(tournament, 'l1'); + + assert.strictEqual(selection.required_preparation_count, 1); + assert.strictEqual(selection.current_preparation_count, 1); + assert.strictEqual(selection.missing_preparation_count, 0); + assert.deepStrictEqual(selection.selected_matches, []); + assert.deepStrictEqual(selection.auto_selected_matches, []); + }); + + _it('fetches the preparation selection for one location from db-backed data', async () => { + const app = { + db: { + tournaments: { + findOne_async: async () => ({ + key: 't1', + }), + }, + locations: { + find_async: async () => ([ + { _id: 'l1', name: 'Loc 1' }, + ]), + }, + courts: { + find_async: async () => ([ + { _id: 'c1', location_id: 'l1', is_active: true, match_id: null }, + ]), + }, + matches: { + find_async: async () => ([ + make_preparation_match({ + _id: 'm1', + setup: { + match_num: 1, + scheduled_time_str: '09:30', + location_id: 'l1', + }, + }), + ]), + }, + umpires: { + find_async: async () => ([]), + }, + }, + }; + + const selection = await match_automation.fetch_location_preparation_selection(app, 't1', 'l1'); + + assert.strictEqual(selection.location_id, 'l1'); + assert.strictEqual(selection.location.name, 'Loc 1'); + assert.strictEqual(selection.required_preparation_count, 1); + assert.strictEqual(selection.missing_preparation_count, 1); + assert.deepStrictEqual(selection.selected_matches.map((match) => match._id), ['m1']); + assert.deepStrictEqual(selection.auto_selected_matches.map((match) => match._id), []); + }); + + _it('sorts preparation candidates like the main page', () => { + const tournament = { + courts: [], + matches: [ + make_preparation_match({ + _id: 'm4', + match_order: 2, + setup: { + match_num: 4, + scheduled_date: '2026-04-07', + scheduled_time_str: '10:00', + }, + }), + make_preparation_match({ + _id: 'm2', + match_order: 1, + setup: { + match_num: 2, + scheduled_date: '2026-04-07', + scheduled_time_str: '10:00', + }, + }), + make_preparation_match({ + _id: 'm1', + setup: { + match_num: 1, + scheduled_date: '2026-04-07', + scheduled_time_str: '09:30', + }, + }), + make_preparation_match({ + _id: 'm3', + setup: { + match_num: 3, + scheduled_date: '2026-04-07', + scheduled_time_str: '00:00', + }, + }), + ], + }; + + const candidates = match_automation.find_location_preparation_candidates(tournament, 'l1'); + + assert.deepStrictEqual(candidates.map((match) => match._id), ['m1', 'm2', 'm4', 'm3']); + }); + + _it('finds global preparation candidates without filtering by location', () => { + const tournament = { + courts: [ + make_court({ _id: 'c1', location_id: 'l1' }), + make_court({ _id: 'c2', location_id: 'l2' }), + ], + matches: [ + make_preparation_match({ + _id: 'm2', + setup: { + match_num: 2, + scheduled_time_str: '10:00', + location_id: 'l2', + }, + }), + make_preparation_match({ + _id: 'm1', + setup: { + match_num: 1, + scheduled_time_str: '09:30', + location_id: 'l1', + }, + }), + ], + }; + + const candidates = match_automation.find_global_preparation_candidates(tournament); + + assert.deepStrictEqual(candidates.map((match) => match._id), ['m1', 'm2']); + }); + + _it('keeps a realistic location fixture stable across active, inactive and prepared matches', () => { + const tournament = make_realistic_location_fixture(); + + const need = match_automation.calculate_location_preparation_need(tournament, 'l-hall'); + const status = match_automation.calculate_location_preparation_status(tournament, 'l-hall'); + const selection = match_automation.calculate_location_preparation_selection(tournament, 'l-hall'); + + assert.strictEqual(need.active_court_count, 2); + assert.strictEqual(need.successor_need_count, 1); + assert.strictEqual(need.free_court_count, 1); + assert.strictEqual(need.required_preparation_count, 2); + + assert.strictEqual(status.current_preparation_count, 1); + assert.strictEqual(status.missing_preparation_count, 1); + + assert.deepStrictEqual(selection.candidates.map((match) => match._id), ['m-eligible-1', 'm-eligible-2']); + assert.deepStrictEqual(selection.selected_matches.map((match) => match._id), ['m-eligible-1']); + assert.deepStrictEqual(selection.auto_selected_matches.map((match) => match._id), []); + }); +}); diff --git a/test/test_match_utils_officials.js b/test/test_match_utils_officials.js new file mode 100644 index 0000000..d8db2d6 --- /dev/null +++ b/test/test_match_utils_officials.js @@ -0,0 +1,645 @@ +'use strict'; + +const assert = require('assert'); + +const { _describe, _it } = require('./tutils.js'); +const match_utils = require('../bts/match_utils.js'); +const admin = require('../bts/admin.js'); +const btp_manager = require('../bts/btp_manager.js'); +const match_automation = require('../bts/match_automation.js'); + +function make_official(overrides = {}) { + const pick = (field, fallback) => Object.prototype.hasOwnProperty.call(overrides, field) ? overrides[field] : fallback; + return { + _id: overrides._id || 'u1', + name: overrides.name || 'Test Official', + is_umpire: overrides.is_umpire === true, + is_service_judge: overrides.is_service_judge === true, + is_planed_as_umpire: overrides.is_planed_as_umpire === true, + is_planed_as_service_judge: overrides.is_planed_as_service_judge === true, + umpire_on_court: pick('umpire_on_court', 'c1'), + service_judge_on_court: pick('service_judge_on_court', null), + umpire_wait: pick('umpire_wait', 111), + service_judge_wait: pick('service_judge_wait', 222), + umpire_pause: pick('umpire_pause', 333), + service_judge_pause: pick('service_judge_pause', 444), + inactive_list: pick('inactive_list', 555), + last_time_on_court_ts: pick('last_time_on_court_ts', 666), + status: overrides.status || 'oncourt', + court_id: pick('court_id', 'c1'), + checked_in: overrides.checked_in === true, + }; +} + +_describe('match utils official state helpers', () => { + _it('moves an official to standby and clears all active list markers', () => { + const official = make_official({ + is_planed_as_umpire: true, + is_planed_as_service_judge: true, + is_umpire: true, + is_service_judge: true, + }); + + const result = match_utils.apply_official_standby_state(official); + + assert.strictEqual(result.status, 'standby'); + assert.strictEqual(result.umpire_on_court, null); + assert.strictEqual(result.service_judge_on_court, null); + assert.strictEqual(result.is_planed_as_umpire, false); + assert.strictEqual(result.is_planed_as_service_judge, false); + assert.strictEqual(result.umpire_wait, null); + assert.strictEqual(result.service_judge_wait, null); + assert.strictEqual(result.umpire_pause, null); + assert.strictEqual(result.service_judge_pause, null); + assert.strictEqual(result.inactive_list, null); + assert.strictEqual(result.last_time_on_court_ts, null); + assert.strictEqual(result.court_id, null); + }); + + _it('releases a service judge from court into the correct wait list', () => { + const end_ts = 123456; + const official = make_official({ + is_umpire: false, + is_service_judge: true, + is_planed_as_service_judge: true, + service_judge_on_court: 'c9', + }); + + const result = match_utils.apply_official_on_court_release(official, 'service_judge', end_ts); + + assert.strictEqual(result.status, 'ready'); + assert.strictEqual(result.umpire_on_court, null); + assert.strictEqual(result.service_judge_on_court, null); + assert.strictEqual(result.is_planed_as_umpire, false); + assert.strictEqual(result.is_planed_as_service_judge, false); + assert.strictEqual(result.umpire_wait, null); + assert.strictEqual(result.service_judge_wait, end_ts + 100); + assert.strictEqual(result.umpire_pause, null); + assert.strictEqual(result.service_judge_pause, null); + assert.strictEqual(result.inactive_list, null); + assert.strictEqual(result.last_time_on_court_ts, end_ts); + assert.strictEqual(result.court_id, null); + }); + + _it('returns a dual-role umpire back to the service judge wait list in full rotation mode', () => { + const end_ts = 555000; + const official = make_official({ + is_umpire: true, + is_service_judge: true, + is_planed_as_umpire: true, + umpire_on_court: 'c7', + }); + + const result = match_utils.apply_official_on_court_release(official, 'umpire', end_ts); + + assert.strictEqual(result.umpire_wait, null); + assert.strictEqual(result.service_judge_wait, end_ts); + assert.strictEqual(result.inactive_list, null); + }); + + _it('returns any official to the umpire wait list in umpire_only mode', () => { + const end_ts = 777000; + const official = make_official({ + is_umpire: false, + is_service_judge: true, + is_planed_as_service_judge: true, + service_judge_on_court: 'c8', + }); + + const result = match_utils.apply_official_on_court_release(official, 'service_judge', end_ts, { + official_rotation_mode: 'umpire_only' + }); + + assert.strictEqual(result.umpire_wait, end_ts); + assert.strictEqual(result.service_judge_wait, null); + assert.strictEqual(result.inactive_list, null); + }); + + _it('puts a returning official into pause instead of wait when a technical official break is configured', () => { + const end_ts = 888000; + const official = make_official({ + is_umpire: true, + is_service_judge: false, + is_planed_as_umpire: true, + umpire_on_court: 'c3', + }); + + const result = match_utils.apply_official_on_court_release(official, 'umpire', end_ts, { + official_rotation_mode: 'umpire_only', + technical_official_break_after_assignment_ms: 30 * 1000, + }); + + assert.strictEqual(result.status, 'pause'); + assert.strictEqual(result.umpire_pause, end_ts + (30 * 1000)); + assert.strictEqual(result.umpire_wait, null); + }); + + _it('moves an expired technical official break back to the matching wait list', () => { + const official = make_official({ + status: 'pause', + umpire_wait: null, + service_judge_wait: null, + umpire_pause: 123456, + service_judge_pause: null, + }); + + const result = match_utils.apply_official_pause_expiry(official); + + assert.strictEqual(result.status, 'ready'); + assert.strictEqual(result.umpire_pause, null); + assert.strictEqual(result.umpire_wait, 123456); + assert.strictEqual(result.service_judge_wait, null); + }); + + _it('treats ready technical officials as checked in when check-in is configured per player', () => { + const official = make_official({ + checked_in: false, + umpire_on_court: null, + service_judge_on_court: null, + umpire_wait: 123, + service_judge_wait: null, + umpire_pause: null, + service_judge_pause: null, + inactive_list: null, + }); + + assert.strictEqual( + match_utils.get_effective_technical_official_checked_in(official, { + btp_settings: { check_in_per_match: false } + }), + true + ); + }); + + _it('treats paused or inactive technical officials as not checked in when check-in is configured per player', () => { + const paused_official = make_official({ + checked_in: true, + umpire_wait: null, + umpire_pause: 123456, + inactive_list: null, + }); + const inactive_official = make_official({ + checked_in: true, + umpire_wait: null, + umpire_pause: null, + inactive_list: 987654, + }); + + assert.strictEqual( + match_utils.get_effective_technical_official_checked_in(paused_official, { + btp_settings: { check_in_per_match: false } + }), + false + ); + assert.strictEqual( + match_utils.get_effective_technical_official_checked_in(inactive_official, { + btp_settings: { check_in_per_match: false } + }), + false + ); + }); + + _it('drops stale preparation state when highlight is cleared', () => { + const setup = { + state: 'preparation', + highlight: 0, + preparation_call_timestamp: 123456, + location_id: 'l1', + }; + + const result = match_utils.normalize_preparation_state(setup); + + assert.strictEqual(result.state, 'scheduled'); + assert.strictEqual(result.preparation_call_timestamp, undefined); + assert.strictEqual(result.location_id, 'l1'); + }); + + _it('auto-assigns only an umpire in on_preparation_call + umpire_only mode', (done) => { + const calls = []; + const original_assign_next_umpire_to_match = admin._assign_next_umpire_to_match; + const original_assign_next_service_judge_to_match = admin._assign_next_service_judge_to_match; + admin._assign_next_umpire_to_match = (_app, tournament_key, match_id) => { + calls.push(['umpire', tournament_key, match_id]); + return Promise.resolve(); + }; + admin._assign_next_service_judge_to_match = (_app, tournament_key, match_id) => { + calls.push(['service_judge', tournament_key, match_id]); + return Promise.resolve(); + }; + + match_utils.auto_assign_technical_officials_for_match( + {}, + { key: 't1', technical_official_auto_assignment_mode: 'on_preparation_call', official_rotation_mode: 'umpire_only' }, + 'm1', + (err) => { + admin._assign_next_umpire_to_match = original_assign_next_umpire_to_match; + admin._assign_next_service_judge_to_match = original_assign_next_service_judge_to_match; + assert.ifError(err); + assert.deepStrictEqual(calls, [['umpire', 't1', 'm1']]); + done(); + } + ); + }); + + _it('auto-assigns umpire and service judge in on_preparation_call + full mode', (done) => { + const calls = []; + const original_assign_next_umpire_to_match = admin._assign_next_umpire_to_match; + const original_assign_next_service_judge_to_match = admin._assign_next_service_judge_to_match; + admin._assign_next_umpire_to_match = (_app, tournament_key, match_id) => { + calls.push(['umpire', tournament_key, match_id]); + return Promise.resolve(); + }; + admin._assign_next_service_judge_to_match = (_app, tournament_key, match_id) => { + calls.push(['service_judge', tournament_key, match_id]); + return Promise.resolve(); + }; + + match_utils.auto_assign_technical_officials_for_match( + {}, + { key: 't1', technical_official_auto_assignment_mode: 'on_preparation_call', official_rotation_mode: 'umpire_and_service_judge' }, + 'm1', + (err) => { + admin._assign_next_umpire_to_match = original_assign_next_umpire_to_match; + admin._assign_next_service_judge_to_match = original_assign_next_service_judge_to_match; + assert.ifError(err); + assert.deepStrictEqual(calls, [['umpire', 't1', 'm1'], ['service_judge', 't1', 'm1']]); + done(); + } + ); + }); + + _it('still assigns a missing service judge when the umpire is already assigned', (done) => { + const calls = []; + const original_assign_next_umpire_to_match = admin._assign_next_umpire_to_match; + const original_assign_next_service_judge_to_match = admin._assign_next_service_judge_to_match; + admin._assign_next_umpire_to_match = (_app, tournament_key, match_id) => { + calls.push(['umpire', tournament_key, match_id]); + return Promise.reject(new Error('Match already has assigned umpire')); + }; + admin._assign_next_service_judge_to_match = (_app, tournament_key, match_id) => { + calls.push(['service_judge', tournament_key, match_id]); + return Promise.resolve(); + }; + + match_utils.auto_assign_technical_officials_for_match( + {}, + { key: 't1', technical_official_auto_assignment_mode: 'when_available', official_rotation_mode: 'umpire_and_service_judge' }, + 'm1', + (err, changed) => { + admin._assign_next_umpire_to_match = original_assign_next_umpire_to_match; + admin._assign_next_service_judge_to_match = original_assign_next_service_judge_to_match; + assert.ifError(err); + assert.strictEqual(changed, true); + assert.deepStrictEqual(calls, [['umpire', 't1', 'm1'], ['service_judge', 't1', 'm1']]); + done(); + } + ); + }); + + _it('does nothing outside on_preparation_call mode', (done) => { + const calls = []; + const original_assign_next_umpire_to_match = admin._assign_next_umpire_to_match; + admin._assign_next_umpire_to_match = (_app, tournament_key, match_id) => { + calls.push(['umpire', tournament_key, match_id]); + return Promise.resolve(); + }; + + match_utils.auto_assign_technical_officials_for_match( + {}, + { key: 't1', technical_official_auto_assignment_mode: 'manual_only', official_rotation_mode: 'umpire_and_service_judge' }, + 'm1', + (err) => { + admin._assign_next_umpire_to_match = original_assign_next_umpire_to_match; + assert.ifError(err); + assert.deepStrictEqual(calls, []); + done(); + } + ); + }); + + _it('also auto-assigns in when_available mode', (done) => { + const calls = []; + const original_assign_next_umpire_to_match = admin._assign_next_umpire_to_match; + const original_assign_next_service_judge_to_match = admin._assign_next_service_judge_to_match; + admin._assign_next_umpire_to_match = (_app, tournament_key, match_id) => { + calls.push(['umpire', tournament_key, match_id]); + return Promise.resolve(); + }; + admin._assign_next_service_judge_to_match = (_app, tournament_key, match_id) => { + calls.push(['service_judge', tournament_key, match_id]); + return Promise.resolve(); + }; + + match_utils.auto_assign_technical_officials_for_match( + {}, + { key: 't1', technical_official_auto_assignment_mode: 'when_available', official_rotation_mode: 'umpire_and_service_judge' }, + 'm1', + (err) => { + admin._assign_next_umpire_to_match = original_assign_next_umpire_to_match; + admin._assign_next_service_judge_to_match = original_assign_next_service_judge_to_match; + assert.ifError(err); + assert.deepStrictEqual(calls, [['umpire', 't1', 'm1'], ['service_judge', 't1', 'm1']]); + done(); + } + ); + }); + + _it('fills prepared matches when officials become available in when_available mode', (done) => { + const calls = []; + const pushes = []; + const original_assign_next_umpire_to_match = admin._assign_next_umpire_to_match; + const original_assign_next_service_judge_to_match = admin._assign_next_service_judge_to_match; + const original_update_highlight = btp_manager.update_highlight; + const original_fetch_all_location_preparation_selections = match_automation.fetch_all_location_preparation_selections; + + admin._assign_next_umpire_to_match = (_app, tournament_key, match_id) => { + calls.push(['umpire', tournament_key, match_id]); + return Promise.resolve(); + }; + admin._assign_next_service_judge_to_match = (_app, tournament_key, match_id) => { + calls.push(['service_judge', tournament_key, match_id]); + return Promise.resolve(); + }; + btp_manager.update_highlight = (_app, match) => { + pushes.push(match._id); + }; + match_automation.fetch_all_location_preparation_selections = async () => ([]); + + const matches = [ + { _id: 'm1', tournament_key: 't1', setup: { state: 'preparation', preparation_call_timestamp: 10 } }, + { _id: 'm2', tournament_key: 't1', setup: { state: 'preparation', preparation_call_timestamp: 20 } }, + ]; + const app = { + db: { + matches: { + find(query) { + assert.deepStrictEqual(query, { tournament_key: 't1', 'setup.state': 'preparation' }); + return { + sort(sortQuery) { + assert.deepStrictEqual(sortQuery, { 'setup.preparation_call_timestamp': 1 }); + return { + exec(cb) { + cb(null, matches); + } + }; + } + }; + }, + findOne(query, cb) { + cb(null, matches.find((m) => m._id === query._id) || null); + } + }, + umpires: { + find(_query, cb) { + cb(null, []); + } + } + } + }; + + match_utils.auto_assign_technical_officials_for_preparation_matches( + app, + { key: 't1', technical_official_auto_assignment_mode: 'when_available', official_rotation_mode: 'umpire_only' }, + (err) => { + admin._assign_next_umpire_to_match = original_assign_next_umpire_to_match; + admin._assign_next_service_judge_to_match = original_assign_next_service_judge_to_match; + btp_manager.update_highlight = original_update_highlight; + match_automation.fetch_all_location_preparation_selections = original_fetch_all_location_preparation_selections; + assert.ifError(err); + assert.deepStrictEqual(calls, [['umpire', 't1', 'm1'], ['umpire', 't1', 'm2']]); + assert.deepStrictEqual(pushes, ['m1', 'm2']); + done(); + } + ); + }); + + _it('adds likely next preparation matches after current preparation matches in when_available mode', (done) => { + const original_fetch_all_location_preparation_selections = match_automation.fetch_all_location_preparation_selections; + match_automation.fetch_all_location_preparation_selections = async () => ([ + { + selected_matches: [ + { _id: 'm2', tournament_key: 't1', setup: { state: 'scheduled' } }, + { _id: 'm3', tournament_key: 't1', setup: { state: 'scheduled' } }, + ], + }, + ]); + + const app = { + db: { + matches: { + find(query) { + assert.deepStrictEqual(query, { tournament_key: 't1', 'setup.state': 'preparation' }); + return { + sort(sortQuery) { + assert.deepStrictEqual(sortQuery, { 'setup.preparation_call_timestamp': 1 }); + return { + exec(cb) { + cb(null, [ + { _id: 'm1', tournament_key: 't1', setup: { state: 'preparation', preparation_call_timestamp: 10 } }, + ]); + } + }; + } + }; + } + }, + umpires: { + find(_query, cb) { + cb(null, []); + } + } + } + }; + + match_utils.fetch_technical_official_assignment_targets( + app, + { key: 't1', technical_official_auto_assignment_mode: 'when_available', official_rotation_mode: 'umpire_only' }, + (err, targets) => { + match_automation.fetch_all_location_preparation_selections = original_fetch_all_location_preparation_selections; + assert.ifError(err); + assert.deepStrictEqual(targets.map((match) => match._id), ['m1', 'm2', 'm3']); + done(); + } + ); + }); + + _it('deduplicates preparation matches against likely next preparation matches', (done) => { + const original_fetch_all_location_preparation_selections = match_automation.fetch_all_location_preparation_selections; + match_automation.fetch_all_location_preparation_selections = async () => ([ + { + selected_matches: [ + { _id: 'm1', tournament_key: 't1', setup: { state: 'preparation' } }, + { _id: 'm2', tournament_key: 't1', setup: { state: 'scheduled' } }, + ], + }, + ]); + + const app = { + db: { + matches: { + find() { + return { + sort() { + return { + exec(cb) { + cb(null, [ + { _id: 'm1', tournament_key: 't1', setup: { state: 'preparation', preparation_call_timestamp: 10 } }, + ]); + } + }; + } + }; + } + }, + umpires: { + find(_query, cb) { + cb(null, []); + } + } + } + }; + + match_utils.fetch_technical_official_assignment_targets( + app, + { key: 't1', technical_official_auto_assignment_mode: 'when_available', official_rotation_mode: 'umpire_only' }, + (err, targets) => { + match_automation.fetch_all_location_preparation_selections = original_fetch_all_location_preparation_selections; + assert.ifError(err); + assert.deepStrictEqual(targets.map((match) => match._id), ['m1', 'm2']); + done(); + } + ); + }); + + _it('fills remaining when_available target slots with global likely matches based on waiting umpires', (done) => { + const original_fetch_all_location_preparation_selections = match_automation.fetch_all_location_preparation_selections; + const original_fetch_global_preparation_candidates = match_automation.fetch_global_preparation_candidates; + match_automation.fetch_all_location_preparation_selections = async (_app, _tkey, options) => { + assert.strictEqual(options?.ignore_technical_officials_available_rule, true); + return ([ + { selected_matches: [] }, + ]); + }; + match_automation.fetch_global_preparation_candidates = async (_app, _tkey, options) => { + assert.strictEqual(options?.ignore_technical_officials_available_rule, true); + return ([ + { _id: 'm10', tournament_key: 't1', setup: { state: 'scheduled' } }, + { _id: 'm11', tournament_key: 't1', setup: { state: 'scheduled' } }, + { _id: 'm12', tournament_key: 't1', setup: { state: 'scheduled' } }, + { _id: 'm13', tournament_key: 't1', setup: { state: 'scheduled' } }, + ]); + }; + + const app = { + db: { + matches: { + find() { + return { + sort() { + return { + exec(cb) { + cb(null, []); + } + }; + } + }; + } + }, + umpires: { + find(query, cb) { + assert.deepStrictEqual(query, { tournament_key: 't1', umpire_wait: { $ne: null } }); + cb(null, [{ _id: 'u1' }, { _id: 'u2' }, { _id: 'u3' }]); + } + } + } + }; + + match_utils.fetch_technical_official_assignment_targets( + app, + { key: 't1', technical_official_auto_assignment_mode: 'when_available', official_rotation_mode: 'umpire_only' }, + (err, targets) => { + match_automation.fetch_all_location_preparation_selections = original_fetch_all_location_preparation_selections; + match_automation.fetch_global_preparation_candidates = original_fetch_global_preparation_candidates; + assert.ifError(err); + assert.deepStrictEqual(targets.map((match) => match._id), ['m10', 'm11', 'm12']); + done(); + } + ); + }); + + _it('ignores already staffed preparation matches when filling global fallback targets', (done) => { + const original_fetch_all_location_preparation_selections = match_automation.fetch_all_location_preparation_selections; + const original_fetch_global_preparation_candidates = match_automation.fetch_global_preparation_candidates; + match_automation.fetch_all_location_preparation_selections = async () => ([ + { selected_matches: [] }, + ]); + match_automation.fetch_global_preparation_candidates = async () => ([ + { _id: 'm10', tournament_key: 't1', setup: { state: 'scheduled' } }, + { _id: 'm11', tournament_key: 't1', setup: { state: 'scheduled' } }, + { _id: 'm12', tournament_key: 't1', setup: { state: 'scheduled' } }, + { _id: 'm13', tournament_key: 't1', setup: { state: 'scheduled' } }, + ]); + + const app = { + db: { + matches: { + find() { + return { + sort() { + return { + exec(cb) { + cb(null, [ + { + _id: 'prep-full', + tournament_key: 't1', + setup: { + state: 'preparation', + preparation_call_timestamp: 10, + umpire: { _id: 'u-prep' }, + }, + }, + ]); + } + }; + } + }; + } + }, + umpires: { + find(_query, cb) { + cb(null, [{ _id: 'u1' }, { _id: 'u2' }, { _id: 'u3' }]); + } + } + } + }; + + match_utils.fetch_technical_official_assignment_targets( + app, + { key: 't1', technical_official_auto_assignment_mode: 'when_available', official_rotation_mode: 'umpire_only' }, + (err, targets) => { + match_automation.fetch_all_location_preparation_selections = original_fetch_all_location_preparation_selections; + match_automation.fetch_global_preparation_candidates = original_fetch_global_preparation_candidates; + assert.ifError(err); + assert.deepStrictEqual(targets.map((match) => match._id), ['m10', 'm11', 'm12']); + done(); + } + ); + }); + + _it('prefers free courts that match the current tabletoperator queue order', () => { + const sorted = match_utils.sort_free_courts_for_auto_call( + [ + { _id: 'c1', num: 1 }, + { _id: 'c2', num: 2 }, + { _id: 'c3', num: 3 }, + ], + [ + { _id: 'to1', court: null, played_on_court: 'c2', start_ts: 10 }, + { _id: 'to2', court: null, played_on_court: 'c1', start_ts: 20 }, + ], + { tabletoperator_enabled: true } + ); + + assert.deepStrictEqual(sorted.map((court) => court._id), ['c2', 'c1', 'c3']); + }); +}); From 85d06fb1be55d94037cdc11df1a08ae771f96db5 Mon Sep 17 00:00:00 2001 From: Tim Lehr Date: Wed, 15 Apr 2026 08:16:30 +0200 Subject: [PATCH 224/224] Add admin UI for automation and official workflows --- static/css/admin.css | 679 +++++++++- static/css/toprow.css | 157 ++- static/icons/signal_yellow.svg | 6 +- static/js/announcements.js | 469 ++++++- static/js/change.js | 104 +- static/js/ci18n_de.js | 68 +- static/js/ci18n_en.js | 68 +- static/js/cmatch_official_select_helpers.js | 12 +- static/js/ctournament.js | 1358 +++++++++++++++++-- static/js/toprow.js | 1 + test/test_cmatch_official_select.js | 35 + 11 files changed, 2814 insertions(+), 143 deletions(-) diff --git a/static/css/admin.css b/static/css/admin.css index 0111154..b969b16 100644 --- a/static/css/admin.css +++ b/static/css/admin.css @@ -89,6 +89,466 @@ button { flex-direction: row; flex-wrap: wrap; justify-content: space-between; + gap: 0.5em 1em; +} + +.automation_controls_panel { + min-width: 260px; + padding: 0.15em 0.25em 0.25em 0.25em; + display: flex; + justify-content: center; +} + +.automation_orbit { + position: relative; + width: 140px; + height: 140px; + border-radius: 50%; + overflow: hidden; + border: 2px solid #000; + background: + conic-gradient( + from -45deg, + var(--automation-segment-top, #101010) 0deg 90deg, + var(--automation-segment-right, #101010) 90deg 180deg, + var(--automation-segment-bottom, #101010) 180deg 270deg, + var(--automation-segment-left, #101010) 270deg 360deg + ); +} + +.automation_orbit.is-global-active { + box-shadow: + 0 0 0 1px rgba(255, 255, 255, 0.18), + 0 3px 10px rgba(0, 0, 0, 0.18), + 0 10px 18px rgba(0, 0, 0, 0.12); +} + +.automation_orbit.is-paused-global { + filter: saturate(0.72) brightness(0.82); + box-shadow: + 0 0 0 2px rgba(150, 24, 24, 0.34), + 0 4px 14px rgba(70, 0, 0, 0.28), + 0 12px 22px rgba(55, 0, 0, 0.2); +} + +.automation_orbit::before, +.automation_orbit::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + pointer-events: none; +} + +.automation_orbit::before { + background: + linear-gradient(45deg, + transparent calc(50% - 2.5px), + rgba(255, 255, 255, 0.34) calc(50% - 2.5px), + rgba(0, 0, 0, 0.56) calc(50% + 2.5px), + transparent calc(50% + 2.5px) + ), + linear-gradient(-45deg, + transparent calc(50% - 2.5px), + rgba(255, 255, 255, 0.3) calc(50% - 2.5px), + rgba(0, 0, 0, 0.5) calc(50% + 2.5px), + transparent calc(50% + 2.5px) + ), + radial-gradient(circle at 32% 22%, + rgba(255, 255, 255, 0.28) 0%, + rgba(255, 255, 255, 0.16) 16%, + transparent 40% + ), + radial-gradient(circle at 66% 70%, + rgba(255, 255, 255, 0.08) 0%, + rgba(255, 255, 255, 0.04) 14%, + transparent 30% + ), + radial-gradient(circle at 72% 78%, + rgba(0, 0, 0, 0.1) 0%, + rgba(0, 0, 0, 0.04) 16%, + transparent 36% + ); + z-index: 2; +} + +.automation_orbit::after { + background: + radial-gradient(circle at 50% 50%, + transparent 54%, + rgba(255, 255, 255, 0.1) 70%, + rgba(0, 0, 0, 0.12) 100% + ); + box-shadow: + inset 0 3px 5px rgba(255, 255, 255, 0.32), + inset 0 -5px 8px rgba(0, 0, 0, 0.28), + inset 2px 0 3px rgba(255, 255, 255, 0.1), + inset -2px 0 3px rgba(0, 0, 0, 0.12); + z-index: 1; +} + +.automation_orbit.is-paused-global::after { + background: + radial-gradient(circle at 50% 50%, + rgba(130, 20, 20, 0.08) 0%, + rgba(90, 0, 0, 0.16) 62%, + rgba(40, 0, 0, 0.28) 100% + ), + rgba(64, 0, 0, 0.28); + box-shadow: + inset 0 3px 5px rgba(255, 210, 210, 0.18), + inset 0 -6px 10px rgba(30, 0, 0, 0.56), + inset 2px 0 3px rgba(255, 235, 235, 0.05), + inset -2px 0 3px rgba(45, 0, 0, 0.22); +} + +.automation_orbit.is-paused-global::before { + background: + linear-gradient(45deg, + transparent calc(50% - 3px), + rgba(255, 210, 210, 0.2) calc(50% - 3px), + rgba(40, 0, 0, 0.78) calc(50% + 3px), + transparent calc(50% + 3px) + ), + linear-gradient(-45deg, + transparent calc(50% - 3px), + rgba(255, 210, 210, 0.16) calc(50% - 3px), + rgba(40, 0, 0, 0.72) calc(50% + 3px), + transparent calc(50% + 3px) + ), + radial-gradient(circle at 28% 22%, + rgba(255, 230, 230, 0.14) 0%, + rgba(255, 230, 230, 0.06) 16%, + transparent 36% + ), + radial-gradient(circle at 72% 80%, + rgba(20, 0, 0, 0.3) 0%, + rgba(20, 0, 0, 0.12) 16%, + transparent 34% + ); +} + +.automation_center_toggle, +.automation_outer_toggle { + position: absolute; + border: none; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + box-sizing: border-box; + background: transparent; +} + +.automation_center_toggle { + inset: 0; + margin: auto; + width: 56px; + height: 56px; + border-radius: 50%; + font-size: 1.1em; + font-weight: 700; + border: 2px solid #000; + z-index: 4; + box-shadow: 0 0 0 3px #000; + flex-direction: column; + justify-content: flex-start; + padding-top: 9px; +} + +.automation_center_toggle.is-active { + background: linear-gradient(180deg, #f4fff6 0%, #cef3d6 100%); + border-color: #255f2d; + color: #255f2d; + box-shadow: + 0 0 0 3px #000, + 0 0 16px rgba(142, 232, 169, 0.28), + inset 0 2px 4px rgba(255, 255, 255, 0.55), + inset 0 -4px 6px rgba(44, 110, 47, 0.18); +} + +.automation_center_toggle.is-paused { + background: linear-gradient(180deg, #fff0f0 0%, #f0b2b2 100%); + border-color: #7a1414; + color: #6e0d0d; + box-shadow: + 0 0 0 3px #000, + 0 0 16px rgba(160, 0, 0, 0.22), + inset 0 2px 4px rgba(255, 255, 255, 0.28), + inset 0 -4px 6px rgba(120, 20, 20, 0.24); +} + +.automation_center_toggle_label { + position: relative; + z-index: 1; + display: block; + width: 100%; + text-align: center; + font-size: 9px; + line-height: 1; + letter-spacing: 0.12em; + font-weight: 800; + transform: translateY(-1px); +} + +.automation_center_toggle_icon_slot { + position: absolute; + left: 0; + right: 0; + top: 10px; + bottom: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.automation_center_toggle_icon { + position: absolute; + left: 50%; + top: 50%; + line-height: 1; + font-size: 24px; + transform: translate(-50%, -50%); + transition: opacity 120ms ease, transform 120ms ease; + pointer-events: none; +} + +.automation_center_toggle_icon.is-play { + font-size: 26px; + transform: translate(-50%, -50%); +} + +.automation_center_toggle_icon.is-pause { + font-size: 20px; + letter-spacing: -0.08em; + transform: translate(calc(-50% - 2px), -50%); +} + +.automation_center_toggle_icon_current { + opacity: 1; +} + +.automation_center_toggle_icon_preview { + opacity: 0; +} + +.automation_center_toggle:hover .automation_center_toggle_icon_current, +.automation_center_toggle:focus-visible .automation_center_toggle_icon_current { + opacity: 0; +} + +.automation_center_toggle:hover .automation_center_toggle_icon_current.is-play, +.automation_center_toggle:focus-visible .automation_center_toggle_icon_current.is-play { + transform: translate(-50%, -50%) scale(0.9); +} + +.automation_center_toggle:hover .automation_center_toggle_icon_current.is-pause, +.automation_center_toggle:focus-visible .automation_center_toggle_icon_current.is-pause { + transform: translate(calc(-50% - 2px), -50%) scale(0.9); +} + +.automation_center_toggle:hover .automation_center_toggle_icon_preview, +.automation_center_toggle:focus-visible .automation_center_toggle_icon_preview { + opacity: 1; +} + +.automation_center_toggle:hover .automation_center_toggle_icon_preview.is-play, +.automation_center_toggle:focus-visible .automation_center_toggle_icon_preview.is-play { + transform: translate(-50%, -50%) scale(1); +} + +.automation_center_toggle:hover .automation_center_toggle_icon_preview.is-pause, +.automation_center_toggle:focus-visible .automation_center_toggle_icon_preview.is-pause { + transform: translate(calc(-50% - 2px), -50%) scale(1); +} + +.automation_outer_toggle { + inset: 0; + z-index: 1; + clip-path: none; +} + +.automation_outer_toggle.is-reserved { + cursor: default; + color: #d9d9d9; +} + +.automation_outer_toggle:disabled { + pointer-events: none; +} + +.automation_outer_toggle.is-active { + color: #00c000; +} + +.automation_outer_toggle.is-paused { + color: #101010; +} + +.automation_outer_toggle.is-reserved { + color: #8e948d; +} + +.automation_outer_top::before { + content: none; +} + +.automation_outer_top { + clip-path: polygon(50% 50%, 0% 0%, 100% 0%); +} + +.automation_outer_right { + clip-path: polygon(50% 50%, 100% 0%, 100% 100%); +} + +.automation_outer_bottom { + clip-path: polygon(50% 50%, 100% 100%, 0% 100%); +} + +.automation_outer_left { + clip-path: polygon(50% 50%, 0% 100%, 0% 0%); +} + +.automation_outer_toggle_icon { + position: absolute; + width: 32px; + height: 32px; + background-repeat: no-repeat; + background-position: center center; + background-size: contain; + filter: none; + opacity: 0.9; + z-index: 3; + display: block; +} + +.automation_outer_toggle.is-active .automation_outer_toggle_icon { + filter: none; +} + +.automation_outer_toggle.is-reserved .automation_outer_toggle_icon { + filter: grayscale(0.2); + opacity: 0.75; +} + +.automation_icon_preparation { + background-image: url(../icons/preparation.svg); + left: calc(50% - 16px); + top: 15%; + transform: translateY(-50%); +} + +.automation_icon_rotation_disabled, +.automation_icon_rotation_umpire, +.automation_icon_rotation_dual { + left: calc(50% - 16px); + top: 15%; + transform: translateY(-50%); + background-size: contain; + background-position: center center; + background-repeat: no-repeat; +} + +.automation_icon_rotation_disabled { + background-image: url(../icons/no_umpire.svg); + filter: grayscale(1); + opacity: 0.5; +} + +.automation_icon_rotation_umpire { + background-image: url(../icons/umpire.svg); +} + +.automation_icon_rotation_dual { + background-image: none; +} + +.automation_icon_rotation_dual::before, +.automation_icon_rotation_dual::after { + content: ''; + position: absolute; + top: 50%; + width: 28px; + height: 28px; + background-size: contain; + background-position: center center; + background-repeat: no-repeat; + transform: translateY(-50%); +} + +.automation_icon_rotation_dual::before { + left: -18px; + background-image: url(../icons/umpire.svg); +} + +.automation_icon_rotation_dual::after { + right: -18px; + background-image: url(../icons/service_judge.svg); +} + +.automation_icon_rotation_dual { + position: absolute; + display: block; + overflow: visible; +} + +.automation_icon_rotation_dual_swap { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-size: 18px; + line-height: 1; + color: #444; + font-weight: 700; + opacity: 0.95; + pointer-events: none; + display: block; + z-index: 4; +} + +.automation_icon_rotation_dual > * { + display: block; +} + +.automation_icon_preparation_enabled, +.automation_icon_preparation_disabled { + background-image: url(../icons/preparation.svg); + right: 12%; + top: 50%; + transform: translate(50%, -50%); +} + +.automation_icon_preparation_disabled { + filter: grayscale(1); + opacity: 0.5; +} + +.automation_icon_oncourt_enabled, +.automation_icon_oncourt_disabled { + background-image: url(../icons/manual_call.svg); + left: calc(50% - 20px); + bottom: 15%; + transform: translateY(50%); +} + +.automation_icon_oncourt_disabled { + filter: grayscale(1); + opacity: 0.5; +} + +.automation_icon_tablet_enabled, +.automation_icon_tablet_disabled { + background-image: url(../icons/tablet.svg); + left: 12%; + top: 50%; + transform: translate(-50%, -50%); +} + +.automation_icon_tablet_disabled { + filter: grayscale(1); + opacity: 0.5; } .metadata_right_container_2 > table { @@ -138,19 +598,130 @@ a:hover { border: none; } +.tournament_settings .automation_group_box { + margin: 1em 0; + padding: 0.9em 1em 0.3em; + border: 1px solid #9f9f9f; + border-radius: 10px; + background: rgba(255, 255, 255, 0.35); + width: 100%; + min-width: 0; + box-sizing: border-box; +} + +.tournament_settings .automation_group_box.automation_group_box_content_disabled > *:not(legend) { + opacity: 0.58; +} + +.tournament_settings .automation_group_box > legend { + display: flex; + align-items: center; + gap: 0.5em; + padding: 0 0.35em; + font-weight: 700; +} + +.tournament_settings .automation_rule_box { + margin: 0.75em 0; + padding: 0.75em 0.9em; + border: 1px solid #bbb; + border-radius: 8px; + background: rgba(255, 255, 255, 0.45); + width: 100%; + max-width: 100%; + min-width: 0; + box-sizing: border-box; +} + +.tournament_settings .automation_rule_box.automation_rule_box_disabled { + opacity: 0.58; +} + +.tournament_settings .automation_rule_box.automation_rule_box_value_disabled > label.automation_rule_value { + opacity: 0.58; +} + +.tournament_settings .automation_rule_box > legend { + display: flex; + align-items: center; + gap: 0.5em; + padding: 0 0.3em; + font-weight: 600; +} + +.tournament_settings .automation_rule_box > label.automation_rule_value > span { + width: 260px; + min-width: 180px; +} + +.tournament_settings .automation_rule_box > label:not(.automation_rule_value) > span { + width: auto; + min-width: 0; + flex: 1 1 auto; +} + +.tournament_settings .automation_rule_unit { + margin-left: 0.6em; + color: #444; + font-size: 0.95em; +} + +.tournament_settings .automation_rule_box > label { + display: flex; + align-items: center; + width: 100%; + box-sizing: border-box; +} + +.tournament_settings .automation_rule_box > label > input[type=number] { + flex: 0 1 140px; + min-width: 0; +} + +.tournament_settings label.automation_suboption_checkbox { + margin-top: 0.8em; + padding-left: 0; +} + +.tournament_settings label.automation_suboption_checkbox > input[type=checkbox] { + margin-left: 1.43em; +} + +.tournament_settings label.automation_suboption_checkbox > span { + font-weight: 600; + margin-left: 0.56em; +} + +.tournament_settings label.automation_suboption_checkbox.automation_suboption_checkbox_disabled { + opacity: 0.58; +} + +.tournament_settings label.automation_suboption_checkbox_disabled { + opacity: 0.58; +} + +.tournament_settings .automation_suboption_hint { + margin: 0.15em 0 0 3.15em; + color: #666; + font-size: 0.9em; + display: none; +} + .tournament_settings > label, .tournament_settings fieldset > label , .tournament_settings > .settings > label, .tournament_settings fieldset > .settings > label { display: flex; align-items: center; + margin: 4px 0; } .tournament_settings > label > span, .tournament_settings fieldset > label > span, .tournament_settings > .settings > label > span, .tournament_settings fieldset > .settings > label > span { - display: inline-block; + display: flex; + align-items: center; width: 400px; min-width: fit-content; } @@ -173,6 +744,21 @@ a:hover { flex-grow: 1; } +.official_rotation_mode_control { + display: flex; + align-items: center; +} + +.official_rotation_mode_control > span { + display: inline-block; + width: 400px; + min-width: fit-content; +} + +.official_rotation_mode_control > select { + flex-grow: 1; +} + .tournament_settings { @@ -292,6 +878,40 @@ table.striped-table { justify-content: space-evenly; } +.announcement_speech_check_form { + gap: 0.4em; + margin-top: 0.5em; +} + +.announcement_speech_check_status { + display: flex; + flex-wrap: wrap; + gap: 0.35em; + align-items: baseline; +} + +.announcement_speech_check_label { + font-weight: 600; +} + +.announcement_speech_check_value_ok { + color: #0f6b1a; +} + +.announcement_speech_check_value_error, +.announcement_speech_check_value_timeout, +.announcement_speech_check_value_suspicious { + color: #a11b1b; +} + +.announcement_speech_check_value_running { + color: #8a5a00; +} + +.announcement_speech_check_value_unsupported { + color: #555; +} + .status_error, .status_waiting, .status_deactivated, @@ -324,6 +944,7 @@ table.striped-table { background-image: url(../icons/signal_black.svg); } + h2 { margin: 0; padding-top: 15px; @@ -631,6 +1252,10 @@ h2.edit { align-items: stretch; } +.officials_dual_row_inactive > .officials_dual_cell { + background-color: #00000040; +} + .officials_dual_center_space { background: #eee; padding: 0; @@ -647,10 +1272,6 @@ h2.edit { color: inherit; } -.officials_table_court_inactive { - filter: grayscale(1); - opacity: 0.45; -} .officials_role_toggle { display: flex; @@ -700,6 +1321,10 @@ h2.edit { padding: 8px 0; } +.official_on_court_row.official_on_court_row_single_role { + grid-template-columns: 65px minmax(0, 1fr); +} + .official_inactive_row { grid-template-columns: 65px minmax(0, 1fr); } @@ -712,6 +1337,14 @@ h2.edit { background: #ccc; } +.official_section_body > .official_on_court_row.official_on_court_row_inactive:nth-child(odd) { + background: #808080; +} + +.official_section_body > .official_on_court_row.official_on_court_row_inactive:nth-child(even) { + background: #8f8f8f; +} + .official_on_court_leading { display: flex; align-items: center; @@ -722,7 +1355,16 @@ h2.edit { font-weight: 700; } -.official_on_court_leading .court { +.official_on_court_leading .court, +.official_on_court_leading .court_inactive { + margin: 0 0.2em -0.2em 0.2em; + width: 1.5em; + min-width: 1.5em; + height: 1em; + min-height: 1em; + font-size: 1.1em; + line-height: 100%; + background-size: cover; transform: scale(1.5); transform-origin: center; } @@ -913,6 +1555,27 @@ h2.edit { flex: 1 1 auto; } +.official_card_timer { + flex: 0 0 auto; + display: flex; + align-items: center; +} + +.official_card_timer .timer { + min-width: 2.6em; + text-align: center; + font-size: 0.9em; + padding: 0.2em 0.3em 0.0em 0.3em; + margin: -0.1em 0.2em 0em 0.2em; + font-size: 0.7em; + background-color: (255, 0, 0); + color: (255, 255, 255); + border-radius: 0.4em; + font-weight: normal; + text-align: right; + font-family: "SEGMENT"; +} + .official_card_dragging { opacity: 1; } @@ -926,6 +1589,10 @@ h2.edit { cursor: pointer; } +.official_rotation_mode_umpire_only .official_card_trail { + display: none; +} + .official_card_frame .official_card_trail_icon { width: 1em; min-width: 1em; diff --git a/static/css/toprow.css b/static/css/toprow.css index e593c5c..184d570 100644 --- a/static/css/toprow.css +++ b/static/css/toprow.css @@ -1,9 +1,14 @@ .toprow { padding: 0.3em 0 0.3em 1em; - background-color: #00000020; + background-color: rgba(245, 245, 245, 0.7); border-bottom: 1px solid black; display: flex; + position: sticky; + top: 0; + z-index: 30; + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); } .toprow > .toprow_link, .toprow_right > .toprow_link { @@ -11,8 +16,13 @@ color: black; font-weight: bold; } + +.toprow > .toprow_link { + align-self: center; +} .toprow_sep { margin: 0 0.5em; + align-self: center; } .toprow_right { @@ -21,12 +31,127 @@ flex-direction: row; flex-wrap: wrap; justify-content: flex-end; + align-items: center; flex: 1; } .toprow_right > * + * { margin-left: 1.2em; } +.toprow_right > .status_label + .status_badge, +.toprow_right > .btp_status_label + .btp_status_badge, +.toprow_right > .ticker_status_label + .ticker_status_badge, +.toprow_right > .speech_output_status_label + .speech_output_status_badge { + margin-left: 0.3em; +} + +.toprow_service_badge { + display: inline-flex; + align-items: center; + justify-content: center; + position: relative; + width: 8.9em; + height: 2.1em; + font-size: 0.72em; + font-variant-numeric: tabular-nums; + font-weight: 700; + font-family: inherit; + line-height: 1; + color: #2c6e2f; + align-self: center; + padding: 0 0.55em; + text-align: center; + letter-spacing: 0.03em; + box-sizing: border-box; + vertical-align: middle; + --toprow-service-badge-bg: #adffcb; + --toprow-service-badge-border: #2c6e2f; + z-index: 0; +} + +.toprow_service_badge.status_connected, +.toprow_service_badge.status_connecting, +.toprow_service_badge.status_error, +.toprow_service_badge.status_waiting, +.toprow_service_badge.status_deactivated { + background-image: none; + background-size: initial; + background-repeat: initial; + background-position: initial; + min-height: 0; + margin: 0; +} + +.toprow_service_badge::before { + content: ''; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + border: 1px solid var(--toprow-service-badge-border); + border-radius: 0.45em; + background: var(--toprow-service-badge-bg); + z-index: -1; +} + +.toprow_service_badge.status_connected { + --toprow-service-badge-bg: #adffcb; + --toprow-service-badge-border: #2c6e2f; + color: #2c6e2f; +} + +.toprow_service_badge.status_connecting { + --toprow-service-badge-bg: #f2c200; + --toprow-service-badge-border: #6f5600; + color: #6f5600; +} + +.toprow_service_badge.status_error { + --toprow-service-badge-bg: #d40000; + --toprow-service-badge-border: #7a0000; + color: #ffffff; +} + +.toprow_service_badge.status_deactivated, +.toprow_service_badge.status_waiting { + --toprow-service-badge-bg: rgba(25, 25, 25, 0.78); + --toprow-service-badge-border: #000000; + color: #f7f7f7; +} + +.btp_status_badge.is-fetching { + --toprow-service-badge-bg: #adffcb; + --toprow-service-badge-border: #2c6e2f; + color: #2c6e2f; + font-family: inherit; + font-weight: 700; + justify-content: center; + text-align: center; +} + +.btp_status_badge.is-countdown { + justify-content: center; + text-align: center; +} + +.toprow_service_badge_prefix { + display: inline-block; + margin-right: 0.38em; + font-family: inherit; +} + +.toprow_service_badge_timer { + display: inline-block; + font-family: "SEGMENT"; + font-weight: normal; + font-size: calc(100% + 1px); + min-width: 2.6em; + text-align: right; + position: relative; + top: 2px; +} + .toprow_menu { position: relative; display: inline-block; @@ -48,12 +173,15 @@ right: 0; top: 100%; display: none; - min-width: 220px; - background: #fff; + min-width: 264px; + width: max-content; + background: rgba(245, 245, 245, 0.7); border: 1px solid #bbb; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18); padding: 0.35em 0; z-index: 20; + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); } .toprow_menu:hover::after, @@ -75,9 +203,27 @@ .toprow_menu_item { display: block; - padding: 0.45em 0.9em; + padding: 0.45em 1.1em; color: black; white-space: nowrap; + font-weight: normal; +} + +.toprow_menu_item::after { + content: attr(data-label); + display: block; + height: 0; + overflow: hidden; + visibility: hidden; + font-weight: bold; + pointer-events: none; +} + +.toprow_menu_item:link, +.toprow_menu_item:visited, +.toprow_menu_item:hover, +.toprow_menu_item:active { + color: black; } .toprow_menu_separator { @@ -87,6 +233,7 @@ } .toprow_menu_item:hover { - background: #f0f0f0; + background: rgba(225, 225, 225, 0.7); text-decoration: none; + font-weight: bold; } diff --git a/static/icons/signal_yellow.svg b/static/icons/signal_yellow.svg index 3febeab..d80e71d 100644 --- a/static/icons/signal_yellow.svg +++ b/static/icons/signal_yellow.svg @@ -83,11 +83,11 @@ transform="translate(-1.7859373,-2.1384077)" style="display:inline"> + rx="4.0762367" + ry="3.6958981" /> diff --git a/static/js/announcements.js b/static/js/announcements.js index 3a7e8c2..7c83e18 100644 --- a/static/js/announcements.js +++ b/static/js/announcements.js @@ -1,17 +1,23 @@ function getLocationID(matchSetup) { - let location = null; - if(matchSetup.court_id) { + if (!matchSetup) { + return null; + } + if (matchSetup.location_id) { + return matchSetup.location_id; + } + if (matchSetup.court_id) { const court = utils.find(curt.courts, c => c._id === matchSetup.court_id); - location = utils.find(curt.locations, l => l._id === court.location_id); - } else if (matchSetup.location_id) { - location = utils.find(curt.locations, l => l._id === matchSetup.location_id); - + if (court && court.location_id) { + return court.location_id; + } } - return location._id; + return null; } function announceNewMatch(matchSetup) { - if(!(window.localStorage.getItem('enable_announcement_calls_' + getLocationID(matchSetup)) === 'true')) { + const location_id = getLocationID(matchSetup); + const calls_enabled = window.localStorage.getItem('enable_announcement_calls_' + location_id) === 'true'; + if (!calls_enabled) { return; } const field = createFieldAnnouncement(matchSetup); @@ -30,7 +36,9 @@ function announceNewMatch(matchSetup) { } function announcePreparationMatch(matchSetup) { - if(!(window.localStorage.getItem('enable_announcement_preparations_' + getLocationID(matchSetup)) === 'true')) { + const location_id = getLocationID(matchSetup); + const preparations_enabled = window.localStorage.getItem('enable_announcement_preparations_' + location_id) === 'true'; + if (!preparations_enabled) { return; } const field = createFieldPreparationAnnouncement(matchSetup); @@ -414,9 +422,160 @@ let emergencyInterval = null; let emergencyAudio = null; const ANNOUNCEMENT_DEDUP_TTL_MS = 2500; const ANNOUNCEMENT_TAB_ID = `${Date.now()}_${Math.random().toString(36).slice(2)}`; +const ANNOUNCEMENT_LEADER_KEY = 'bts_announcement_leader'; +const ANNOUNCEMENT_LEADER_LEASE_MS = 500; +const ANNOUNCEMENT_LEADER_HEARTBEAT_MS = 200; +const ANNOUNCEMENT_RETRY_BROADCAST_KEY = 'bts_announcement_retry_request'; +const ANNOUNCEMENT_SPEECH_CHECK_STATE_KEY = 'bts_announcement_speech_check_state'; +const ANNOUNCEMENT_RETRY_TTL_MS = 5000; +const ANNOUNCEMENT_LEADER_FAILOVER_SUPPRESS_MS = 1500; const announcementPlaybackQueue = []; let announcementPlaybackActive = false; let announcementVoicesPromise = null; +let announcementLeaderHeartbeat = null; +let announcementLeaderLockHeld = false; +let announcementLeaderLockAcquire = null; +let releaseAnnouncementLeaderLock = null; +let announcementLeaderSuppressUntil = 0; +const processedAnnouncementRetryIds = new Set(); +let announcementSpeechCheckState = { + status: 'untested', + detail: '', + updated_at: null, +}; + +function readAnnouncementSpeechCheckStateStorage() { + try { + const raw = window.localStorage.getItem(ANNOUNCEMENT_SPEECH_CHECK_STATE_KEY); + return raw ? JSON.parse(raw) : null; + } catch (e) { + return null; + } +} + +function writeAnnouncementSpeechCheckStateStorage(state) { + try { + window.localStorage.setItem(ANNOUNCEMENT_SPEECH_CHECK_STATE_KEY, JSON.stringify(state)); + } catch (e) { + // ignore + } +} + +function readAnnouncementLeaderState() { + try { + return JSON.parse(window.localStorage.getItem(ANNOUNCEMENT_LEADER_KEY) || 'null'); + } catch (e) { + return null; + } +} + +function writeAnnouncementLeaderState(owner) { + try { + window.localStorage.setItem(ANNOUNCEMENT_LEADER_KEY, JSON.stringify({ + owner, + expires_at: Date.now() + ANNOUNCEMENT_LEADER_LEASE_MS, + })); + } catch (e) { + // ignore + } +} + +function releaseAnnouncementLeaderIfOwned() { + const current = readAnnouncementLeaderState(); + if (current && current.owner === ANNOUNCEMENT_TAB_ID) { + try { + window.localStorage.removeItem(ANNOUNCEMENT_LEADER_KEY); + } catch (e) { + // ignore + } + } +} + +function refreshAnnouncementLeader() { + if (announcementLeaderSuppressUntil > Date.now()) { + releaseAnnouncementLeaderIfOwned(); + return false; + } + const current = readAnnouncementLeaderState(); + const now = Date.now(); + if (!current || !current.owner || !current.expires_at || current.expires_at <= now || current.owner === ANNOUNCEMENT_TAB_ID) { + writeAnnouncementLeaderState(ANNOUNCEMENT_TAB_ID); + return true; + } + return current.owner === ANNOUNCEMENT_TAB_ID; +} + +function isAnnouncementLeader() { + const current = readAnnouncementLeaderState(); + return !!current && current.owner === ANNOUNCEMENT_TAB_ID && current.expires_at > Date.now(); +} + +function startAnnouncementLeaderHeartbeat() { + if (announcementLeaderHeartbeat != null) { + return; + } + refreshAnnouncementLeader(); + announcementLeaderHeartbeat = window.setInterval(() => { + refreshAnnouncementLeader(); + }, ANNOUNCEMENT_LEADER_HEARTBEAT_MS); + window.addEventListener('visibilitychange', () => { + refreshAnnouncementLeader(); + }); + window.addEventListener('beforeunload', () => { + releaseAnnouncementLeaderIfOwned(); + if (releaseAnnouncementLeaderLock) { + releaseAnnouncementLeaderLock(); + } + }); +} + +function ensureAnnouncementLeaderLock() { + if (announcementLeaderSuppressUntil > Date.now()) { + return Promise.resolve(false); + } + if (!(navigator && navigator.locks && typeof navigator.locks.request === 'function')) { + return Promise.resolve(isAnnouncementLeader() || refreshAnnouncementLeader()); + } + if (announcementLeaderLockHeld) { + return Promise.resolve(true); + } + if (announcementLeaderLockAcquire) { + return announcementLeaderLockAcquire; + } + + announcementLeaderLockAcquire = new Promise((resolve) => { + let resolved = false; + navigator.locks.request('bts-announcement-leader', { mode: 'exclusive', ifAvailable: true }, async (lock) => { + if (!lock) { + resolved = true; + resolve(false); + return false; + } + + announcementLeaderLockHeld = true; + resolved = true; + resolve(true); + + await new Promise((release) => { + releaseAnnouncementLeaderLock = () => { + releaseAnnouncementLeaderLock = null; + announcementLeaderLockHeld = false; + release(); + }; + }); + + return true; + }).catch(() => { + if (!resolved) { + resolve(false); + } + }).finally(() => { + announcementLeaderLockAcquire = null; + }); + }); + + return announcementLeaderLockAcquire; +} function buildAnnouncementClaimKey(matchSetup, kind) { const explicit = matchSetup && matchSetup._announcement_claim_key; @@ -437,10 +596,13 @@ function announcementFingerprint(callArray) { return String(hash); } -function claimAnnouncementPlayback(callArray, claimKey) { +function claimAnnouncementPlaybackSync(callArray, claimKey) { const now = Date.now(); const key = `announcement_claim_${claimKey || announcementFingerprint(callArray)}`; try { + if (!isAnnouncementLeader() && !refreshAnnouncementLeader()) { + return false; + } const raw = window.localStorage.getItem(key); if (raw) { const current = JSON.parse(raw); @@ -460,6 +622,32 @@ function claimAnnouncementPlayback(callArray, claimKey) { } } +function releaseAnnouncementPlaybackClaim(callArray, claimKey) { + const key = `announcement_claim_${claimKey || announcementFingerprint(callArray)}`; + try { + const raw = window.localStorage.getItem(key); + if (!raw) { + return; + } + const current = JSON.parse(raw); + if (current && current.owner === ANNOUNCEMENT_TAB_ID) { + window.localStorage.removeItem(key); + } + } catch (e) { + // ignore + } +} + +function claimAnnouncementPlayback(callArray, claimKey) { + const lockName = `bts-announcement-claim:${claimKey || announcementFingerprint(callArray)}`; + if (!(navigator && navigator.locks && typeof navigator.locks.request === 'function')) { + return Promise.resolve(claimAnnouncementPlaybackSync(callArray, claimKey)); + } + return navigator.locks.request(lockName, { mode: 'exclusive' }, () => { + return claimAnnouncementPlaybackSync(callArray, claimKey); + }); +} + function emergency_announce(enable) { @@ -525,25 +713,144 @@ function findAnnouncementVoice(voices) { return null; } -function playAnnouncementBatch(parts, voice, done) { +function setAnnouncementSpeechCheckState(status, detail) { + announcementSpeechCheckState = { + status, + detail: detail || '', + updated_at: Date.now(), + }; + writeAnnouncementSpeechCheckStateStorage(announcementSpeechCheckState); + window.dispatchEvent(new CustomEvent('announcement-speech-check-state-changed', { + detail: getAnnouncementSpeechCheckState(), + })); + return announcementSpeechCheckState; +} + +function getAnnouncementSpeechCheckState() { + const storedState = readAnnouncementSpeechCheckStateStorage(); + if (storedState && (!announcementSpeechCheckState.updated_at || (storedState.updated_at || 0) > (announcementSpeechCheckState.updated_at || 0))) { + announcementSpeechCheckState = storedState; + } + return { ...announcementSpeechCheckState }; +} + +function runAnnouncementSpeechCheck() { + if (!(window.speechSynthesis && typeof window.SpeechSynthesisUtterance === 'function')) { + return Promise.resolve(setAnnouncementSpeechCheckState('unsupported', ci18n('announcements:speechcheck:unsupported'))); + } + + return getAnnouncementVoices().then((voices) => { + const voice = findAnnouncementVoice(voices); + return new Promise((resolve) => { + let done = false; + let started = false; + const startedAt = Date.now(); + const finish = (status, detail) => { + if (done) { + return; + } + done = true; + resolve(setAnnouncementSpeechCheckState(status, detail)); + }; + + const words = new SpeechSynthesisUtterance(ci18n('announcements:speechcheck:text')); + words.lang = ci18n('announcements:lang'); + words.rate = curt.announcement_speed ? curt.announcement_speed : 1.05; + words.pitch = 0; + words.volume = 1; + words.voice = voice; + + const timeout = window.setTimeout(() => { + finish('timeout', ci18n('announcements:speechcheck:timeout')); + }, 4000); + + const wrappedFinish = (status, detail) => { + window.clearTimeout(timeout); + finish(status, detail); + }; + words.onstart = function () { + started = true; + }; + words.onend = function () { + const elapsed = Date.now() - startedAt; + if (!started || elapsed < 300) { + wrappedFinish('suspicious', ci18n('announcements:speechcheck:suspicious')); + return; + } + wrappedFinish('active', ci18n('announcements:speechcheck:ok')); + }; + words.onerror = function (event) { + const suffix = event && event.error ? ` (${event.error})` : ''; + wrappedFinish('error', ci18n('announcements:speechcheck:error') + suffix); + }; + + try { + window.speechSynthesis.cancel(); + window.speechSynthesis.speak(words); + } catch (e) { + window.clearTimeout(timeout); + finish('error', ci18n('announcements:speechcheck:error')); + } + }); + }).catch(() => { + return setAnnouncementSpeechCheckState('error', ci18n('announcements:speechcheck:error')); + }); +} + +function playAnnouncementBatch(parts, voice, done, claimKey) { const filteredParts = (parts || []).filter((part) => !!part); let index = 0; + let batchStarted = false; + let batchStatus = 'ok'; const playNext = () => { if (index >= filteredParts.length) { - done(); + done(batchStarted ? batchStatus : 'suspicious'); return; } + let utteranceStarted = false; + let utteranceStartedAt = 0; const words = new SpeechSynthesisUtterance(filteredParts[index]); words.lang = ci18n('announcements:lang'); words.rate = curt.announcement_speed ? curt.announcement_speed : 1.05; words.pitch = 0; words.volume = 1; words.voice = voice; + words.onstart = function () { + batchStarted = true; + utteranceStarted = true; + utteranceStartedAt = Date.now(); + console.log('[bts] announcement utterance start', { + claimKey: claimKey || null, + index, + part: filteredParts[index], + visibilityState: document.visibilityState, + }); + }; words.onend = function () { + const elapsed = utteranceStartedAt ? (Date.now() - utteranceStartedAt) : 0; + if (!utteranceStarted || elapsed < 300) { + if (batchStatus !== 'error') { + batchStatus = 'suspicious'; + } + } + console.log('[bts] announcement utterance end', { + claimKey: claimKey || null, + index, + part: filteredParts[index], + visibilityState: document.visibilityState, + }); index += 1; playNext(); }; - words.onerror = function () { + words.onerror = function (event) { + batchStatus = 'error'; + console.log('[bts] announcement utterance error', { + claimKey: claimKey || null, + index, + part: filteredParts[index], + error: event && event.error ? event.error : null, + visibilityState: document.visibilityState, + }); index += 1; playNext(); }; @@ -552,6 +859,75 @@ function playAnnouncementBatch(parts, voice, done) { playNext(); } +function releaseAnnouncementLeaderForFailover() { + announcementLeaderSuppressUntil = Date.now() + ANNOUNCEMENT_LEADER_FAILOVER_SUPPRESS_MS; + releaseAnnouncementLeaderIfOwned(); + if (releaseAnnouncementLeaderLock) { + releaseAnnouncementLeaderLock(); + } +} + +function requestAnnouncementRetry(batch) { + if (!batch || !batch.claimKey || (batch.retryCount || 0) >= 1) { + return; + } + const request = { + id: `${batch.claimKey}:${Date.now()}:${Math.random().toString(36).slice(2)}`, + claimKey: batch.claimKey, + callArray: batch.callArray, + excludeTabId: ANNOUNCEMENT_TAB_ID, + retryCount: (batch.retryCount || 0) + 1, + expiresAt: Date.now() + ANNOUNCEMENT_RETRY_TTL_MS, + }; + try { + window.localStorage.setItem(ANNOUNCEMENT_RETRY_BROADCAST_KEY, JSON.stringify(request)); + } catch (e) { + // ignore + } +} + +function handleAnnouncementRetryRequest(request) { + if (!request || !request.id || processedAnnouncementRetryIds.has(request.id)) { + return; + } + processedAnnouncementRetryIds.add(request.id); + if (request.excludeTabId === ANNOUNCEMENT_TAB_ID) { + return; + } + if (request.expiresAt && request.expiresAt < Date.now()) { + return; + } + announce(request.callArray || [], false, request.claimKey, { + retryCount: request.retryCount || 0, + allowRetry: false, + }); +} + +window.addEventListener('storage', function(event) { + if (event.key === ANNOUNCEMENT_SPEECH_CHECK_STATE_KEY && event.newValue) { + try { + const state = JSON.parse(event.newValue); + if (state && (!announcementSpeechCheckState.updated_at || (state.updated_at || 0) >= (announcementSpeechCheckState.updated_at || 0))) { + announcementSpeechCheckState = state; + window.dispatchEvent(new CustomEvent('announcement-speech-check-state-changed', { + detail: getAnnouncementSpeechCheckState(), + })); + } + } catch (e) { + // ignore + } + return; + } + if (event.key !== ANNOUNCEMENT_RETRY_BROADCAST_KEY || !event.newValue) { + return; + } + try { + handleAnnouncementRetryRequest(JSON.parse(event.newValue)); + } catch (e) { + // ignore + } +}); + function processAnnouncementPlaybackQueue() { if (announcementPlaybackActive) { return; @@ -563,26 +939,75 @@ function processAnnouncementPlaybackQueue() { announcementPlaybackActive = true; getAnnouncementVoices().then((voices) => { const voice = findAnnouncementVoice(voices); - playAnnouncementBatch(nextBatch.callArray, voice, () => { + console.log('[bts] announcement batch start', { + claimKey: nextBatch.claimKey || null, + parts: (nextBatch.callArray || []).filter(Boolean), + }); + playAnnouncementBatch(nextBatch.callArray, voice, (status) => { + console.log('[bts] announcement batch end', { + claimKey: nextBatch.claimKey || null, + }); + if (status === 'ok' || status === 'active') { + setAnnouncementSpeechCheckState('active', ci18n('announcements:speechcheck:ok')); + } else if (status === 'suspicious') { + setAnnouncementSpeechCheckState('suspicious', ci18n('announcements:speechcheck:suspicious')); + } else if (status === 'error') { + setAnnouncementSpeechCheckState('error', ci18n('announcements:speechcheck:error')); + } + if ((status === 'suspicious' || status === 'error') && nextBatch.allowRetry !== false) { + releaseAnnouncementPlaybackClaim(nextBatch.callArray, nextBatch.claimKey); + releaseAnnouncementLeaderForFailover(); + requestAnnouncementRetry(nextBatch); + } const pauseMs = curt.announcement_pause_time_ms ? (curt.announcement_pause_time_ms * 1000) : 2000; setTimeout(() => { announcementPlaybackActive = false; processAnnouncementPlaybackQueue(); }, pauseMs); - }); + }, nextBatch.claimKey); }).catch(() => { + console.log('[bts] announcement batch end', { + claimKey: nextBatch.claimKey || null, + error: true, + }); announcementPlaybackActive = false; processAnnouncementPlaybackQueue(); }); } -function announce(callArray, local, claimKey) { - if(!(window.localStorage.getItem('enable_free_announcements') === 'true') && !local) { - return; - } - if (!local && !claimAnnouncementPlayback(callArray, claimKey)) { +function announce(callArray, local, claimKey, options) { + const enqueue = (enqueueOptions) => { + const resolvedOptions = enqueueOptions || {}; + announcementPlaybackQueue.push({ + callArray, + claimKey, + retryCount: resolvedOptions.retryCount || 0, + allowRetry: resolvedOptions.allowRetry !== false, + }); + if (!local) { + console.log(`[bts] announcement played ${claimKey}`); + } + processAnnouncementPlaybackQueue(); + }; + + if (local) { + enqueue({}); return; } - announcementPlaybackQueue.push({ callArray }); - processAnnouncementPlaybackQueue(); + + Promise.resolve(ensureAnnouncementLeaderLock()).then((isLeader) => { + if (!isLeader) { + console.log(`[bts] announcement skipped ${claimKey}`); + return; + } + return Promise.resolve(claimAnnouncementPlayback(callArray, claimKey)).then((claimed) => { + if (!claimed) { + console.log(`[bts] announcement skipped ${claimKey}`); + return; + } + enqueue(options || {}); + }); + }).catch(() => { + console.log(`[bts] announcement skipped ${claimKey}`); + }); } diff --git a/static/js/change.js b/static/js/change.js index e873416..84392b3 100644 --- a/static/js/change.js +++ b/static/js/change.js @@ -87,11 +87,33 @@ function default_handler(rerender, special_funcs) { if (field === 'tabletoperator_enabled') { ctournament.refresh_current_view(); } + if (field === 'official_rotation_mode' && current_view === 'edit') { + ctournament.update_officials(); + } if (['btp_enabled', 'ticker_enabled'].includes(field)) { if (current_view === 'show') { ctournament.update_metadata_settings(); } } + if ([ + 'automation_enabled', + 'official_rotation_mode', + 'call_preparation_matches_automatically_enabled', + 'call_next_possible_scheduled_match_in_preparation', + 'tabletoperator_enabled', + ].includes(field) && current_view === 'show') { + ctournament.update_show_automation_controls(); + } + } + + function _official_pause_target_field(list_name) { + if (list_name === 'umpire_pause') { + return 'umpire_manual_pause'; + } + if (list_name === 'service_judge_pause') { + return 'service_judge_manual_pause'; + } + return list_name; } function change_score(cval) { @@ -148,7 +170,12 @@ function default_handler(rerender, special_funcs) { switch (c.ctype) { case 'free_announce': - _handle_announcement_event('free_announce', c.val, () => announce([c.val.text], false, _announcement_claim_key(c, 'free_announce'))); + _handle_announcement_event('free_announce', c.val, () => { + if (!(window.localStorage.getItem('enable_free_announcements') === 'true')) { + return; + } + announce([c.val.text], false, _announcement_claim_key(c, 'free_announce')); + }); break; case 'emergency_announce': curt.enable_emergency = c.val; @@ -199,7 +226,9 @@ function default_handler(rerender, special_funcs) { ctournament.update_logo(); break; case 'court_current_match': - //nothing to do here + if (current_view === 'show' && ctournament && typeof ctournament.update_location_preparation_need_labels === 'function') { + ctournament.update_location_preparation_need_labels(); + } break; case 'tabletoperator_add': //nothing to do here @@ -216,6 +245,9 @@ function default_handler(rerender, special_funcs) { case 'match_edit': ctournament.update_match(c); ctournament.update_officials(); + if (current_view === 'show' && ctournament && typeof ctournament.update_location_preparation_need_labels === 'function') { + ctournament.update_location_preparation_need_labels(); + } break; case 'match_add': const match_id = c.val.match__id; @@ -228,6 +260,9 @@ function default_handler(rerender, special_funcs) { } else { ctournament.add_match(c); } + if (current_view === 'show' && ctournament && typeof ctournament.update_location_preparation_need_labels === 'function') { + ctournament.update_location_preparation_need_labels(); + } break; case 'match_delete': { @@ -237,18 +272,35 @@ function default_handler(rerender, special_funcs) { cerror.silent('Cannot find deleted match ' + match_id); } rerender(); + if (current_view === 'show' && ctournament && typeof ctournament.update_location_preparation_need_labels === 'function') { + ctournament.update_location_preparation_need_labels(); + } } break; case 'courts_changed': curt.courts = c.val.all_courts; rerender(); + if (current_view === 'show' && ctournament && typeof ctournament.update_location_preparation_need_labels === 'function') { + ctournament.update_location_preparation_need_labels(); + } break; case 'court_changed': const court = utils.find(curt.courts, court => court._id === c.val.court_id); if(court) { court.is_active = c.val.is_active; + court.has_umpire = c.val.has_umpire; + court.has_service_judge = c.val.has_service_judge; + if (Object.prototype.hasOwnProperty.call(c.val, 'match_id')) { + court.match_id = c.val.match_id; + } } ctournament.update_court(court); + if (ctournament && typeof ctournament.update_officials === 'function') { + ctournament.update_officials(); + } + if (current_view === 'show' && ctournament && typeof ctournament.update_location_preparation_need_labels === 'function') { + ctournament.update_location_preparation_need_labels(); + } break; case 'locations_changed': curt.locations = c.val.all_locations; @@ -290,11 +342,11 @@ function default_handler(rerender, special_funcs) { c.val.match.setup._match_id = c.val.match__id; } _handle_announcement_event('match_preparation_call', c.val, () => announcePreparationMatch(c.val.match.setup)); - curt.matches.forEach((match) => { - match.setup.location_id = c.val.location_id; - }); ctournament.update_match(c); ctournament.update_upcoming_match(c); + if (current_view === 'show' && ctournament && typeof ctournament.update_location_preparation_need_labels === 'function') { + ctournament.update_location_preparation_need_labels(); + } break; case 'match_called_on_court': _attach_setup_announcement_claim(c, 'match_called_on_court'); @@ -368,19 +420,21 @@ function default_handler(rerender, special_funcs) { case 'advertisement_removed': ctournament.remove_advertisement(c); break; - //case 'update_player_status': { - // const cval = c.val; - // const id = cval.match__id; -// - // // Find the match - // const m = utils.find(curt.matches, m => m._id === id); - // if (!m) { - // cerror.silent('Cannot find match to update player status, ID: ' + JSON.stringify(id)); - // return; - // } - // m.btp_winner = cval.btp_winner; - // m.setup = cval.setup;} - // break; + case 'update_player_status': { + const cval = c.val; + const id = cval.match__id; + + const m = utils.find(curt.matches, m => m._id === id); + if (!m) { + cerror.silent('Cannot find match to update player status, ID: ' + JSON.stringify(id)); + return; + } + + m.btp_winner = cval.btp_winner; + m.setup = cval.setup; + cmatch.update_players(m); + break; + } case 'umpires_changed': apply_umpires_changed(c.val); break; @@ -408,7 +462,10 @@ function default_handler(rerender, special_funcs) { u.service_judge_wait = umpire.service_judge_wait; u.umpire_pause = umpire.umpire_pause; u.service_judge_pause = umpire.service_judge_pause; + u.umpire_manual_pause = umpire.umpire_manual_pause; + u.service_judge_manual_pause = umpire.service_judge_manual_pause; u.inactive_list = umpire.inactive_list; + u.checked_in = umpire.checked_in; } else { curt.umpires.push(umpire); } @@ -442,13 +499,15 @@ function default_handler(rerender, special_funcs) { official.inactive_list = null; official.service_judge_pause = null; official.umpire_pause = null; + official.service_judge_manual_pause = null; + official.umpire_manual_pause = null; official.service_judge_wait = null; official.umpire_wait = null; official.service_judge_on_court = null; official.umpire_on_court = null; official.is_planed_as_service_judge = false; official.is_planed_as_umpire = false; - official[c.val.to_list] = c.val.new_ts; + official[_official_pause_target_field(c.val.to_list)] = c.val.new_ts; ctournament.update_officials(); break; case 'official_edit': @@ -458,17 +517,24 @@ function default_handler(rerender, special_funcs) { break; case 'score': change_score(c.val); + if (current_view === 'show' && ctournament && typeof ctournament.update_location_preparation_need_labels === 'function') { + ctournament.update_location_preparation_need_labels(); + } // Most dialogs don't show any matches, so do not rerender break; case 'update_btp_settings': if(!curt.btp_settings) { curt.btp_settings = {}; } + const previous_check_in_per_match = curt.btp_settings.check_in_per_match; const btp_settings = c.val.btp_settings; for (const [key, value] of Object.entries(btp_settings)) { curt.btp_settings[key] = value; } ctournament.update_btp_settings_ui(); + if (previous_check_in_per_match !== curt.btp_settings.check_in_per_match) { + ctournament.refresh_current_view(); + } break; case 'update_btp_scoring_formats': if(!curt.scoring_formats) { diff --git a/static/js/ci18n_de.js b/static/js/ci18n_de.js index 021d331..9bf0a64 100644 --- a/static/js/ci18n_de.js +++ b/static/js/ci18n_de.js @@ -20,12 +20,14 @@ var ci18n_de = { 'All Locations': 'Alle Locations', 'only location': 'nur', 'edit tournament': 'bearbeiten', + 'edit BTS settings': 'BTS-Einstellungen bearbeiten', 'Court': 'Feld', 'Match': 'Spiel', 'Players': 'Spieler', 'Umpire': 'Schiedsrichter', 'State': 'Status', 'Umpire:': 'Schiedsrichter:', + 'Technical officials rotation:': 'Rotation der technischen Offiziellen', 'Service judge:': 'Aufschlagrichter:', 'Finished Matches': 'Abgeschlossene Spiele', 'Time:': 'Zeit:', @@ -52,6 +54,7 @@ var ci18n_de = { 'team competition': 'Mannschafts-Wettbewerb', 'nation competition': 'Internationales Turnier', 'update from BTP': 'aktualisieren', + 'update BTP data': 'BTP-Daten aktualisieren', 'update ticker': 'aktualisieren', 'Tournaments': 'Turniere', 'referee view': 'Referee-Ansicht', @@ -109,6 +112,16 @@ var ci18n_de = { 'announcements:game_for_place': 'Spiel um Platz', 'announcements:voice': 'Google Deutsch', 'announcements:lang': 'de-DE', + 'announcements:speechcheck:label': 'Sprachausgabe:', + 'announcements:speechcheck:button': 'Sprachtest', + 'announcements:speechcheck:untested': 'noch nicht getestet', + 'announcements:speechcheck:running': 'Test läuft ...', + 'announcements:speechcheck:ok': 'funktioniert', + 'announcements:speechcheck:error': 'fehlgeschlagen oder blockiert', + 'announcements:speechcheck:suspicious': 'verdächtig schnell beendet, vermutlich blockiert', + 'announcements:speechcheck:timeout': 'keine Rückmeldung vom Browser', + 'announcements:speechcheck:unsupported': 'vom Browser nicht unterstützt', + 'announcements:speechcheck:text': 'Dies ist ein Test der Sprachausgabe.', 'tournament:edit:add': 'Hinzufügen', 'tournament:edit:delete':'Löschen', @@ -197,6 +210,16 @@ var ci18n_de = { 'tournament:edit:tabletoperator_break_seconds': 'Pausenzeit nach Tabletbedienung (sec)', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Schiedrichter und Tabletbediener aufrufen', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Klapptafeln anstelle von Tablets in Nutzung', + 'tournament:edit:official_rotation_mode': 'Rotation der technischen Offiziellen:', + 'tournament:edit:official_rotation_mode:disabled': 'deaktiviert', + 'tournament:edit:official_rotation_mode:umpire_only': 'nur Schiedsrichterrotation', + 'tournament:edit:official_rotation_mode:umpire_and_service_judge': 'Schieds- und Aufschlagrichterrotation', + 'tournament:edit:technical_official_auto_assignment_mode': 'Automatisches Ansetzen:', + 'tournament:edit:technical_official_auto_assignment_mode:manual_only': 'nur manuell', + 'tournament:edit:technical_official_auto_assignment_mode:on_match_call_if_possible': 'beim Spielaufruf, wenn auf dem Feld möglich', + 'tournament:edit:technical_official_auto_assignment_mode:on_preparation_call': 'beim Aufruf in Vorbereitung', + 'tournament:edit:technical_official_auto_assignment_mode:when_available': 'sobald technische Offizielle verfügbar sind', + 'tournament:edit:technical_official_break_after_assignment_seconds': 'Pause nach Einsatz (sek):', 'tournament:edit:annoncement_include_event': 'Disziplin ansagen', 'tournament:edit:annoncement_include_round': 'Turnierrunden ansagen', 'tournament:edit:annoncement_include_matchnumber': 'Spielnummer ansagen', @@ -236,8 +259,46 @@ var ci18n_de = { 'tournament:edit:upcoming_matches_animation_pause': 'Animationsunterbrechung am Anfang und Ende der Seite (sec)', 'tournament:edit:upcoming_matches_max_count': 'Maximale Anzahl von Spielen in der Spielübersicht', 'tournament:edit:self_check_in_called_overlay_duration_ms': 'Einblenddauer der aufgerufenen Self-Check-In-Kachel (sek)', - 'tournament:edit:call_preparation_matches_automatically_enabled': 'Spiele in Vorbereitung automatisch auf freien Felden aufrufen', - 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Nächst mögliches Spiel automatisch in Vorbereitung aufrufen', + 'tournament:edit:call_preparation_matches_automatically_enabled': 'Automatik für Spiele in Vorbereitung aktivieren', + 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Automatik für Spiele aufrufen aktivieren', + 'tournament:edit:call_on_court_participant_readiness_mode': 'Teilnehmer-Regel', + 'tournament:edit:call_on_court_participant_readiness_mode:value': 'Voraussetzung', + 'tournament:edit:option:call_on_court_participant_readiness_mode:disabled': 'deaktiviert', + 'tournament:edit:option:call_on_court_participant_readiness_mode:checked_in': 'Alle Spieler müssen eingecheckt sein', + 'tournament:edit:option:call_on_court_participant_readiness_mode:pause_expired': 'Pausenzeiten aller beteiligten Spieler müssen abgelaufen sein', + 'tournament:edit:call_on_court_technical_officials_mode': 'Technische Offizielle', + 'tournament:edit:call_on_court_technical_officials_mode:value': 'Voraussetzung', + 'tournament:edit:option:call_on_court_technical_officials_mode:disabled': 'deaktiviert', + 'tournament:edit:option:call_on_court_technical_officials_mode:checked_in': 'Die Offiziellen müssen eingecheckt sein', + 'tournament:edit:option:call_on_court_technical_officials_mode:available': 'Offizielle müssen verfügbar sein', + 'tournament:edit:call_on_court_technical_officials_mode:hint_rotation_disabled': 'Nur verfügbar, wenn die Rotation der technischen Offiziellen aktiv ist.', + 'tournament:edit:call_on_court_technical_officials_mode:hint_auto_assignment_mode': '„Offizielle müssen verfügbar sein“ ist nur verfügbar bei automatischem Ansetzen „sobald technische Offizielle verfügbar sind“, „beim Aufruf in Vorbereitung“ oder „beim Spielaufruf, wenn auf dem Feld möglich“.', + 'tournament:edit:call_on_court_require_official_space_enabled': 'Verwende ein Feld nur, wenn es Platz für die angesetzten Offiziellen gibt', + 'tournament:edit:call_on_court_only_preparation_enabled': 'Nur Spiele in Vorbereitung aufrufen', + 'tournament:edit:call_on_court_only_preparation_minutes': 'Mindestzeit in Vorbereitung', + 'tournament:edit:call_on_court_time_limit_before_scheduled_enabled': 'Zeitregel vor eigener Ansetzung aktivieren', + 'tournament:edit:call_on_court_time_limit_before_scheduled_minutes': 'Frühestens so viele Minuten vor der eigenen Ansetzung', + 'tournament:edit:call_on_court_block_ahead_limit_enabled': 'Blockregel aktivieren', + 'tournament:edit:call_on_court_block_ahead_limit': 'Maximal so viele Blöcke voraus', + 'tournament:edit:call_on_court_time_ahead_of_frontier_enabled': 'Zeitregel relativ zum ersten nicht nutzbaren Spiel aktivieren', + 'tournament:edit:call_on_court_time_ahead_of_frontier_minutes': 'Maximal so viele Minuten später als das erste nicht nutzbare Spiel', + 'tournament:edit:call_on_court_matches_ahead_of_frontier_enabled': 'Spielanzahl-Regel relativ zum ersten nicht nutzbaren Spiel aktivieren', + 'tournament:edit:call_on_court_matches_ahead_of_frontier_limit': 'Maximal so viele Spiele voraus', + 'tournament:edit:call_on_court_player_pause_expired_enabled': 'Pausenzeiten aller beteiligten Spieler müssen abgelaufen sein', + 'tournament:edit:preparation_successor_rally_count': 'Ballwechsel bis ein Nachfolger in Vorbereitung benötigt wird', + 'tournament:edit:preparation_call_time_limit_before_scheduled_enabled': 'Zeitregel vor eigener Ansetzung aktivieren', + 'tournament:edit:preparation_call_time_limit_before_scheduled_minutes': 'Frühestens so viele Minuten vor der eigenen Ansetzung', + 'tournament:edit:preparation_call_block_ahead_limit_enabled': 'Blockregel aktivieren', + 'tournament:edit:preparation_call_block_ahead_limit': 'Maximal so viele Blöcke voraus', + 'tournament:edit:preparation_call_time_ahead_of_frontier_enabled': 'Zeitregel relativ zum ersten nicht nutzbaren Spiel aktivieren', + 'tournament:edit:preparation_call_time_ahead_of_frontier_minutes': 'Maximal so viele Minuten später als das erste nicht nutzbare Spiel', + 'tournament:edit:preparation_call_matches_ahead_of_frontier_enabled': 'Spielanzahl-Regel relativ zum ersten nicht nutzbaren Spiel aktivieren', + 'tournament:edit:preparation_call_matches_ahead_of_frontier_limit': 'Maximal so viele Spiele voraus', + 'tournament:edit:preparation_call_player_pause_expired_enabled': 'Pausenzeiten aller beteiligten Spieler müssen abgelaufen sein', + 'tournament:edit:preparation_call_technical_officials_available_enabled': 'Technische Offizielle müssen verfügbar sein', + 'tournament:edit:preparation_call_technical_officials_available_enabled:hint_rotation_disabled': 'Nur verfügbar, wenn die Rotation der technischen Offiziellen aktiv ist.', + 'tournament:edit:preparation_call_technical_officials_available_enabled:hint_auto_assignment_mode': 'Nur verfügbar bei automatischem Ansetzen „sobald technische Offizielle verfügbar sind“ oder „beim Aufruf in Vorbereitung“.', + 'tournament:edit:minutes': 'Minuten', 'tournament:edit:scoring_formats': 'Punktsysteme', 'tournament:edit:scoring_formats:dialog_title': 'Punktsystem bearbeiten', 'tournament:edit:scoring_formats:dialog_hint': 'Aus BTP importierte Felder sind schreibgeschützt. Bearbeitet werden nur lokale Timing-Werte.', @@ -287,6 +348,9 @@ var ci18n_de = { 'match:edit:now_on_court': 'Jetzt auf dem Feld', 'match:edit:error:service_judge_requires_umpire': 'Ein Aufschlagrichter kann nur gesetzt werden, wenn auch ein Schiedsrichter gesetzt ist.', 'match:edit:show_all_officials': 'Alle Offiziellen anzeigen', + 'match:edit:preparation': 'In Vorbereitung:', + 'match:edit:not_in_preparation': 'nicht in Vorbereitung', + 'match:edit:in_preparation_for': 'in Vorbereitung für {location_name}', 'match:edit:swap_hint': 'Tausch', 'match:delete:really': 'Wirklich Spiel {match_id} löschen?', 'match:add_umpire': 'Schiedsrichter hinzufügen', diff --git a/static/js/ci18n_en.js b/static/js/ci18n_en.js index 8719832..37cbb91 100644 --- a/static/js/ci18n_en.js +++ b/static/js/ci18n_en.js @@ -20,12 +20,14 @@ var ci18n_en = { 'All Locations': 'All Locations', 'only location': 'only', 'edit tournament': 'edit', + 'edit BTS settings': 'Edit BTS settings', 'Court': 'Court', 'Match': 'Match', 'Players': 'Players', 'Umpire': 'Umpire', 'State': 'State', 'Umpire:': 'Umpire:', + 'Technical officials rotation:': 'Technical officials rotation', 'Service judge:': 'Service judge:', 'Finished Matches': 'Finished Matches', 'Time:': 'Time:', @@ -52,6 +54,7 @@ var ci18n_en = { 'team competition': 'team competition', 'nation competition': 'international competition', 'update from BTP': 'update', + 'update BTP data': 'Update BTP data', 'update ticker': 'update', 'Tournaments': 'Tournaments', 'referee view': 'Referee view', @@ -108,6 +111,16 @@ var ci18n_en = { 'announcements:game_for_place': 'Game for place ', 'announcements:voice': 'Google UK English Male', 'announcements:lang': 'en-EN', + 'announcements:speechcheck:label': 'Speech output:', + 'announcements:speechcheck:button': 'Speech test', + 'announcements:speechcheck:untested': 'not tested yet', + 'announcements:speechcheck:running': 'test running ...', + 'announcements:speechcheck:ok': 'working', + 'announcements:speechcheck:error': 'failed or blocked', + 'announcements:speechcheck:suspicious': 'ended suspiciously fast, probably blocked', + 'announcements:speechcheck:timeout': 'no browser response', + 'announcements:speechcheck:unsupported': 'not supported by browser', + 'announcements:speechcheck:text': 'This is a speech output test.', 'tournament:edit:add': 'Add', 'tournament:edit:delete': 'Delete', @@ -198,6 +211,16 @@ var ci18n_en = { 'tournament:edit:tabletoperator_with_state_from_match_enabled': 'Call up national association of first player in match', 'tournament:edit:tabletoperator_with_umpire_enabled': 'Announce Umpire and Tabletoperator ', 'tournament:edit:tabletoperator_use_manual_counting_boards_enabled': 'Usage of Counting Boards instead of Tablets', + 'tournament:edit:official_rotation_mode': 'Technical officials rotation:', + 'tournament:edit:official_rotation_mode:disabled': 'disabled', + 'tournament:edit:official_rotation_mode:umpire_only': 'umpire rotation only', + 'tournament:edit:official_rotation_mode:umpire_and_service_judge': 'umpire and service judge rotation', + 'tournament:edit:technical_official_auto_assignment_mode': 'Automatic assignment:', + 'tournament:edit:technical_official_auto_assignment_mode:manual_only': 'manual only', + 'tournament:edit:technical_official_auto_assignment_mode:on_match_call_if_possible': 'on match call, if possible on court', + 'tournament:edit:technical_official_auto_assignment_mode:on_preparation_call': 'on preparation call', + 'tournament:edit:technical_official_auto_assignment_mode:when_available': 'as soon as technical officials are available', + 'tournament:edit:technical_official_break_after_assignment_seconds': 'Break after assignment (sec):', 'tournament:edit:annoncement_include_event': 'Announce event', 'tournament:edit:annoncement_include_round': 'Announce round of tournament', 'tournament:edit:annoncement_include_matchnumber': 'Announce number of match', @@ -234,8 +257,46 @@ var ci18n_en = { 'tournament:edit:upcoming_matches_animation_pause': 'Animation interruption at the beginning and end of the page (sec)', 'tournament:edit:upcoming_matches_max_count': 'Maximum number of games in the game overview', 'tournament:edit:self_check_in_called_overlay_duration_ms': 'Display duration of the called self check-in card (sec)', - 'tournament:edit:call_preparation_matches_automatically_enabled': 'Call games in preparation on free fields automatically', - 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Automatically call up next possible game in preparation', + 'tournament:edit:call_preparation_matches_automatically_enabled': 'Enable automation for matches in preparation', + 'tournament:edit:call_next_possible_scheduled_match_in_preparation': 'Enable automation for calling matches', + 'tournament:edit:call_on_court_participant_readiness_mode': 'Participant rule', + 'tournament:edit:call_on_court_participant_readiness_mode:value': 'Requirement', + 'tournament:edit:option:call_on_court_participant_readiness_mode:disabled': 'disabled', + 'tournament:edit:option:call_on_court_participant_readiness_mode:checked_in': 'All players must be checked in', + 'tournament:edit:option:call_on_court_participant_readiness_mode:pause_expired': 'All pause times of participating players must have expired', + 'tournament:edit:call_on_court_technical_officials_mode': 'Technical officials', + 'tournament:edit:call_on_court_technical_officials_mode:value': 'Requirement', + 'tournament:edit:option:call_on_court_technical_officials_mode:disabled': 'disabled', + 'tournament:edit:option:call_on_court_technical_officials_mode:checked_in': 'Officials must be checked in', + 'tournament:edit:option:call_on_court_technical_officials_mode:available': 'Officials must be available', + 'tournament:edit:call_on_court_technical_officials_mode:hint_rotation_disabled': 'Only available when the technical officials rotation is active.', + 'tournament:edit:call_on_court_technical_officials_mode:hint_auto_assignment_mode': '“Officials must be available” is only available with automatic assignment “as soon as technical officials are available”, “on preparation call”, or “on match call if possible on the court”.', + 'tournament:edit:call_on_court_require_official_space_enabled': 'Only use a court if there is room for the assigned officials', + 'tournament:edit:call_on_court_only_preparation_enabled': 'Only call matches already in preparation', + 'tournament:edit:call_on_court_only_preparation_minutes': 'Minimum time in preparation', + 'tournament:edit:call_on_court_time_limit_before_scheduled_enabled': 'Enable time rule before own scheduled time', + 'tournament:edit:call_on_court_time_limit_before_scheduled_minutes': 'At the earliest this many minutes before the own scheduled time', + 'tournament:edit:call_on_court_block_ahead_limit_enabled': 'Enable block rule', + 'tournament:edit:call_on_court_block_ahead_limit': 'At most this many blocks ahead', + 'tournament:edit:call_on_court_time_ahead_of_frontier_enabled': 'Enable time rule relative to the first unusable match', + 'tournament:edit:call_on_court_time_ahead_of_frontier_minutes': 'At most this many minutes later than the first unusable match', + 'tournament:edit:call_on_court_matches_ahead_of_frontier_enabled': 'Enable match-count rule relative to the first unusable match', + 'tournament:edit:call_on_court_matches_ahead_of_frontier_limit': 'At most this many matches ahead', + 'tournament:edit:call_on_court_player_pause_expired_enabled': 'All pause times of participating players must have expired', + 'tournament:edit:preparation_successor_rally_count': 'Rallies before a successor is needed in preparation', + 'tournament:edit:preparation_call_time_limit_before_scheduled_enabled': 'Enable time rule before own scheduled time', + 'tournament:edit:preparation_call_time_limit_before_scheduled_minutes': 'At the earliest this many minutes before the own scheduled time', + 'tournament:edit:preparation_call_block_ahead_limit_enabled': 'Enable block rule', + 'tournament:edit:preparation_call_block_ahead_limit': 'At most this many blocks ahead', + 'tournament:edit:preparation_call_time_ahead_of_frontier_enabled': 'Enable time rule relative to the first unusable match', + 'tournament:edit:preparation_call_time_ahead_of_frontier_minutes': 'At most this many minutes later than the first unusable match', + 'tournament:edit:preparation_call_matches_ahead_of_frontier_enabled': 'Enable match-count rule relative to the first unusable match', + 'tournament:edit:preparation_call_matches_ahead_of_frontier_limit': 'At most this many matches ahead', + 'tournament:edit:preparation_call_player_pause_expired_enabled': 'All pause times of participating players must have expired', + 'tournament:edit:preparation_call_technical_officials_available_enabled': 'Technical officials must be available', + 'tournament:edit:preparation_call_technical_officials_available_enabled:hint_rotation_disabled': 'Only available when technical officials rotation is enabled.', + 'tournament:edit:preparation_call_technical_officials_available_enabled:hint_auto_assignment_mode': 'Only available with automatic assignment set to “as soon as technical officials are available” or “on preparation call”.', + 'tournament:edit:minutes': 'minutes', 'tournament:edit:scoring_formats': 'Scoring formats', 'tournament:edit:scoring_formats:dialog_title': 'Edit scoring format', 'tournament:edit:scoring_formats:dialog_hint': 'Fields imported from BTP are read-only. Only local timing values can be edited.', @@ -282,6 +343,9 @@ var ci18n_en = { 'match:edit:now_on_court': 'Now on court', 'match:edit:error:service_judge_requires_umpire': 'A service judge can only be assigned if an umpire is also assigned.', 'match:edit:show_all_officials': 'Show all officials', + 'match:edit:preparation': 'In preparation:', + 'match:edit:not_in_preparation': 'not in preparation', + 'match:edit:in_preparation_for': 'in preparation for {location_name}', 'match:edit:swap_hint': 'Swap', 'match:delete:really': 'Really delete match {match_id}?', 'match:add_umpire': 'Add umpire', diff --git a/static/js/cmatch_official_select_helpers.js b/static/js/cmatch_official_select_helpers.js index 727fe88..79527b2 100644 --- a/static/js/cmatch_official_select_helpers.js +++ b/static/js/cmatch_official_select_helpers.js @@ -11,6 +11,8 @@ var cmatch_official_select_helpers = (function() { const append_separator = (label) => { entries.push({ type: 'separator', label: `--- ${label} ---` }); }; + const is_waiting_list_label = (label) => + label === ci18n_fn('Waiting list umpire') || label === ci18n_fn('Waiting list service judge'); const append_options = (items, role, secondary_mode = false) => { for (const official of items) { entries.push({ @@ -65,6 +67,8 @@ var cmatch_official_select_helpers = (function() { const secondary_wait_field = `${secondary_role}_wait`; const primary_pause_field = `${primary_role}_pause`; const secondary_pause_field = `${secondary_role}_pause`; + const primary_manual_pause_field = `${primary_role}_manual_pause`; + const secondary_manual_pause_field = `${secondary_role}_manual_pause`; const primary_wait_items = sort_by_wait( all_officials.filter((u) => u[primary_wait_field] != null && should_render_in_lower_lists(u)), primary_wait_field @@ -74,11 +78,11 @@ var cmatch_official_select_helpers = (function() { secondary_wait_field ); const primary_pause_items = sort_by_wait( - all_officials.filter((u) => u[primary_pause_field] != null && should_render_in_lower_lists(u)), + all_officials.filter((u) => (u[primary_pause_field] != null || u[primary_manual_pause_field] != null) && should_render_in_lower_lists(u)), primary_pause_field ); const secondary_pause_items = sort_by_wait( - all_officials.filter((u) => u[secondary_pause_field] != null && should_render_in_lower_lists(u)), + all_officials.filter((u) => (u[secondary_pause_field] != null || u[secondary_manual_pause_field] != null) && should_render_in_lower_lists(u)), secondary_pause_field ); const preparation_primary = sort_by_name(preparation_matches.map((match) => match.setup && match.setup[primary_role]).filter(Boolean)); @@ -95,7 +99,7 @@ var cmatch_official_select_helpers = (function() { ); const fallback_inactive_officials = all_officials .filter((u) => !visible_official_ids.has(u._id)) - .filter((u) => u.umpire_wait == null && u.service_judge_wait == null && u.umpire_pause == null && u.service_judge_pause == null && u.inactive_list == null) + .filter((u) => u.umpire_wait == null && u.service_judge_wait == null && u.umpire_pause == null && u.service_judge_pause == null && u.umpire_manual_pause == null && u.service_judge_manual_pause == null && u.inactive_list == null) .sort((a, b) => natcmp_fn(a.name || '', b.name || '')); fallback_inactive_officials.forEach((official) => { if (official.is_umpire && !official.is_service_judge) { @@ -127,7 +131,7 @@ var cmatch_official_select_helpers = (function() { let rendered_any = false; sections.forEach((section) => { if (!section.items.length) return; - if (rendered_any) { + if (rendered_any || !is_waiting_list_label(section.label)) { append_separator(section.label.replace(/:$/, '')); } append_options(section.items, section.role, section.secondary_mode); diff --git a/static/js/ctournament.js b/static/js/ctournament.js index 28dbd25..a840aef 100644 --- a/static/js/ctournament.js +++ b/static/js/ctournament.js @@ -17,6 +17,11 @@ let official_drag_image_el = null; let official_drag_active = false; let official_drag_refresh_pending = false; let pending_official_role_overrides = new Map(); +let preparation_selection_request_inflight = false; +let preparation_selection_request_pending = false; +let btp_next_fetch_countdown_interval = null; +let speech_output_badge_listener_registered = false; +const ANNOUNCEMENT_SPEECH_CHECK_STATE_STORAGE_KEY = 'bts_announcement_speech_check_state'; var ctournament = (function() { function _route_single(rex, func, handler) { @@ -39,6 +44,9 @@ var ctournament = (function() { } curt = response.tournament; + preparation_selection_request_inflight = false; + preparation_selection_request_pending = false; + curt.location_preparation_selection_by_location_id = {}; if (curt.language && curt.language !== 'auto') { ci18n.switch_language(curt.language); } @@ -153,6 +161,12 @@ var ctournament = (function() { m.presses = cval.presses; m.team1_won = cval.team1_won; m.shuttle_count = cval.shuttle_count; + if (cval.court_id !== undefined) { + m.setup.court_id = cval.court_id; + } + if (cval.now_on_court !== undefined) { + m.setup.now_on_court = cval.now_on_court; + } const new_section = cmatch.calc_section(m); if (old_section === new_section) { @@ -231,6 +245,7 @@ var ctournament = (function() { } const new_section = cmatch.calc_section(m); cmatch.update_match(m, old_section, new_section); + update_location_preparation_need_labels(); return old_section; } @@ -279,6 +294,7 @@ var ctournament = (function() { m.btp_winner = cval.match.btp_winner; const new_section = cmatch.calc_section(m); cmatch.update_match(m, old_section, new_section); + update_location_preparation_need_labels(); rerender_public_match_views(old_section, new_section); if (old_section != new_section || new_section == 'unassigned') { @@ -441,6 +457,202 @@ var ctournament = (function() { 'tabletoperator_set_break_after_tabletservice', 'tabletoperator_break_seconds', ].forEach(field_name => _set_disabled_by_name(field_name, !tabletoperator_enabled)); + uiu.qsEach('[name="tabletoperator_enabled"]', function(el) { + const box = el.closest('.automation_group_box'); + if (box) { + box.classList.toggle('automation_group_box_content_disabled', !tabletoperator_enabled); + } + }); + + const preparation_automation_enabled = !!curt.call_preparation_matches_automatically_enabled; + [ + 'preparation_successor_rally_count', + 'preparation_call_time_limit_before_scheduled_enabled', + 'preparation_call_time_limit_before_scheduled_minutes', + 'preparation_call_block_ahead_limit_enabled', + 'preparation_call_block_ahead_limit', + 'preparation_call_time_ahead_of_frontier_enabled', + 'preparation_call_time_ahead_of_frontier_minutes', + 'preparation_call_matches_ahead_of_frontier_enabled', + 'preparation_call_matches_ahead_of_frontier_limit', + 'preparation_call_technical_officials_available_enabled', + ].forEach(field_name => _set_disabled_by_name(field_name, !preparation_automation_enabled)); + const call_on_court_automation_enabled = !!curt.call_next_possible_scheduled_match_in_preparation; + [ + 'call_on_court_participant_readiness_mode', + 'call_on_court_technical_officials_mode', + 'call_on_court_require_official_space_enabled', + 'call_on_court_only_preparation_enabled', + 'call_on_court_only_preparation_minutes', + 'call_on_court_time_limit_before_scheduled_enabled', + 'call_on_court_time_limit_before_scheduled_minutes', + 'call_on_court_block_ahead_limit_enabled', + 'call_on_court_block_ahead_limit', + 'call_on_court_time_ahead_of_frontier_enabled', + 'call_on_court_time_ahead_of_frontier_minutes', + 'call_on_court_matches_ahead_of_frontier_enabled', + 'call_on_court_matches_ahead_of_frontier_limit', + ].forEach(field_name => _set_disabled_by_name(field_name, !call_on_court_automation_enabled)); + + const technical_official_rotation_enabled = (curt.official_rotation_mode || 'umpire_and_service_judge') !== 'disabled'; + const technical_official_auto_assignment_mode = curt.technical_official_auto_assignment_mode || 'manual_only'; + const preparation_officials_rule_mode_supported = + technical_official_auto_assignment_mode === 'when_available' || + technical_official_auto_assignment_mode === 'on_preparation_call'; + const preparation_officials_rule_enabled = + preparation_automation_enabled && + technical_official_rotation_enabled && + preparation_officials_rule_mode_supported; + if (!preparation_officials_rule_enabled && curt.preparation_call_technical_officials_available_enabled) { + curt.preparation_call_technical_officials_available_enabled = false; + const checkbox = document.querySelector('[name="preparation_call_technical_officials_available_enabled"]'); + if (checkbox) { + checkbox.checked = false; + } + send_single_prop('preparation_call_technical_officials_available_enabled', false, function(err) { + if (err) { + cerror.net(err); + } + }); + } + _set_disabled_by_name('preparation_call_technical_officials_available_enabled', !preparation_officials_rule_enabled); + uiu.qsEach('[name="preparation_call_technical_officials_available_enabled"]', function(el) { + const label = el.closest('label'); + if (label) { + label.classList.toggle('automation_suboption_checkbox_disabled', !preparation_officials_rule_enabled); + } + const hint = label ? label.nextElementSibling : null; + if (hint && hint.classList.contains('automation_suboption_hint')) { + let hint_key = null; + if (preparation_automation_enabled) { + if (!technical_official_rotation_enabled) { + hint_key = 'tournament:edit:preparation_call_technical_officials_available_enabled:hint_rotation_disabled'; + } else if (!preparation_officials_rule_mode_supported) { + hint_key = 'tournament:edit:preparation_call_technical_officials_available_enabled:hint_auto_assignment_mode'; + } + } + hint.style.display = hint_key ? 'block' : 'none'; + if (hint_key) { + uiu.text(hint, ci18n(hint_key)); + } + } + }); + const call_on_court_officials_rule_mode_supported = + technical_official_auto_assignment_mode === 'when_available' || + technical_official_auto_assignment_mode === 'on_preparation_call' || + technical_official_auto_assignment_mode === 'on_match_call_if_possible'; + const call_on_court_officials_select_enabled = + call_on_court_automation_enabled && + technical_official_rotation_enabled; + const call_on_court_officials_available_option_enabled = + call_on_court_officials_select_enabled && + call_on_court_officials_rule_mode_supported; + const call_on_court_officials_mode = curt.call_on_court_technical_officials_mode || 'disabled'; + if ((!call_on_court_officials_select_enabled && call_on_court_officials_mode !== 'disabled') || + (!call_on_court_officials_available_option_enabled && call_on_court_officials_mode === 'available')) { + curt.call_on_court_technical_officials_mode = 'disabled'; + const select = document.querySelector('[name="call_on_court_technical_officials_mode"]'); + if (select) { + select.value = 'disabled'; + } + send_single_prop('call_on_court_technical_officials_mode', 'disabled', function(err) { + if (err) { + cerror.net(err); + } + }); + } + _set_disabled_by_name('call_on_court_technical_officials_mode', !call_on_court_officials_select_enabled); + _set_disabled_by_name('call_on_court_require_official_space_enabled', !call_on_court_officials_select_enabled); + uiu.qsEach('[name="call_on_court_technical_officials_mode"]', function(el) { + const box = el.closest('.automation_rule_box'); + if (box) { + box.classList.toggle('automation_rule_box_disabled', !call_on_court_officials_select_enabled); + } + const available_option = el.querySelector('option[value="available"]'); + if (available_option) { + available_option.disabled = !call_on_court_officials_available_option_enabled; + } + const hint = box ? box.nextElementSibling : null; + if (hint && hint.classList.contains('automation_suboption_hint')) { + let hint_key = null; + if (call_on_court_automation_enabled) { + if (!technical_official_rotation_enabled) { + hint_key = 'tournament:edit:call_on_court_technical_officials_mode:hint_rotation_disabled'; + } else if (!call_on_court_officials_rule_mode_supported) { + hint_key = 'tournament:edit:call_on_court_technical_officials_mode:hint_auto_assignment_mode'; + } + } + hint.style.display = hint_key ? 'block' : 'none'; + if (hint_key) { + uiu.text(hint, ci18n(hint_key)); + } + } + }); + uiu.qsEach('[name="call_on_court_require_official_space_enabled"]', function(el) { + const label = el.closest('label'); + if (label) { + label.classList.toggle('automation_suboption_checkbox_disabled', !call_on_court_officials_select_enabled); + } + }); + uiu.qsEach('[name="call_preparation_matches_automatically_enabled"]', function(el) { + const box = el.closest('.automation_group_box'); + if (box) { + box.classList.toggle('automation_group_box_content_disabled', !preparation_automation_enabled); + } + }); + uiu.qsEach('[name="call_next_possible_scheduled_match_in_preparation"]', function(el) { + const box = el.closest('.automation_group_box'); + if (box) { + box.classList.toggle('automation_group_box_content_disabled', !call_on_court_automation_enabled); + } + }); + + [ + ['preparation_call_time_limit_before_scheduled_enabled', 'preparation_call_time_limit_before_scheduled_minutes'], + ['preparation_call_block_ahead_limit_enabled', 'preparation_call_block_ahead_limit'], + ['preparation_call_time_ahead_of_frontier_enabled', 'preparation_call_time_ahead_of_frontier_minutes'], + ['preparation_call_matches_ahead_of_frontier_enabled', 'preparation_call_matches_ahead_of_frontier_limit'], + ].forEach(([enabled_field, value_field]) => { + const enabled = !!curt[enabled_field]; + _set_disabled_by_name(value_field, !preparation_automation_enabled || !enabled); + uiu.qsEach('[name="' + enabled_field + '"]', function(el) { + const box = el.closest('.automation_rule_box'); + if (box) { + box.classList.toggle('automation_rule_box_disabled', !preparation_automation_enabled); + box.classList.toggle('automation_rule_box_value_disabled', preparation_automation_enabled && !enabled); + } + }); + }); + [ + 'call_on_court_participant_readiness_mode', + ['call_on_court_only_preparation_enabled', 'call_on_court_only_preparation_minutes'], + ['call_on_court_time_limit_before_scheduled_enabled', 'call_on_court_time_limit_before_scheduled_minutes'], + ['call_on_court_block_ahead_limit_enabled', 'call_on_court_block_ahead_limit'], + ['call_on_court_time_ahead_of_frontier_enabled', 'call_on_court_time_ahead_of_frontier_minutes'], + ['call_on_court_matches_ahead_of_frontier_enabled', 'call_on_court_matches_ahead_of_frontier_limit'], + ].forEach((entry) => { + if (typeof entry === 'string') { + uiu.qsEach('[name="' + entry + '"]', function(el) { + const box = el.closest('.automation_rule_box'); + if (box) { + box.classList.toggle('automation_rule_box_disabled', !call_on_court_automation_enabled); + } + }); + return; + } + const [enabled_field, value_field] = entry; + const enabled = !!curt[enabled_field]; + _set_disabled_by_name(value_field, !call_on_court_automation_enabled || !enabled); + uiu.qsEach('[name="' + enabled_field + '"]', function(el) { + const box = el.closest('.automation_rule_box'); + if (box) { + box.classList.toggle('automation_rule_box_disabled', !call_on_court_automation_enabled); + box.classList.toggle('automation_rule_box_value_disabled', call_on_court_automation_enabled && !enabled); + } + }); + }); + + apply_court_official_checkbox_dependencies(); } function set_live_settings_status(status_key) { @@ -531,6 +743,8 @@ var ctournament = (function() { on_error(el, old_value); return cerror.net(err); } + curt[field] = value; + update_edit_dependencies(); on_success(el, value); }); }); @@ -553,6 +767,7 @@ var ctournament = (function() { cmatch.render_courts(uiu.qs('.courts_container')); cmatch.render_unassigned(uiu.qs('.unassigned_container')); cmatch.render_finished(uiu.qs('.finished_container')); + update_location_preparation_need_labels(); } function _show_render_tabletoperators() { if(curt.tabletoperator_enabled) { @@ -848,6 +1063,43 @@ var ctournament = (function() { window.localStorage.setItem(checkboxId, checkbox.checked); }); } + + { + const form = uiu.el(container, 'form', 'announcement_speech_check_form'); + const statusWrap = uiu.el(form, 'div', 'announcement_speech_check_status'); + const statusLabel = uiu.el(statusWrap, 'span', 'announcement_speech_check_label', ci18n('announcements:speechcheck:label')); + const statusValue = uiu.el(statusWrap, 'span', 'announcement_speech_check_value'); + const button = uiu.el(form, 'button', { + type: 'button', + }, ci18n('announcements:speechcheck:button')); + + const updateSpeechCheckStatus = function(state) { + const current = state || (typeof getAnnouncementSpeechCheckState === 'function' + ? getAnnouncementSpeechCheckState() + : { status: 'unsupported', detail: ci18n('announcements:speechcheck:unsupported') }); + statusValue.className = 'announcement_speech_check_value announcement_speech_check_value_' + (current.status || 'untested'); + statusValue.textContent = current.detail || ci18n('announcements:speechcheck:untested'); + }; + + updateSpeechCheckStatus(); + + button.addEventListener('click', function() { + if (typeof runAnnouncementSpeechCheck !== 'function') { + updateSpeechCheckStatus({ status: 'unsupported', detail: ci18n('announcements:speechcheck:unsupported') }); + return; + } + button.disabled = true; + statusValue.className = 'announcement_speech_check_value announcement_speech_check_value_running'; + statusValue.textContent = ci18n('announcements:speechcheck:running'); + Promise.resolve(runAnnouncementSpeechCheck()).then((state) => { + updateSpeechCheckStatus(state); + button.disabled = false; + }).catch(() => { + updateSpeechCheckStatus({ status: 'error', detail: ci18n('announcements:speechcheck:error') }); + button.disabled = false; + }); + }); + } } function render_enable_location_courts(target, locations) { @@ -869,7 +1121,10 @@ var ctournament = (function() { checkbox.checked = (storedValue === null) ? true : (storedValue === 'true'); // Label anzeigen mit dem Location-Namen - uiu.el(form, 'label', { for: checkboxId }, loc.name + " ["+ loc.short_name +"]" || 'Unbenannte Location'); + uiu.el(form, 'label', { + for: checkboxId, + 'data-location-need-label': loc._id, + }, format_location_courts_label(loc)); // Event Listener zum Speichern in localStorage und Aufruf mit Parametern checkbox.addEventListener('change', function () { @@ -882,6 +1137,268 @@ var ctournament = (function() { }); } + function calculate_location_preparation_need_statuses() { + const courts = Array.isArray(curt.courts) ? curt.courts : []; + const matches = Array.isArray(curt.matches) ? curt.matches : []; + const status_by_location_id = new Map(); + const occupied_court_ids = new Set( + matches + .filter((match) => { + const setup = match && match.setup; + return !!setup && setup.now_on_court === true && setup.court_id; + }) + .map((match) => match.setup.court_id) + ); + + (curt.locations || []).forEach((loc) => { + const location_courts = courts.filter((court) => court && court.location_id === loc._id); + const active_location_courts = location_courts.filter((court) => court && court.is_active === true); + const active_location_court_ids = new Set(active_location_courts.map((court) => court._id)); + const successor_need_count = matches.filter((match) => { + const setup = match && match.setup; + return !!setup + && setup.now_on_court === true + && setup.needs_preparation_successor === true + && active_location_court_ids.has(setup.court_id); + }).length; + const free_court_count = active_location_courts.filter((court) => !occupied_court_ids.has(court._id)).length; + const active_court_count = active_location_courts.length; + const required_preparation_count = Math.min(active_court_count, successor_need_count + free_court_count); + const current_preparation_count = matches.filter((match) => { + const setup = match && match.setup; + return !!setup && setup.state === 'preparation' && setup.location_id === loc._id; + }).length; + status_by_location_id.set(loc._id, { + location_id: loc._id, + active_court_count, + successor_need_count, + free_court_count, + required_preparation_count, + current_preparation_count, + missing_preparation_count: Math.max(0, required_preparation_count - current_preparation_count), + }); + }); + + return status_by_location_id; + } + + function request_location_preparation_selections() { + if (!curt || !curt.key) { + return; + } + if (preparation_selection_request_inflight) { + preparation_selection_request_pending = true; + return; + } + preparation_selection_request_inflight = true; + send({ + type: 'preparation_selection_get', + tournament_key: curt.key, + }, function(err, response) { + preparation_selection_request_inflight = false; + if (err) { + if (preparation_selection_request_pending) { + preparation_selection_request_pending = false; + request_location_preparation_selections(); + } + return; + } + const selection_by_location_id = {}; + (response.selections || []).forEach((selection) => { + if (selection && selection.location_id != null) { + selection_by_location_id[String(selection.location_id)] = selection; + } + }); + curt.location_preparation_selection_by_location_id = selection_by_location_id; + const labels = document.querySelectorAll('[data-location-need-label]'); + if (labels.length) { + update_location_preparation_need_labels(false); + } + if (preparation_selection_request_pending) { + preparation_selection_request_pending = false; + request_location_preparation_selections(); + } + }); + } + + function format_location_courts_label(location) { + const location_name = (location.name + " [" + location.short_name + "]") || 'Unbenannte Location'; + return location_name; + } + + function update_location_preparation_need_labels(fetch_selections = true) { + const labels = document.querySelectorAll('[data-location-need-label]'); + if (!labels.length) { + return; + } + if (fetch_selections) { + request_location_preparation_selections(); + } + const statuses = calculate_location_preparation_need_statuses(); + labels.forEach((label) => { + const location_id = label.getAttribute('data-location-need-label'); + const location = utils.find(curt.locations || [], (loc) => String(loc._id) === String(location_id)); + if (!location) { + return; + } + const location_name = (location.name + " [" + location.short_name + "]") || 'Unbenannte Location'; + label.textContent = location_name; + }); + } + + function render_automation_controls(target) { + const container = uiu.el(target, 'div', 'automation_controls_panel'); + const overall_active = curt.automation_enabled !== false; + const orbit = uiu.el(container, 'div', 'automation_orbit' + (overall_active ? ' is-global-active' : ' is-paused-global')); + + const add_outer_toggle = function(position_class, icon_class, active, on_toggle, title, extra_class) { + const button = uiu.el(orbit, 'button', { + type: 'button', + 'class': [ + 'automation_outer_toggle', + position_class, + active ? 'is-active' : 'is-paused', + extra_class || '', + ].filter(Boolean).join(' '), + 'title': title, + 'aria-label': title, + }); + const icon = uiu.el(button, 'span', 'automation_outer_toggle_icon ' + icon_class); + if (icon_class === 'automation_icon_rotation_dual') { + uiu.el(icon, 'span', 'automation_icon_rotation_dual_swap', '⇄'); + } + if (typeof on_toggle === 'function') { + button.addEventListener('click', on_toggle); + } else { + button.disabled = true; + } + return button; + }; + + const preparation_active = !!curt.call_preparation_matches_automatically_enabled; + const on_court_active = !!curt.call_next_possible_scheduled_match_in_preparation; + const rotation_mode = curt.official_rotation_mode || 'umpire_and_service_judge'; + const next_rotation_mode = function(mode) { + if (mode === 'disabled') return 'umpire_only'; + if (mode === 'umpire_only') return 'umpire_and_service_judge'; + return 'disabled'; + }; + const orbit_segment_color = function(active, reserved) { + if (reserved) { + return '#8e948d'; + } + return active ? '#00c000' : '#101010'; + }; + orbit.style.setProperty('--automation-segment-top', orbit_segment_color(rotation_mode !== 'disabled', false)); + orbit.style.setProperty('--automation-segment-right', orbit_segment_color(preparation_active, false)); + const tabletoperator_enabled = !!curt.tabletoperator_enabled; + orbit.style.setProperty('--automation-segment-bottom', orbit_segment_color(on_court_active, false)); + orbit.style.setProperty('--automation-segment-left', orbit_segment_color(tabletoperator_enabled, false)); + + add_outer_toggle( + 'automation_outer_top', + rotation_mode === 'disabled' + ? 'automation_icon_rotation_disabled' + : (rotation_mode === 'umpire_only' + ? 'automation_icon_rotation_umpire' + : 'automation_icon_rotation_dual'), + rotation_mode !== 'disabled', + function() { + send_single_prop('official_rotation_mode', next_rotation_mode(rotation_mode), function(err) { + if (err) { + return cerror.net(err); + } + }); + }, + rotation_mode === 'disabled' + ? 'Rotation deaktiviert' + : (rotation_mode === 'umpire_only' + ? 'Nur Schiedsrichterrotation' + : 'Schieds- und Aufschlagrichterrotation') + ); + + add_outer_toggle( + 'automation_outer_right', + preparation_active + ? 'automation_icon_preparation_enabled' + : 'automation_icon_preparation_disabled', + preparation_active, + function() { + send_single_prop('call_preparation_matches_automatically_enabled', !preparation_active, function(err) { + if (err) { + return cerror.net(err); + } + }); + }, + 'Automatik fuer Spiele in Vorbereitung' + ); + + add_outer_toggle( + 'automation_outer_bottom', + on_court_active + ? 'automation_icon_oncourt_enabled' + : 'automation_icon_oncourt_disabled', + on_court_active, + function() { + send_single_prop('call_next_possible_scheduled_match_in_preparation', !on_court_active, function(err) { + if (err) { + return cerror.net(err); + } + }); + }, + 'Automatik fuer Spiele aufs Feld' + ); + + add_outer_toggle( + 'automation_outer_left', + tabletoperator_enabled + ? 'automation_icon_tablet_enabled' + : 'automation_icon_tablet_disabled', + tabletoperator_enabled, + function() { + send_single_prop('tabletoperator_enabled', !tabletoperator_enabled, function(err) { + if (err) { + return cerror.net(err); + } + }); + }, + 'Tabletbediener einsetzen' + ); + + const center = uiu.el(orbit, 'button', { + type: 'button', + 'class': 'automation_center_toggle ' + (overall_active ? 'is-active' : 'is-paused'), + 'title': overall_active ? 'Gesamte Automatik pausieren' : 'Gesamte Automatik starten', + 'aria-label': overall_active ? 'Gesamte Automatik pausieren' : 'Gesamte Automatik starten', + }); + uiu.el(center, 'span', 'automation_center_toggle_label', 'AUTO'); + const center_icon_slot = uiu.el(center, 'span', 'automation_center_toggle_icon_slot'); + uiu.el(center_icon_slot, 'span', 'automation_center_toggle_icon automation_center_toggle_icon_current ' + (overall_active ? 'is-play' : 'is-pause'), overall_active ? '▶' : '❚❚'); + uiu.el(center_icon_slot, 'span', 'automation_center_toggle_icon automation_center_toggle_icon_preview ' + (overall_active ? 'is-pause' : 'is-play'), overall_active ? '❚❚' : '▶'); + center.addEventListener('click', function() { + const next_value = !overall_active; + send_single_prop('automation_enabled', next_value, function(err) { + if (err) { + return cerror.net(err); + } + }); + }); + } + + function update_show_automation_controls() { + if (current_view !== 'show') { + return; + } + uiu.qsEach('.automation_controls_panel', function(panel) { + const parent = panel.parentNode; + if (!parent) { + return; + } + uiu.remove(panel); + render_automation_controls(parent); + }); + } + function build_location_view_menu_items() { const base_path = '/admin/t/' + encodeURIComponent(curt.key); const bup_lang = ((curt.language && curt.language !== 'auto') ? '&lang=' + encodeURIComponent(curt.language) : ''); @@ -894,15 +1411,17 @@ var ctournament = (function() { href: base_path + path_suffix, }]; - locations.forEach((loc) => { - const params = new URLSearchParams({ - location: loc.name, - }); - items.push({ - label: label + ' (' + ci18n('only location') + ' ' + loc.name + ')', - href: base_path + path_suffix + '?' + params.toString(), + if (locations.length > 1) { + locations.forEach((loc) => { + const params = new URLSearchParams({ + location: loc.name, + }); + items.push({ + label: label + ' (' + ci18n('only location') + ' ' + loc.name + ')', + href: base_path + path_suffix + '?' + params.toString(), + }); }); - }); + } items.push({ class: 'toprow_menu_separator', @@ -921,7 +1440,25 @@ var ctournament = (function() { view_items.pop(); } - return [{ + const items = [{ + label: ci18n('edit BTS settings'), + href: base_path + '/edit', + }]; + if (curt.btp_enabled) { + items.push({ + label: ci18n('update BTP data'), + func: ui_btp_fetch, + }); + } + if (curt.ticker_enabled) { + items.push({ + label: ci18n('update ticker'), + func: ui_ticker_push, + }); + } + items.push({ + class: 'toprow_menu_separator', + }, { label: ci18n('Scoreboard'), href: '/bup/#btsh_e=' + encodeURIComponent(curt.key) + '&display' + bup_dm_style + bup_lang, }, { @@ -932,11 +1469,163 @@ var ctournament = (function() { }, { class: 'toprow_menu_separator', }, - ...view_items]; + ...view_items); + + return items; } - function ui_show() { - current_view = 'show' - crouting.set('t/:key/', { key: curt.key }); + + function build_show_toprow_right_items() { + return [{ + label: 'BTS', + class: 'status_label', + }, { + label: '', + class: 'toprow_service_badge status_badge', + }, { + label: 'BTP', + class: 'btp_status_label', + }, { + label: '', + class: 'toprow_service_badge btp_status_badge', + }, { + label: 'Ticker', + class: 'ticker_status_label', + }, { + label: '', + class: 'toprow_service_badge ticker_status_badge', + }, { + label: 'Sprachausgabe', + class: 'speech_output_status_label', + }, { + label: '', + class: 'toprow_service_badge speech_output_status_badge', + }, { + label: '\u2630', + class: 'toprow_menu_button', + items: build_location_view_menu_items(), + }]; + } + + function set_badge_text(badge, text) { + if (!badge) { + return; + } + while (badge.firstChild) { + badge.removeChild(badge.firstChild); + } + badge.textContent = text; + } + + function set_btp_badge_countdown(badge, ms_remaining) { + if (!badge) { + return; + } + while (badge.firstChild) { + badge.removeChild(badge.firstChild); + } + uiu.el(badge, 'span', 'toprow_service_badge_prefix', 'Sync in:'); + uiu.el(badge, 'span', 'toprow_service_badge_timer', format_btp_next_fetch_remaining(ms_remaining)); + } + + function service_badge_text(status_name) { + switch (status_name) { + case 'connected': + return 'aktiv'; + case 'connecting': + return 'connecting'; + case 'error': + return 'error'; + case 'deactivated': + return 'aus'; + case 'waiting': + return 'wartet'; + default: + return status_name || ''; + } + } + + function speech_output_badge_text(status_name) { + switch (status_name) { + case 'active': + case 'ok': + return 'aktiv'; + case 'running': + return 'sync...'; + case 'untested': + return 'offen'; + case 'unsupported': + return 'kein support'; + case 'suspicious': + return 'unsicher'; + case 'timeout': + return 'timeout'; + case 'error': + return 'error'; + default: + return status_name || 'offen'; + } + } + + function speech_output_badge_class(status_name) { + switch (status_name) { + case 'active': + case 'ok': + return 'status_connected'; + case 'running': + return 'status_connected is-fetching'; + case 'untested': + return 'status_waiting'; + case 'unsupported': + return 'status_deactivated'; + case 'suspicious': + case 'timeout': + case 'error': + return 'status_error'; + default: + return 'status_waiting'; + } + } + + function update_service_badge(service_id, c) { + if (!c || !c.val) { + return; + } + const badge_class = service_id + '_badge'; + uiu.qsEach('.' + badge_class, (badge_el) => { + badge_el.className = 'toprow_service_badge ' + badge_class + ' status_' + c.val.status; + badge_el.title = c.val.message || ''; + set_badge_text(badge_el, service_badge_text(c.val.status)); + }); + } + + function update_speech_output_badge(state) { + const current = state || (typeof getAnnouncementSpeechCheckState === 'function' + ? getAnnouncementSpeechCheckState() + : { status: 'untested', detail: '' }); + uiu.qsEach('.speech_output_status_badge', (badge_el) => { + badge_el.className = 'toprow_service_badge speech_output_status_badge ' + speech_output_badge_class(current.status); + badge_el.title = current.detail || ''; + set_badge_text(badge_el, speech_output_badge_text(current.status)); + }); + } + + function ensure_speech_output_badge_listener() { + if (speech_output_badge_listener_registered) { + return; + } + speech_output_badge_listener_registered = true; + window.addEventListener('announcement-speech-check-state-changed', function(event) { + update_speech_output_badge(event && event.detail ? event.detail : null); + }); + window.addEventListener('storage', function(event) { + if (event.key !== ANNOUNCEMENT_SPEECH_CHECK_STATE_STORAGE_KEY) { + return; + } + update_speech_output_badge(); + }); + } + + function render_show_toprow() { toprow.set([{ label: ci18n('Tournaments'), func: ui_list, @@ -944,11 +1633,79 @@ var ctournament = (function() { label: curt.name || curt.key, func: ui_show, 'class': 'ct_name', - }], [{ - label: '\u2630', - class: 'toprow_menu_button', - items: build_location_view_menu_items(), - }]); + }], build_show_toprow_right_items()); + ensure_btp_next_fetch_countdown(); + ensure_speech_output_badge_listener(); + bts_status_changed({ val: curt.status || { status: 'connected', message: '' } }); + btp_status_changed({ val: curt.btp_status }); + ticker_status_changed({ val: curt.ticker_status || { status: 'deactivated', message: '' } }); + update_speech_output_badge(); + } + + function format_btp_next_fetch_remaining(ms_remaining) { + const total_seconds = Math.max(0, Math.ceil(ms_remaining / 1000)); + const minutes = Math.floor(total_seconds / 60); + const seconds = total_seconds % 60; + return minutes + ':' + String(seconds).padStart(2, '0'); + } + + function update_btp_next_fetch_countdown() { + const countdown = uiu.qs('.btp_status_badge'); + if (!countdown || !curt) { + return; + } + const btp_status = curt.btp_status || {}; + const next_fetch_ts = btp_status.next_fetch_ts; + const status_name = btp_status.status; + countdown.className = 'toprow_service_badge btp_status_badge'; + if (!curt.btp_enabled || !curt.btp_autofetch_enabled) { + set_badge_text(countdown, service_badge_text(status_name || 'deactivated')); + if (status_name) { + countdown.classList.add('status_' + status_name); + } + countdown.title = ''; + return; + } + if (status_name === 'error') { + set_badge_text(countdown, 'error'); + countdown.title = btp_status.message || 'BTP-Fehler'; + countdown.classList.add('status_error'); + return; + } + if (status_name === 'connecting') { + set_badge_text(countdown, 'connecting'); + countdown.title = btp_status.message || 'BTP verbindet'; + countdown.classList.add('status_connecting'); + return; + } + if (btp_status.fetch_in_progress) { + set_badge_text(countdown, 'sync...'); + countdown.title = 'BTP-Aktualisierung laeuft'; + countdown.classList.add('status_connected', 'is-fetching'); + return; + } + if (!next_fetch_ts) { + set_badge_text(countdown, service_badge_text(status_name || 'connected')); + countdown.title = btp_status.message || ''; + countdown.classList.add('status_' + (status_name || 'connected')); + return; + } + const ms_remaining = next_fetch_ts - Date.now(); + set_btp_badge_countdown(countdown, ms_remaining); + countdown.title = 'Naechste BTP-Aktualisierung'; + countdown.classList.add('status_connected', 'is-countdown'); + } + + function ensure_btp_next_fetch_countdown() { + if (btp_next_fetch_countdown_interval) { + return; + } + btp_next_fetch_countdown_interval = setInterval(update_btp_next_fetch_countdown, 1000); + } + function ui_show() { + current_view = 'show' + crouting.set('t/:key/', { key: curt.key }); + render_show_toprow(); const main = uiu.qs('.main'); uiu.empty(main); @@ -971,8 +1728,7 @@ var ctournament = (function() { const meta_right_top_div = uiu.el(meta_right_div, 'div', 'metadata_right_top_container'); render_enable_location_courts(meta_right_top_div, curt.locations); - - render_settings(meta_right_top_div); + render_automation_controls(meta_right_top_div); const errors_scroll_left_div = uiu.el(meta_right_div, 'div', 'errors_scroll_left'); @@ -1052,17 +1808,12 @@ var ctournament = (function() { if (current_view !== 'show') { return; } - const settings_div = document.querySelector('.metadata_right_container_2'); - if (!settings_div) { - return; - } - const target = settings_div.parentNode; - settings_div.remove(); - render_settings(target); + render_show_toprow(); } function btp_status_changed(c) { set_service_status('btp_status', c); + update_btp_next_fetch_countdown(); } function ticker_status_changed(c) { set_service_status('ticker_status', c); @@ -1081,6 +1832,9 @@ var ctournament = (function() { div_el.className = service_id + ' status_' + c.val.status; div_el.title = c.val.message; }); + if (service_id !== 'btp_status') { + update_service_badge(service_id, c); + } } } @@ -1428,12 +2182,98 @@ var ctournament = (function() { }); }; - const bts_fieldset = uiu.el(tournament_flow_div, 'fieldset'); - input.call_preparation_matches_automatically_enabled = create_checkbox(curt, bts_fieldset, 'call_preparation_matches_automatically_enabled'); - input.call_next_possible_scheduled_match_in_preparation = create_checkbox(curt, bts_fieldset, 'call_next_possible_scheduled_match_in_preparation'); + const bts_fieldset = uiu.el(tournament_flow_div, 'fieldset', 'automation_group_box'); + const bts_legend = uiu.el(bts_fieldset, 'legend'); + input.call_preparation_matches_automatically_enabled = uiu.el(bts_legend, 'input', { + type: 'checkbox', + name: 'call_preparation_matches_automatically_enabled', + }); + if (curt.call_preparation_matches_automatically_enabled) { + input.call_preparation_matches_automatically_enabled.checked = true; + } + uiu.el(bts_legend, 'span', {}, ci18n('tournament:edit:call_preparation_matches_automatically_enabled')); + bind_live_prop(input.call_preparation_matches_automatically_enabled, 'call_preparation_matches_automatically_enabled'); + input.preparation_successor_rally_count = create_numeric_input(curt, bts_fieldset, 'preparation_successor_rally_count', 1, 100, 11, 1); + input.preparation_call_player_pause_expired_enabled = create_checkbox(curt, bts_fieldset, 'preparation_call_player_pause_expired_enabled', 'automation_suboption_checkbox'); + input.preparation_call_technical_officials_available_enabled = create_checkbox(curt, bts_fieldset, 'preparation_call_technical_officials_available_enabled', 'automation_suboption_checkbox'); + input.preparation_call_technical_officials_available_hint = uiu.el(bts_fieldset, 'div', 'automation_suboption_hint'); + { + const rule = create_rule_limit_input(curt, bts_fieldset, 'preparation_call_time_limit_before_scheduled_enabled', 'preparation_call_time_limit_before_scheduled_minutes', 30, 0, 180, 1, 'tournament:edit:minutes'); + input.preparation_call_time_limit_before_scheduled_enabled = rule.enabled_input; + input.preparation_call_time_limit_before_scheduled_minutes = rule.value_input; + } + { + const rule = create_rule_limit_input(curt, bts_fieldset, 'preparation_call_block_ahead_limit_enabled', 'preparation_call_block_ahead_limit', 1, 0, 10, 1, null); + input.preparation_call_block_ahead_limit_enabled = rule.enabled_input; + input.preparation_call_block_ahead_limit = rule.value_input; + } + { + const rule = create_rule_limit_input(curt, bts_fieldset, 'preparation_call_time_ahead_of_frontier_enabled', 'preparation_call_time_ahead_of_frontier_minutes', 30, 0, 180, 1, 'tournament:edit:minutes'); + input.preparation_call_time_ahead_of_frontier_enabled = rule.enabled_input; + input.preparation_call_time_ahead_of_frontier_minutes = rule.value_input; + } + { + const rule = create_rule_limit_input(curt, bts_fieldset, 'preparation_call_matches_ahead_of_frontier_enabled', 'preparation_call_matches_ahead_of_frontier_limit', 1, 0, 50, 1, null); + input.preparation_call_matches_ahead_of_frontier_enabled = rule.enabled_input; + input.preparation_call_matches_ahead_of_frontier_limit = rule.value_input; + } + const free_courts_fieldset = uiu.el(tournament_flow_div, 'fieldset', 'automation_group_box'); + const free_courts_legend = uiu.el(free_courts_fieldset, 'legend'); + input.call_next_possible_scheduled_match_in_preparation = uiu.el(free_courts_legend, 'input', { + type: 'checkbox', + name: 'call_next_possible_scheduled_match_in_preparation', + }); + if (curt.call_next_possible_scheduled_match_in_preparation) { + input.call_next_possible_scheduled_match_in_preparation.checked = true; + } + uiu.el(free_courts_legend, 'span', {}, ci18n('tournament:edit:call_next_possible_scheduled_match_in_preparation')); + bind_live_prop(input.call_next_possible_scheduled_match_in_preparation, 'call_next_possible_scheduled_match_in_preparation'); + { + const rule = create_rule_limit_input(curt, free_courts_fieldset, 'call_on_court_only_preparation_enabled', 'call_on_court_only_preparation_minutes', 0, 0, 180, 1, 'tournament:edit:minutes'); + input.call_on_court_only_preparation_enabled = rule.enabled_input; + input.call_on_court_only_preparation_minutes = rule.value_input; + } + input.call_on_court_participant_readiness_mode = create_rule_select_input(curt, free_courts_fieldset, 'call_on_court_participant_readiness_mode', ['disabled', 'checked_in', 'pause_expired'], () => { + if (curt.call_on_court_player_pause_expired_enabled === true) { + return 'pause_expired'; + } + return 'disabled'; + }); + input.call_on_court_technical_officials_mode = create_rule_select_input(curt, free_courts_fieldset, 'call_on_court_technical_officials_mode', ['disabled', 'checked_in', 'available'], () => 'disabled'); + input.call_on_court_require_official_space_enabled = create_checkbox(curt, input.call_on_court_technical_officials_mode.rule_box, 'call_on_court_require_official_space_enabled'); + input.call_on_court_technical_officials_hint = uiu.el(input.call_on_court_technical_officials_mode.rule_box, 'div', 'automation_suboption_hint'); + { + const rule = create_rule_limit_input(curt, free_courts_fieldset, 'call_on_court_time_limit_before_scheduled_enabled', 'call_on_court_time_limit_before_scheduled_minutes', 30, 0, 180, 1, 'tournament:edit:minutes'); + input.call_on_court_time_limit_before_scheduled_enabled = rule.enabled_input; + input.call_on_court_time_limit_before_scheduled_minutes = rule.value_input; + } + { + const rule = create_rule_limit_input(curt, free_courts_fieldset, 'call_on_court_block_ahead_limit_enabled', 'call_on_court_block_ahead_limit', 1, 0, 10, 1, null); + input.call_on_court_block_ahead_limit_enabled = rule.enabled_input; + input.call_on_court_block_ahead_limit = rule.value_input; + } + { + const rule = create_rule_limit_input(curt, free_courts_fieldset, 'call_on_court_time_ahead_of_frontier_enabled', 'call_on_court_time_ahead_of_frontier_minutes', 30, 0, 180, 1, 'tournament:edit:minutes'); + input.call_on_court_time_ahead_of_frontier_enabled = rule.enabled_input; + input.call_on_court_time_ahead_of_frontier_minutes = rule.value_input; + } + { + const rule = create_rule_limit_input(curt, free_courts_fieldset, 'call_on_court_matches_ahead_of_frontier_enabled', 'call_on_court_matches_ahead_of_frontier_limit', 1, 0, 50, 1, null); + input.call_on_court_matches_ahead_of_frontier_enabled = rule.enabled_input; + input.call_on_court_matches_ahead_of_frontier_limit = rule.value_input; + } - const tablet_fieldset = uiu.el(tournament_flow_div, 'fieldset'); - input.tabletoperator_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_enabled'); + const tablet_fieldset = uiu.el(tournament_flow_div, 'fieldset', 'automation_group_box'); + const tablet_legend = uiu.el(tablet_fieldset, 'legend'); + input.tabletoperator_enabled = uiu.el(tablet_legend, 'input', { + type: 'checkbox', + name: 'tabletoperator_enabled', + }); + if (curt.tabletoperator_enabled) { + input.tabletoperator_enabled.checked = true; + } + uiu.el(tablet_legend, 'span', {}, ci18n('tournament:edit:tabletoperator_enabled')); + bind_live_prop(input.tabletoperator_enabled, 'tabletoperator_enabled'); input.tabletoperator_with_umpire_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_with_umpire_enabled'); input.tabletoperator_winner_of_quaterfinals_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_winner_of_quaterfinals_enabled'); input.tabletoperator_use_manual_counting_boards_enabled = create_checkbox(curt, tablet_fieldset, 'tabletoperator_use_manual_counting_boards_enabled'); @@ -1492,9 +2332,7 @@ var ctournament = (function() { input.self_check_in_called_overlay_duration_ms = create_duration_seconds_input(curt, upcoming_fieldset, 'self_check_in_called_overlay_duration_ms', 1, 60, 12, 0.5); } -// officials_host ###################################################################################################### - - // irgendwo in ui_edit (oder wo du initial renderst) + // officials_host ###################################################################################################### const officials_host = uiu.el(form, 'div', { id: 'officials_host' }); update_official_tables(officials_host); // initial + später auch für Updates @@ -3522,6 +4360,9 @@ function render_on_court_officials_table(main, { for (const row of rows) { const tr = uiu.el(table, 'div', 'officials_dual_row'); + if (row.is_inactive_court) { + tr.classList.add('officials_dual_row_inactive'); + } if (row.match_id) { tr.setAttribute('data-match-id', row.match_id); } @@ -3829,8 +4670,80 @@ function enable_preparation_official_dragdrop(preparation_table, lower_tbodies, function update_official_tables(officials_host) { officials_host.innerHTML = ''; + const official_rotation_mode = curt.official_rotation_mode || 'umpire_and_service_judge'; + const include_service_judges = official_rotation_mode === 'umpire_and_service_judge'; const officials_div = uiu.el(officials_host, 'div', 'settings'); - const all_officials = (curt.umpires || []).filter((official) => official && official._id); + if (!include_service_judges) { + officials_div.classList.add('official_rotation_mode_umpire_only'); + } + uiu.el(officials_div, 'h2', 'edit', ci18n('Technical officials rotation:')); + const official_rotation_mode_label = uiu.el(officials_div, 'label', 'official_rotation_mode_control'); + uiu.el(official_rotation_mode_label, 'span', {}, ci18n('tournament:edit:official_rotation_mode')); + const official_rotation_mode_select = uiu.el(official_rotation_mode_label, 'select', { + name: 'official_rotation_mode', + }); + [ + 'disabled', + 'umpire_only', + 'umpire_and_service_judge', + ].forEach((mode) => { + const attrs = { value: mode }; + if (official_rotation_mode === mode) { + attrs.selected = 'selected'; + } + uiu.el(official_rotation_mode_select, 'option', attrs, ci18n(`tournament:edit:official_rotation_mode:${mode}`)); + }); + bind_live_prop(official_rotation_mode_select, 'official_rotation_mode'); + + if (official_rotation_mode === 'disabled') { + return; + } + + const technical_official_auto_assignment_mode = curt.technical_official_auto_assignment_mode || 'manual_only'; + const technical_official_auto_assignment_mode_label = uiu.el(officials_div, 'label', 'official_rotation_mode_control'); + uiu.el(technical_official_auto_assignment_mode_label, 'span', {}, ci18n('tournament:edit:technical_official_auto_assignment_mode')); + const technical_official_auto_assignment_mode_select = uiu.el(technical_official_auto_assignment_mode_label, 'select', { + name: 'technical_official_auto_assignment_mode', + }); + [ + 'manual_only', + 'on_match_call_if_possible', + 'on_preparation_call', + 'when_available', + ].forEach((mode) => { + const attrs = { value: mode }; + if (technical_official_auto_assignment_mode === mode) { + attrs.selected = 'selected'; + } + uiu.el( + technical_official_auto_assignment_mode_select, + 'option', + attrs, + ci18n(`tournament:edit:technical_official_auto_assignment_mode:${mode}`) + ); + }); + bind_live_prop(technical_official_auto_assignment_mode_select, 'technical_official_auto_assignment_mode'); + + const technical_official_break_after_assignment_seconds_label = uiu.el(officials_div, 'label', 'official_rotation_mode_control'); + uiu.el(technical_official_break_after_assignment_seconds_label, 'span', {}, ci18n('tournament:edit:technical_official_break_after_assignment_seconds')); + const technical_official_break_after_assignment_seconds_input = uiu.el(technical_official_break_after_assignment_seconds_label, 'input', { + type: 'number', + name: 'technical_official_break_after_assignment_seconds', + value: curt.technical_official_break_after_assignment_seconds || 0, + min: 0, + max: 3600, + step: 1, + }); + bind_live_prop(technical_official_break_after_assignment_seconds_input, 'technical_official_break_after_assignment_seconds', { + get_value: input_el => Number(input_el.value), + }); + + const all_officials = (curt.umpires || []).filter((official) => { + if (!(official && official._id)) { + return false; + } + return include_service_judges || !!official.is_umpire; + }); const officialById = new Map(all_officials.map((official) => [official._id, official])); let dragged_meta = null; const sorted_courts = [...(curt.courts || [])].sort((a, b) => cbts_utils.natcmp(String(a.num || ''), String(b.num || ''))); @@ -3842,7 +4755,7 @@ function update_official_tables(officials_host) { const setup = match.setup || {}; return setup.state !== 'preparation' && !['oncourt', 'blocked', 'finished'].includes(setup.state) - && ((setup.umpire && setup.umpire._id) || (setup.service_judge && setup.service_judge._id)); + && ((setup.umpire && setup.umpire._id) || (include_service_judges && setup.service_judge && setup.service_judge._id)); }) .sort((a, b) => cbts_utils.natcmp(String(a.setup?.match_num || ''), String(b.setup?.match_num || ''))); const visible_official_ids = new Set(); @@ -3853,6 +4766,34 @@ function update_official_tables(officials_host) { }; const official_display_name = (official) => official.name || `${official.firstname || ''} ${official.surname || ''}`.trim(); + const get_official_pause_sort_ts = (official, role) => { + const auto_pause = official?.[`${role}_pause`]; + if (auto_pause != null) { + return Number(auto_pause) || 0; + } + const manual_pause = official?.[`${role}_manual_pause`]; + return Number(manual_pause) || 0; + }; + const create_official_pause_timer_state = (official, role) => { + const pause_target_ts = Number(official?.[`${role}_pause`]); + if (!Number.isFinite(pause_target_ts)) { + return null; + } + const remaining_ms = Math.max(0, pause_target_ts - Date.now()); + return { + settings: { + negative_timers: false, + }, + lang: 'de', + timer: { + duration: remaining_ms, + start: Date.now(), + upwards: false, + exigent: false, + }, + bgColor: '#ff0000', + }; + }; const official_card_variant_class = (variant, official = null) => { switch (variant) { case 'on_court': @@ -4115,12 +5056,16 @@ function update_official_tables(officials_host) { if (err) return error_handler(err); }); }; - const render_official_card = (parent, official, icon_class, drag_meta_factory = null, stack_drop_options = null, variant = 'list') => { + const render_official_card = (parent, official, icon_class, drag_meta_factory = null, stack_drop_options = null, variant = 'list', timer_state = null) => { const card = uiu.el(parent, 'div', `official_card_frame official_card_skin ${official_card_variant_class(variant, official)}`); card.setAttribute('data-official-id', official._id); card.draggable = !!drag_meta_factory; uiu.el(card, 'div', `official_card_icon ${icon_class}`); uiu.el(card, 'div', 'official_card_name', official_display_name(official)); + if (timer_state) { + const timer_host = uiu.el(card, 'div', 'official_card_timer'); + cmatch.create_timer(timer_state, timer_host, '#ffffff', '#ffffff'); + } const icon_trail = uiu.el(card, 'div', 'official_card_trail'); icon_trail.setAttribute('data-state', role_state_from_official(official)); icon_trail.draggable = false; @@ -4295,7 +5240,8 @@ function update_official_tables(officials_host) { icon_class, options.drag_meta_factory, options, - options.variant || 'list' + options.variant || 'list', + options.timer_state_factory ? options.timer_state_factory(official) : null ); append_drop(filtered_officials[index + 1]?._id || null); }); @@ -4357,8 +5303,10 @@ function update_official_tables(officials_host) { const render_match_assignment_row = (section, match, source_type, assign_type) => { const setup = match.setup || {}; mark_visible(setup.umpire); - mark_visible(setup.service_judge); - const row = uiu.el(section, 'div', 'official_on_court_row'); + if (include_service_judges) { + mark_visible(setup.service_judge); + } + const row = uiu.el(section, 'div', `official_on_court_row${include_service_judges ? '' : ' official_on_court_row_single_role'}`); const leading = uiu.el(row, 'div', 'official_on_court_leading official_preparation_leading'); uiu.text(leading, `#${setup.match_num || ''}`); const render_assignment_slot = (role, icon_class) => { @@ -4431,12 +5379,14 @@ function update_official_tables(officials_host) { } }; render_assignment_slot('umpire', 'umpire'); - render_assignment_slot('service_judge', 'service_judge'); + if (include_service_judges) { + render_assignment_slot('service_judge', 'service_judge'); + } }; const render_vertical_list_section = (title, specs, row_class = '') => { const section = create_official_section(title); section.classList.add('official_section_body_stacklist'); - const row = uiu.el(section, 'div', `official_on_court_row${row_class ? ' ' + row_class : ''}`); + const row = uiu.el(section, 'div', `official_on_court_row${include_service_judges ? '' : ' official_on_court_row_single_role'}${row_class ? ' ' + row_class : ''}`); uiu.el(row, 'div', 'official_on_court_leading official_preparation_leading'); specs.forEach((spec) => { const stack = uiu.el(row, 'div', 'official_card_stack'); @@ -4445,6 +5395,7 @@ function update_official_tables(officials_host) { can_drop: (meta) => meta_can_drop_to_list(meta, spec.to_list), drag_meta_factory: (official) => ({ source_type: 'list', official_id: official._id, from_list: spec.to_list, official }), variant: spec.variant || 'list', + timer_state_factory: spec.timer_state_factory, on_drop: (meta, before_official_id) => { if (meta.source_type === 'preparation' || meta.source_type === 'assigned') { remove_meta_from_match_to_list(stack, meta, spec.to_list, before_official_id); @@ -4456,24 +5407,42 @@ function update_official_tables(officials_host) { }); return { section, row }; }; - uiu.el(officials_div, 'h2', 'edit', ci18n('Umpire:')); const on_court_placeholder_row = create_official_section(ci18n('On court:')); sorted_courts.forEach((court) => { const court_umpire = curt.umpires.find((official) => official.umpire_on_court === court._id); - const court_service_judge = curt.umpires.find((official) => official.service_judge_on_court === court._id); + const court_service_judge = include_service_judges + ? curt.umpires.find((official) => official.service_judge_on_court === court._id) + : null; const has_match_on_court = !!court.match_id; mark_visible(court_umpire); - mark_visible(court_service_judge); - const court_row = uiu.el(on_court_placeholder_row, 'div', 'official_on_court_row'); + if (include_service_judges) { + mark_visible(court_service_judge); + } + const court_row = uiu.el(on_court_placeholder_row, 'div', `official_on_court_row${include_service_judges ? '' : ' official_on_court_row_single_role'}`); + if (court.is_active === false) { + court_row.classList.add('official_on_court_row_inactive'); + } const court_leading = uiu.el(court_row, 'div', 'official_on_court_leading'); - uiu.el(court_leading, 'div', 'court', String(court.num || '')); - const umpire_slot = uiu.el(court_row, 'div', 'official_card_frame official_card_drop'); - const service_judge_slot = uiu.el(court_row, 'div', 'official_card_frame official_card_drop'); + const is_active_court = court.is_active !== false; + const should_dim_court_icon = !has_match_on_court || !is_active_court; + const court_icon_class = [ + is_active_court ? 'court' : 'court_inactive', + should_dim_court_icon ? 'officials_table_court_inactive' : '' + ].filter(Boolean).join(' '); + uiu.el(court_leading, 'div', court_icon_class, is_active_court ? String(court.num || '') : ''); + const umpire_slot = uiu.el(court_row, 'div', 'official_on_court_slot official_card_frame official_card_drop official_card_drop_disabled'); + const service_judge_slot = include_service_judges + ? uiu.el(court_row, 'div', 'official_on_court_slot official_card_frame official_card_drop official_card_drop_disabled') + : null; register_drop_target(umpire_slot, () => false); - register_drop_target(service_judge_slot, () => false); + if (service_judge_slot) { + register_drop_target(service_judge_slot, () => false); + } if (!has_match_on_court) { umpire_slot.classList.add('official_card_drop_disabled'); - service_judge_slot.classList.add('official_card_drop_disabled'); + if (service_judge_slot) { + service_judge_slot.classList.add('official_card_drop_disabled'); + } } if (court_umpire) { render_official_card( @@ -4485,7 +5454,7 @@ function update_official_tables(officials_host) { 'on_court' ); } - if (court_service_judge) { + if (court_service_judge && service_judge_slot) { render_official_card( service_judge_slot, court_service_judge, @@ -4496,37 +5465,45 @@ function update_official_tables(officials_host) { ); } }); - const in_preparation_section = create_official_section(ci18n('In preparation:')); - preparation_matches.forEach((match) => { - render_match_assignment_row(in_preparation_section, match, 'preparation', 'assign_official_to_preparation_match'); - }); - const assigned_section = create_official_section(ci18n('Assigned to a match:')); - assigned_matches.forEach((match) => { - render_match_assignment_row(assigned_section, match, 'assigned', 'assign_official_to_match'); - }); + if (preparation_matches.length > 0) { + const in_preparation_section = create_official_section(ci18n('In preparation:')); + preparation_matches.forEach((match) => { + render_match_assignment_row(in_preparation_section, match, 'preparation', 'assign_official_to_preparation_match'); + }); + } + if (assigned_matches.length > 0) { + const assigned_section = create_official_section(ci18n('Assigned to a match:')); + assigned_matches.forEach((match) => { + render_match_assignment_row(assigned_section, match, 'assigned', 'assign_official_to_match'); + }); + } const should_render_in_lower_lists = (official) => !visible_official_ids.has(official._id); const waiting_umpires = [...(curt.umpires || [])] .filter((official) => official.umpire_wait && should_render_in_lower_lists(official)) .sort((a, b) => (a.umpire_wait || 0) - (b.umpire_wait || 0)); - const waiting_service_judges = [...(curt.umpires || [])] - .filter((official) => official.service_judge_wait && should_render_in_lower_lists(official)) - .sort((a, b) => (a.service_judge_wait || 0) - (b.service_judge_wait || 0)); + const waiting_service_judges = include_service_judges + ? [...(curt.umpires || [])] + .filter((official) => official.service_judge_wait && should_render_in_lower_lists(official)) + .sort((a, b) => (a.service_judge_wait || 0) - (b.service_judge_wait || 0)) + : []; const paused_umpires = [...(curt.umpires || [])] - .filter((official) => official.umpire_pause && should_render_in_lower_lists(official)) - .sort((a, b) => (a.umpire_pause || 0) - (b.umpire_pause || 0)); - const paused_service_judges = [...(curt.umpires || [])] - .filter((official) => official.service_judge_pause && should_render_in_lower_lists(official)) - .sort((a, b) => (a.service_judge_pause || 0) - (b.service_judge_pause || 0)); + .filter((official) => (official.umpire_pause != null || official.umpire_manual_pause != null) && should_render_in_lower_lists(official)) + .sort((a, b) => get_official_pause_sort_ts(a, 'umpire') - get_official_pause_sort_ts(b, 'umpire')); + const paused_service_judges = include_service_judges + ? [...(curt.umpires || [])] + .filter((official) => (official.service_judge_pause != null || official.service_judge_manual_pause != null) && should_render_in_lower_lists(official)) + .sort((a, b) => get_official_pause_sort_ts(a, 'service_judge') - get_official_pause_sort_ts(b, 'service_judge')) + : []; const inactive_officials = [...(curt.umpires || [])] .filter((official) => official.inactive_list && should_render_in_lower_lists(official)) .sort((a, b) => (a.inactive_list || 0) - (b.inactive_list || 0)); render_vertical_list_section(ci18n('Waiting for the next game:'), [ { items: waiting_umpires, icon_class: 'umpire', to_list: 'umpire_wait' }, - { items: waiting_service_judges, icon_class: 'service_judge', to_list: 'service_judge_wait' } + ...(include_service_judges ? [{ items: waiting_service_judges, icon_class: 'service_judge', to_list: 'service_judge_wait' }] : []) ]); render_vertical_list_section(ci18n('Currently on break:'), [ - { items: paused_umpires, icon_class: 'umpire', to_list: 'umpire_pause' }, - { items: paused_service_judges, icon_class: 'service_judge', to_list: 'service_judge_pause' } + { items: paused_umpires, icon_class: 'umpire', to_list: 'umpire_pause', timer_state_factory: (official) => create_official_pause_timer_state(official, 'umpire') }, + ...(include_service_judges ? [{ items: paused_service_judges, icon_class: 'service_judge', to_list: 'service_judge_pause', timer_state_factory: (official) => create_official_pause_timer_state(official, 'service_judge') }] : []) ]); const inactive_section = create_official_section(ci18n('Not available:')); inactive_section.classList.add('official_section_body_stacklist'); @@ -4535,6 +5512,7 @@ function update_official_tables(officials_host) { const inactive_stack = uiu.el(inactive_row, 'div', 'official_card_stack'); const fallback_inactive_officials = all_officials .filter((official) => !visible_official_ids.has(official._id)) + .filter((official) => official.umpire_wait == null && official.service_judge_wait == null && official.umpire_pause == null && official.service_judge_pause == null && official.umpire_manual_pause == null && official.service_judge_manual_pause == null && official.inactive_list == null) .sort((a, b) => cbts_utils.natcmp(String(official_display_name(a)), String(official_display_name(b)))); const all_inactive_officials = [...inactive_officials]; fallback_inactive_officials.forEach((official) => { @@ -4617,9 +5595,45 @@ function update_officials() { }); }); const umpire_td = uiu.el(tr, 'td', {}); - const umpire_cb = create_simple_checkbox(umpire_td, {'name' : 'umpire_cb', 'data-court-id': c._id, 'disabled': true,}, true); + const umpire_cb = create_simple_checkbox(umpire_td, {'name' : 'umpire_cb', 'data-court-id': c._id}, c.has_umpire !== false); + umpire_cb.addEventListener('change', (e) => { + const court_id = e.target.getAttribute('data-court-id'); + const court = utils.find(curt.courts, (entry) => entry._id === court_id); + if (court) { + court.has_umpire = e.target.checked; + update_court(court); + } + send_with_live_status({ + type: 'court_edit', + tournament_key: curt.key, + has_umpire: e.target.checked, + court_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); const service_judge_td = uiu.el(tr, 'td', {}); - const service_judge_cb = create_simple_checkbox(service_judge_td, {'name' : 'service_judge_cb', 'data-court-id': c._id, 'disabled': true,}, true); + const service_judge_cb = create_simple_checkbox(service_judge_td, {'name' : 'service_judge_cb', 'data-court-id': c._id}, c.has_service_judge !== false); + service_judge_cb.addEventListener('change', (e) => { + const court_id = e.target.getAttribute('data-court-id'); + const court = utils.find(curt.courts, (entry) => entry._id === court_id); + if (court) { + court.has_service_judge = e.target.checked; + update_court(court); + } + send_with_live_status({ + type: 'court_edit', + tournament_key: curt.key, + has_service_judge: e.target.checked, + court_id, + }, err => { + if (err) { + return cerror.net(err); + } + }); + }); const actions_td = uiu.el(tr, 'td', {}); const del_btn = uiu.el(actions_td, 'button', { 'data-court-id': c._id, @@ -4635,6 +5649,7 @@ function update_officials() { const nums = curt.courts.map(c => parseInt(c.num)); const maxnum = Math.max(0, Math.max.apply(null, nums)); + apply_court_official_checkbox_dependencies(); } function create_simple_checkbox(parant_el, attrs, is_checked) { @@ -4646,12 +5661,92 @@ function update_officials() { return result; } + function get_effective_court_official_checkbox_state(court) { + const rotation_mode = curt.official_rotation_mode || 'umpire_and_service_judge'; + const is_active = court && court.is_active !== false; + const stored_has_umpire = court && court.has_umpire !== false; + const stored_has_service_judge = court && court.has_service_judge !== false; + + if (!is_active) { + return { + umpire_checked: false, + umpire_disabled: true, + service_judge_checked: false, + service_judge_disabled: true, + }; + } + + if (rotation_mode === 'disabled') { + return { + umpire_checked: false, + umpire_disabled: true, + service_judge_checked: false, + service_judge_disabled: true, + }; + } + + if (rotation_mode === 'umpire_only') { + return { + umpire_checked: stored_has_umpire, + umpire_disabled: false, + service_judge_checked: false, + service_judge_disabled: true, + }; + } + + return { + umpire_checked: stored_has_umpire, + umpire_disabled: false, + service_judge_checked: stored_has_umpire ? stored_has_service_judge : false, + service_judge_disabled: !stored_has_umpire, + }; + } + + function apply_court_official_checkbox_dependencies(court = null) { + const courts_table = uiu.qs('.courts_table'); + if (!courts_table) { + return; + } + + const courts = court ? [court] : (curt.courts || []); + courts.forEach((current_court) => { + if (!current_court || !current_court._id) { + return; + } + const state = get_effective_court_official_checkbox_state(current_court); + const umpire_checkbox = courts_table.querySelector(`[name="umpire_cb"][data-court-id="${current_court._id}"]`); + if (umpire_checkbox) { + umpire_checkbox.checked = !!state.umpire_checked; + umpire_checkbox.disabled = !!state.umpire_disabled; + } + const service_judge_checkbox = courts_table.querySelector(`[name="service_judge_cb"][data-court-id="${current_court._id}"]`); + if (service_judge_checkbox) { + service_judge_checkbox.checked = !!state.service_judge_checked; + service_judge_checkbox.disabled = !!state.service_judge_disabled; + } + }); + } + function update_court(court) { switch (get_admin_subpage()){ case 'edit': const courts_table = uiu.qs('.courts_table'); - const checkbox = courts_table.querySelector(`[name="active_cb"][data-court-id="${court._id}"]`); - checkbox.checked = court.is_active; + if (!courts_table || !court) { + break; + } + const active_checkbox = courts_table.querySelector(`[name="active_cb"][data-court-id="${court._id}"]`); + if (active_checkbox) { + active_checkbox.checked = court.is_active; + } + const umpire_checkbox = courts_table.querySelector(`[name="umpire_cb"][data-court-id="${court._id}"]`); + if (umpire_checkbox) { + umpire_checkbox.checked = court.has_umpire !== false; + } + const service_judge_checkbox = courts_table.querySelector(`[name="service_judge_cb"][data-court-id="${court._id}"]`); + if (service_judge_checkbox) { + service_judge_checkbox.checked = court.has_service_judge !== false; + } + apply_court_official_checkbox_dependencies(court); break; default: cmatch.update_court(court); @@ -4659,8 +5754,8 @@ function update_officials() { } } - function create_checkbox(curt, parent_el, filed_id) { - const label = uiu.el(parent_el, 'label'); + function create_checkbox(curt, parent_el, filed_id, label_class) { + const label = uiu.el(parent_el, 'label', label_class); const attrs = { type: 'checkbox', name: filed_id, @@ -4728,6 +5823,100 @@ function update_officials() { return result; } + function create_select_input(curt, parent_el, filed_id, values) { + const label = uiu.el(parent_el, 'label'); + uiu.el(label, 'span', {}, ci18n('tournament:edit:' + filed_id)); + const result = uiu.el(label, 'select', { + name: filed_id, + }); + const current_value = curt[filed_id] == null ? 'none' : String(curt[filed_id]); + for (const value of values) { + const value_str = String(value); + const attrs = { value: value_str }; + if (current_value === value_str) { + attrs.selected = 'selected'; + } + uiu.el(result, 'option', attrs, ci18n(`tournament:edit:option:${filed_id}:${value_str}`)); + } + bind_live_prop(result, filed_id, { + get_value: input_el => input_el.value === 'none' ? 'none' : Number(input_el.value), + }); + return result; + } + + function create_rule_select_input(curt, parent_el, filed_id, values, fallback_value_fn) { + const box = uiu.el(parent_el, 'fieldset', 'automation_rule_box'); + const legend = uiu.el(box, 'legend'); + uiu.el(legend, 'span', {}, ci18n('tournament:edit:' + filed_id)); + const value_label = uiu.el(box, 'label', 'automation_rule_value'); + uiu.el(value_label, 'span', {}, ci18n('tournament:edit:' + filed_id + ':value')); + const result = uiu.el(value_label, 'select', { + name: filed_id, + }); + const fallback_value = typeof fallback_value_fn === 'function' ? fallback_value_fn() : 'disabled'; + const current_value = curt[filed_id] == null ? String(fallback_value) : String(curt[filed_id]); + for (const value of values) { + const attrs = { value }; + if (current_value === value) { + attrs.selected = 'selected'; + } + uiu.el(result, 'option', attrs, ci18n(`tournament:edit:option:${filed_id}:${value}`)); + } + bind_live_prop(result, filed_id, { + get_value: input_el => input_el.value, + }); + result.rule_box = box; + return result; + } + + function create_rule_limit_input(curt, parent_el, enabled_field_id, value_field_id, default_value, min_value, max_value, step_value, unit_label_key) { + const value_is_set = curt[value_field_id] != null && curt[value_field_id] !== 'none' && curt[value_field_id] !== ''; + const enabled_value = curt[enabled_field_id] != null ? !!curt[enabled_field_id] : value_is_set; + const numeric_value = value_is_set ? Number(curt[value_field_id]) : default_value; + + const box = uiu.el(parent_el, 'fieldset', 'automation_rule_box'); + const legend = uiu.el(box, 'legend'); + const enabled_input = uiu.el(legend, 'input', { + type: 'checkbox', + name: enabled_field_id, + }); + if (enabled_value) { + enabled_input.checked = true; + } + uiu.el(legend, 'span', {}, ci18n('tournament:edit:' + enabled_field_id)); + + const value_label = uiu.el(box, 'label', 'automation_rule_value'); + uiu.el(value_label, 'span', {}, ci18n('tournament:edit:' + value_field_id)); + const value_input = uiu.el(value_label, 'input', { + type: 'number', + name: value_field_id, + value: Number.isFinite(numeric_value) ? numeric_value : default_value, + min: min_value, + max: max_value, + step: step_value, + }); + if (unit_label_key) { + uiu.el(value_label, 'span', 'automation_rule_unit', ci18n(unit_label_key)); + } + + const sync_disabled = () => { + value_input.disabled = !enabled_input.checked; + }; + sync_disabled(); + enabled_input.addEventListener('change', sync_disabled); + + bind_live_prop(enabled_input, enabled_field_id); + bind_live_prop(value_input, value_field_id, { + get_value: input_el => Number(input_el.value), + }); + + return { + box, + enabled_input, + value_input, + }; + } + function create_duration_seconds_input(curt, parent_el, filed_id, min_seconds, max_seconds, default_seconds, step_seconds) { const text_input = uiu.el(parent_el, 'label'); uiu.el(text_input, 'span', {}, ci18n('tournament:edit:' + filed_id)); @@ -5725,7 +6914,14 @@ function update_officials() { const match = matches[pos]; const setup = utils.deep_copy(match.setup); setup.tournament_name = curt.name; - const s = calc.remote_state(pseudo_state, setup, match.presses); + let s = null; + try { + s = calc.remote_state(pseudo_state, setup, match.presses); + } catch (err) { + console.error(`[bts] bulk scoresheet remote_state failed for #${setup.match_num || '?'} (${match._id})`, err); + cerror.silent(`Scoresheet for #${setup.match_num || '?'} skipped: ${err.message}`); + return render_next_scoresheet(task, pos + 1, cb); + } s.ui = {}; scoresheet.load_sheet(scoresheet.sheet_name(setup), function (xml) { @@ -5866,6 +7062,7 @@ function update_officials() { update_match, update_officials, update_upcoming_match, + update_location_preparation_need_labels, update_logo, update_display, update_location, @@ -5888,6 +7085,7 @@ function update_officials() { update_edit_dependencies, update_btp_settings_ui, update_show_tabletoperators, + update_show_automation_controls, close_scoring_format_dialog_if_open, refresh_current_view, handle_view_announcement, diff --git a/static/js/toprow.js b/static/js/toprow.js index 80bb5a6..9097ed7 100644 --- a/static/js/toprow.js +++ b/static/js/toprow.js @@ -26,6 +26,7 @@ function update_container(container, elems, with_sep) { } const item_attrs = { 'class': 'toprow_menu_item' + ((item.func || item.href) ? ' vlink' : '') + (item.class ? (' ' + item.class) : ''), + 'data-label': item.label, }; if (item.href) { item_attrs.href = item.href; diff --git a/test/test_cmatch_official_select.js b/test/test_cmatch_official_select.js index 8898726..e36d91d 100644 --- a/test/test_cmatch_official_select.js +++ b/test/test_cmatch_official_select.js @@ -80,4 +80,39 @@ _describe('cmatch official select entries', () => { 'Beta (Service judge)', ]); }); + + _it('does not render assigned or preparation headings when those groups are empty', () => { + const tournament = { + umpires: [ + { _id: 'u1', name: 'Wartet U', umpire_wait: 10 }, + { _id: 'u2', name: 'Court U', umpire_on_court: 'c1' }, + ], + matches: [] + }; + + const entries = build_entries(tournament, false, true); + const labels = entries.map((entry) => entry.label); + + assert.strictEqual(labels.includes('--- Assigned to a match ---'), false); + assert.strictEqual(labels.includes('--- In preparation ---'), false); + }); + + _it('renders assigned and preparation headings only when the corresponding groups have entries', () => { + const tournament = { + umpires: [ + { _id: 'u1', name: 'Assigned U' }, + { _id: 'u2', name: 'Prep U' }, + ], + matches: [ + { setup: { state: 'ready', match_num: 7, umpire: { _id: 'u1', name: 'Assigned U' } } }, + { setup: { state: 'preparation', match_num: 8, preparation_call_timestamp: 1, umpire: { _id: 'u2', name: 'Prep U' } } }, + ] + }; + + const entries = build_entries(tournament, false, true); + const labels = entries.map((entry) => entry.label); + + assert.strictEqual(labels.includes('--- Assigned to a match ---'), true); + assert.strictEqual(labels.includes('--- In preparation ---'), true); + }); });

  • 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�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