From f03c79e3b3383c827a8efb1b93bc8af9e9e45a4a Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Sun, 1 Feb 2026 21:59:22 +0100 Subject: [PATCH] tournament scoring settings --- app/Events/ResultChanged.php | 1 + app/Http/Controllers/TournamentController.php | 34 +++-- app/Models/Protocol.php | 3 +- app/Models/Round.php | 55 ++------ app/Models/Tournament.php | 66 +++++++--- config/database.php | 3 - ..._01_140215_tournament_scoring_settings.php | 38 ++++++ resources/js/components/rounds/Item.jsx | 4 +- .../js/components/tournament/ScoreChart.jsx | 44 ++++--- .../js/components/tournament/Scoreboard.jsx | 15 ++- .../components/tournament/SettingsDialog.jsx | 118 ++++++++++++++++++ resources/js/helpers/Participant.js | 1 + resources/js/helpers/Tournament.js | 42 +++++-- resources/js/i18n/de.js | 18 +++ resources/js/i18n/en.js | 18 +++ routes/api.php | 1 + 16 files changed, 346 insertions(+), 115 deletions(-) create mode 100644 database/migrations/2026_02_01_140215_tournament_scoring_settings.php diff --git a/app/Events/ResultChanged.php b/app/Events/ResultChanged.php index 61207a0..92be10d 100644 --- a/app/Events/ResultChanged.php +++ b/app/Events/ResultChanged.php @@ -23,6 +23,7 @@ class ResultChanged implements ShouldBroadcast public function __construct(Result $result) { $this->result = $result; + $result->setRelations([]); $result->load(['user', 'verified_by']); } diff --git a/app/Http/Controllers/TournamentController.php b/app/Http/Controllers/TournamentController.php index 3649ae8..e7fd6b5 100644 --- a/app/Http/Controllers/TournamentController.php +++ b/app/Http/Controllers/TournamentController.php @@ -111,30 +111,40 @@ class TournamentController extends Controller return $rounds->toArray(); } + public function recalc(Request $request, Tournament $tournament) { + $this->authorize('update', $tournament); + + foreach ($tournament->rounds as $round) { + $round->updatePlacement(); + } + $tournament->updatePlacement(); + + return $tournament->toArray(); + } + public function settings(Request $request, Tournament $tournament) { $this->authorize('update', $tournament); $validatedData = $request->validate([ 'group_size' => 'integer|nullable', 'group_swap_style' => 'string|nullable|in:admin,always,finished,never', + 'limit_scoreboard' => 'integer|nullable', 'result_reveal' => 'string|nullable|in:always,finishers,never,participants', + 'round_scoring' => 'string|nullable|in:placement,relative,time', + 'round_scoring_limit' => 'integer|nullable', 'show_numbers' => 'boolean|nullable', + 'show_scoreboard' => 'boolean|nullable', + 'total_scoring' => 'string|nullable|in:avg,sum', + 'total_scoring_limit' => 'integer|nullable', ]); - if (array_key_exists('group_size', $validatedData)) { - $tournament->group_size = $validatedData['group_size']; - } - if (array_key_exists('group_swap_style', $validatedData)) { - $tournament->group_swap_style = $validatedData['group_swap_style']; - } - if (isset($validatedData['result_reveal'])) { - $tournament->result_reveal = $validatedData['result_reveal']; - } - if (array_key_exists('show_numbers', $validatedData)) { - $tournament->show_numbers = $validatedData['show_numbers']; + foreach ($validatedData as $name => $value) { + if (!is_null($value)) { + $tournament->{$name} = $value; + } } $tournament->save(); if ($tournament->wasChanged()) { TournamentChanged::dispatch($tournament); - Protocol::tournamentSettings($tournament, $request->user()); + Protocol::tournamentSettings($tournament, $request->user(), $validatedData); } return $tournament->toArray(); } diff --git a/app/Models/Protocol.php b/app/Models/Protocol.php index 76b096f..3ab422f 100644 --- a/app/Models/Protocol.php +++ b/app/Models/Protocol.php @@ -316,13 +316,14 @@ class Protocol extends Model ProtocolAdded::dispatch($protocol); } - public static function tournamentSettings(Tournament $tournament, User $user = null) { + public static function tournamentSettings(Tournament $tournament, User $user = null, $changeset = null) { $protocol = static::create([ 'tournament_id' => $tournament->id, 'user_id' => $user ? $user->id : null, 'type' => 'tournament.settings', 'details' => [ 'tournament' => static::tournamentMemo($tournament), + 'changeset' => $changeset, ], ]); ProtocolAdded::dispatch($protocol); diff --git a/app/Models/Round.php b/app/Models/Round.php index 80e3bee..2088655 100644 --- a/app/Models/Round.php +++ b/app/Models/Round.php @@ -23,6 +23,10 @@ class Round extends Model ]; } + public function broadcastWith($event) { + $this->setRelations([]); + } + public function isComplete() { if (count($this->tournament->participants) == 0) return false; @@ -46,7 +50,7 @@ class Round extends Model if ($time > 0) { $reference_times[] = $time; } - if (count($reference_times) >= 5) { + if (count($reference_times) >= $this->tournament->round_scoring_limit) { break; } } @@ -73,7 +77,7 @@ class Round extends Model $result->updatePlacement($score, count($results) - $running + 1); } } - } elseif (!$this->tournament->hasFixedRunners()) { + } else { $results = $this->results->sort([Result::class, 'compareResult']); $reversed = $results->reverse(); @@ -94,49 +98,10 @@ class Round extends Model } elseif ($result->forfeit) { $result->updatePlacement(0, count($results)); } else { - $result->updatePlacement($running, count($results) - $running + 1); - } - } - } else { - $runners = []; - foreach ($this->tournament->participants as $p) { - if ($p->isRunner()) { - $runners[] = $p; - } else { - $result = $p->findResult($this); - if ($result) { - $result->updatePlacement(null, null); - } - } - } - - usort($runners, Participant::compareResult($this)); - $mapped = array_map(function ($p) { - return ['participant' => $p, 'result' => $p->findResult($this)]; - }, $runners); - $filtered = array_filter($mapped, function ($r) { - return $r['result'] && ($r['result']->time || $r['result']->forfeit || $r['result']->disqualified); - }); - $reversed = array_reverse($filtered); - - $running = 0; - $bonus = 1; - $lastResult = null; - foreach ($reversed as $r) { - $betterThanLast = is_null($lastResult) || $r['result']->getEffectiveTime() < $lastResult; - if (!$r['result']->disqualified && !$r['result']->forfeit && $betterThanLast) { - $running += $bonus; - $lastResult = $r['result']->getEffectiveTime(); - $bonus = 1; - } else { - ++$bonus; - } - if ($r['result']->disqualified) { - $r['result']->updatePlacement(0, count($filtered) + 1); - } elseif ($r['result']->forfeit) { - $r['result']->updatePlacement(0, count($filtered)); - } else { - $r['result']->updatePlacement($running, count($filtered) - $running + 1); + $score = $this->tournament->round_scoring === 'relative' + ? $running / count($results) * 100 + : $running; + $result->updatePlacement($score, count($results) - $running + 1); } } } diff --git a/app/Models/Tournament.php b/app/Models/Tournament.php index ce12ace..dad2522 100644 --- a/app/Models/Tournament.php +++ b/app/Models/Tournament.php @@ -4,6 +4,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; class Tournament extends Model { @@ -26,7 +27,7 @@ class Tournament extends Model { } public function hasTimeBasedScoring(): bool { - return in_array($this->type, ['open-grouped-async']); + return $this->round_scoring == 'time'; } public function getRunners() { @@ -39,37 +40,52 @@ class Tournament extends Model { return $runners; } + public function getScorableRounds(): Collection { + return $this->rounds()->limit($this->total_scoring_limit)->get(); + } + public function hasScoreboard(): bool { - return $this->type == 'signup-async'; + return $this->show_scoreboard; } public function updatePlacement() { $runners = []; + foreach ($this->getScorableRounds() as $round) { + foreach ($round->results as $result) { + if (!isset($runners[$result->user_id])) { + $runners[$result->user_id] = [ + 'runner' => $this->findOrCreateRunner($result->user_id), + 'rounds' => 0, + 'total' => 0, + ]; + } + ++$runners[$result->user_id]['rounds']; + $runners[$result->user_id]['total'] += $result->score; + } + } + foreach ($this->participants as $p) { - if ($p->isRunner()) { - $p->score = 0; - $runners[] = $p; - } else { + if (!array_key_exists($p->user_id, $runners)) { $p->updatePlacement(null, null); } } - if (empty($runners)) { - return; - } - foreach ($this->rounds as $round) { - foreach ($runners as $p) { - $result = $p->findResult($round); - if ($result) { - $p->score += $result->score; - } + + $results = []; + foreach ($runners as $entry) { + $result = $entry['runner']; + if ($this->total_scoring == 'avg') { + $result->score = $entry['total'] / $entry['rounds']; + } else { + $result ->score = $entry['total']; } + $results[] = $result; } - usort($runners, [Participant::class, 'compareScore']); - $placement = count($runners); + usort($results, [Participant::class, 'compareScore']); + $placement = count($results); $skipped = 0; - $lastScore = $runners[0]->score; - foreach ($runners as $p) { + $lastScore = $results[0]->score; + foreach ($results as $p) { if ($p->score > $lastScore) { $placement -= $skipped; $skipped = 1; @@ -81,6 +97,18 @@ class Tournament extends Model { } } + public function findOrCreateRunner($user_id) { + $runner = $this->participants()->firstOrNew(['user_id' => $user_id]); + if (is_null($runner->roles)) { + $runner->roles = ['runner']; + } elseif (!in_array('runner', $runner->roles)) { + $roles = $runner->roles; + $roles[] = 'runner'; + $runner->roles = $roles; + } + return $runner; + } + public function pickGroup($number, User $user, $exclude = []) { $available_rounds = $this->rounds() ->where('number', '=', $number) diff --git a/config/database.php b/config/database.php index a61db88..4dc7901 100644 --- a/config/database.php +++ b/config/database.php @@ -59,9 +59,6 @@ return [ 'strict' => true, 'timezone' => '+00:00', 'engine' => null, - 'options' => extension_loaded('pdo_mysql') ? array_filter([ - PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), - ]) : [], ], 'pgsql' => [ diff --git a/database/migrations/2026_02_01_140215_tournament_scoring_settings.php b/database/migrations/2026_02_01_140215_tournament_scoring_settings.php new file mode 100644 index 0000000..6828a70 --- /dev/null +++ b/database/migrations/2026_02_01_140215_tournament_scoring_settings.php @@ -0,0 +1,38 @@ +string('round_scoring')->default('placement'); + $table->integer('round_scoring_limit')->default(5); + $table->string('total_scoring')->default('sum'); + $table->integer('total_scoring_limit')->default(20); + $table->boolean('show_scoreboard')->default(false); + $table->integer('limit_scoreboard')->default(10); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('tournaments', function (Blueprint $table) { + $table->dropColumn('round_scoring'); + $table->dropColumn('round_scoring_limit'); + $table->dropColumn('total_scoring'); + $table->dropColumn('total_scoring_limit'); + $table->dropColumn('show_scoreboard'); + $table->dropColumn('limit_scoreboard'); + }); + } +}; diff --git a/resources/js/components/rounds/Item.jsx b/resources/js/components/rounds/Item.jsx index b2e26ce..70a66ba 100644 --- a/resources/js/components/rounds/Item.jsx +++ b/resources/js/components/rounds/Item.jsx @@ -41,7 +41,7 @@ const getClassName = (round, tournament, user) => { } if (hasFinishedRound(user, round)) { classNames.push('has-finished'); - } else if (isRunner(user, tournament)) { + } else if (hasFixedRunners(tournament) && isRunner(user, tournament)) { classNames.push('has-not-finished'); } return classNames.join(' '); @@ -111,7 +111,7 @@ const Item = ({ {mayEditRound(user, tournament, round) ? : null} - {maySeeGroups(user, tournament, round) ? + {hasAssignedGroups(tournament) && maySeeGroups(user, tournament, round) ? : null} {mayViewProtocol(user, tournament, round) ? diff --git a/resources/js/components/tournament/ScoreChart.jsx b/resources/js/components/tournament/ScoreChart.jsx index b63c81e..afd6861 100644 --- a/resources/js/components/tournament/ScoreChart.jsx +++ b/resources/js/components/tournament/ScoreChart.jsx @@ -3,11 +3,12 @@ import React from 'react'; import { Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; import { getUserName } from '../../helpers/Participant'; -import { getRunners, getScoreTable } from '../../helpers/Tournament'; +import { getScoreboardRunners, getScoreTable } from '../../helpers/Tournament'; +import { useUser } from '../../hooks/user'; const COLORS = [ '#7cb5ec', - '#434348', + '#d3d3d8', '#90ed7d', '#f7a35c', '#8085e9', @@ -20,23 +21,28 @@ const COLORS = [ const ScoreChart = ({ tournament, -}) => - - - - - - - {getRunners(tournament).map((runner, index) => - - )} - -; +}) => { + const { user } = useUser(); + + return ( + + + + + Math.round(value * 100) / 100} /> + + {getScoreboardRunners(tournament, user).map((runner, index) => + + )} + + + ); +}; ScoreChart.propTypes = { tournament: PropTypes.shape({ diff --git a/resources/js/components/tournament/Scoreboard.jsx b/resources/js/components/tournament/Scoreboard.jsx index 27bb087..5abbaa2 100644 --- a/resources/js/components/tournament/Scoreboard.jsx +++ b/resources/js/components/tournament/Scoreboard.jsx @@ -5,9 +5,9 @@ import { useTranslation } from 'react-i18next'; import Icon from '../common/Icon'; import Box from '../users/Box'; -import { comparePlacement } from '../../helpers/Participant'; -import { getRunners } from '../../helpers/Tournament'; +import { getScoreboardFormatOptions, getScoreboardRunners } from '../../helpers/Tournament'; import { useUser } from '../../hooks/user'; +import i18n from '../../i18n'; const getRowClassName = (tournament, participant, user) => { const classNames = ['score']; @@ -61,6 +61,8 @@ const Scoreboard = ({ tournament }) => { const { t } = useTranslation(); const { user } = useUser(); + const entries = React.useMemo(() => getScoreboardRunners(tournament, user), [tournament, user]); + return @@ -70,7 +72,7 @@ const Scoreboard = ({ tournament }) => { - {getRunners(tournament).sort(comparePlacement).map(participant => + {entries.map(participant => - + )} @@ -100,6 +106,7 @@ const Scoreboard = ({ tournament }) => { Scoreboard.propTypes = { tournament: PropTypes.shape({ + limit_scoreboard: PropTypes.number, }), }; diff --git a/resources/js/components/tournament/SettingsDialog.jsx b/resources/js/components/tournament/SettingsDialog.jsx index ec38a9a..092102e 100644 --- a/resources/js/components/tournament/SettingsDialog.jsx +++ b/resources/js/components/tournament/SettingsDialog.jsx @@ -49,6 +49,15 @@ const unlock = async tournament => { } }; +const recalc = async tournament => { + try { + await axios.post(`/api/tournaments/${tournament.id}/recalc`); + toastr.success(i18n.t('tournaments.recalcSuccess')); + } catch (e) { + toastr.error(i18n.t('tournaments.recalcError', { error: e.message })); + } +}; + const setDiscord = async (tournament, guild_id) => { try { await axios.post(`/api/tournaments/${tournament.id}/discord`, { guild_id }); @@ -170,6 +179,109 @@ const SettingsDialog = ({ )} +
+ + {i18n.t('tournaments.roundScoring')} + + + settings(tournament, { round_scoring: value })} + value={tournament.round_scoring} + > + {['placement', 'relative', 'time'].map((key) => + + )} + +
+ {tournament.round_scoring === 'time' ? +
+ {i18n.t('tournaments.roundScoringLimit')} + { + const num = parseInt(value, 10); + if (value >= 0 && value <= 10) { + settings(tournament, { round_scoring_limit: num }); + } + }} + type="number" + value={tournament.round_scoring_limit} + /> +
+ : null} +
+ + {i18n.t('tournaments.totalScoring')} + + + settings(tournament, { total_scoring: value })} + value={tournament.total_scoring} + > + {['sum', 'avg'].map((key) => + + )} + +
+
+ {i18n.t('tournaments.totalScoringLimit')} + { + const num = parseInt(value, 10); + if (value >= 0 && value <= 50) { + settings(tournament, { total_scoring_limit: num }); + } + }} + type="number" + value={tournament.total_scoring_limit} + /> +
+
+ +
+
+ {i18n.t('tournaments.showScoreboard')} + + settings(tournament, { show_scoreboard: value })} + value={tournament.show_scoreboard} + /> +
+
+ {i18n.t('tournaments.limitScoreboard')} + { + const num = parseInt(value, 10); + if (value >= 3 && value <= 25) { + settings(tournament, { limit_scoreboard: num }); + } + }} + type="number" + value={tournament.limit_scoreboard} + /> +

{i18n.t('tournaments.discord')}

@@ -215,9 +327,15 @@ SettingsDialog.propTypes = { discord: PropTypes.string, group_size: PropTypes.number, group_swap_style: PropTypes.string, + limit_scoreboard: PropTypes.number, locked: PropTypes.bool, result_reveal: PropTypes.string, + round_scoring: PropTypes.string, + round_scoring_limit: PropTypes.number, show_numbers: PropTypes.bool, + show_scoreboard: PropTypes.bool, + total_scoring: PropTypes.string, + total_scoring_limit: PropTypes.number, }), }; diff --git a/resources/js/helpers/Participant.js b/resources/js/helpers/Participant.js index 1a24f8e..fcfa31a 100644 --- a/resources/js/helpers/Participant.js +++ b/resources/js/helpers/Participant.js @@ -101,6 +101,7 @@ export const sortByUsername = (participants, round) => { export default { compareFinished, + comparePlacement, compareResult, compareUsername, findResult, diff --git a/resources/js/helpers/Tournament.js b/resources/js/helpers/Tournament.js index 68ca767..44207a2 100644 --- a/resources/js/helpers/Tournament.js +++ b/resources/js/helpers/Tournament.js @@ -197,37 +197,59 @@ export const getRunners = tournament => { .sort(Participant.compareUsername); }; +export const getScoreboardRunners = (tournament, user) => { + const runners = getRunners(tournament).filter( + (r) => r.placement != null && (r.placement <= tournament.limit_scoreboard || r.user_id === user?.id), + ); + runners.sort(Participant.comparePlacement); + return runners; +} + export const hasAssignedGroups = tournament => (tournament?.type === 'open-grouped-async'); export const hasFixedRunners = tournament => !['open-async', 'open-grouped-async'].includes(tournament?.type); -export const hasScoreboard = tournament => !!(tournament && tournament.type === 'signup-async'); +export const hasFractionalScoring = tournament => tournament?.round_scoring !== 'placement'; + +export const hasScoreboard = tournament => !!(tournament && tournament.show_scoreboard); export const hasSignup = tournament => !!(tournament && tournament.type === 'signup-async'); -export const hasTimeBasedScoring = tournament => (tournament?.type === 'open-grouped-async'); +export const hasTimeBasedScoring = tournament => (tournament?.round_scoring === 'time'); + +export const getScoreFormatOptions = tournament => (hasFractionalScoring(tournament) + ? { decimals: 2 } + : { decimals: 0 } +); -export const getScoreFormatOptions = tournament => (hasTimeBasedScoring(tournament) +export const getScoreboardFormatOptions = tournament => (hasFractionalScoring(tournament) || tournament?.total_scoring === 'avg' ? { decimals: 2 } : { decimals: 0 } ); export const getScoreTable = tournament => { if (!tournament || !tournament.rounds || !tournament.rounds.length) return []; - const runners = getRunners(tournament); + const runners = getScoreboardRunners(tournament); if (!runners.length) return []; - const running = {}; + const count = {}; + const total = {}; runners.forEach(participant => { - running[participant.id] = 0; + count[participant.id] = 0; + total[participant.id] = 0; }); - const data = [...tournament.rounds, {}].reverse().map(round => { + const data = [...tournament.rounds, {}].slice(0, tournament.total_scoring_limit).reverse().map(round => { const entry = { number: round.number ? `#${round.number}` : '' }; runners.forEach(participant => { const result = Participant.findResult(participant, round); - if (result && result.score) { - running[participant.id] += result.score; + if (result) { + ++count[participant.id]; + if (result.score) { + total[participant.id] += result.score; + } } - entry[Participant.getUserName(participant)] = running[participant.id]; + entry[Participant.getUserName(participant)] = tournament.total_scoring === 'avg' + ? total[participant.id] / (count[participant.id] || 1) + : total[participant.id]; }); return entry; }); diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 4a6c24e..34f18c3 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -953,6 +953,7 @@ export default { never: 'Nie', }, inviteBot: 'Bot einladen', + limitScoreboard: 'Scoreboard Größe', locked: 'Turnier sperren', lockError: 'Fehler beim Sperren', lockSuccess: 'Turnier gesperrt', @@ -962,6 +963,9 @@ export default { open: 'Anmeldung geöffnet', openError: 'Fehler beim Öffnen der Anmledung', openSuccess: 'Anmeldung geöffnet', + recalcButton: 'Punkte neu berechnen', + recalcError: 'Fehler bei der Berechnung', + recalcSuccess: 'Punkte neu berechnet', resultReveal: 'Frühzeitige Ergebnisse', resultRevealDescription: 'Ob und wann Ergebnisse von offenen Runden angezeigt werden sollen.', resultRevealOption: { @@ -976,12 +980,26 @@ export default { never: 'Ergebnis wird erst mit Abschluss der Runde veröffentlicht.', participants: 'Ergebnis wird allen registrierten Teilnehmern des Turniers angezeigt.', }, + roundScoring: 'Punkte pro Runde', + roundScoringLimit: 'über N Ergebnisse', + roundScoringOption: { + placement: 'Platzierung', + relative: 'Relative Platzierung', + time: 'Zeit', + }, scoreboard: 'Scoreboard', scoreChart: 'Turnierverlauf', settings: 'Einstellungen', settingsError: 'Fehler beim Speichern', settingsSuccess: 'Einstellungen gespeichert', showNumbers: 'Nummern einblenden', + showScoreboard: 'Scoreboard einblenden', + totalScoring: 'Gesamtwertung', + totalScoringLimit: 'über N Runden', + totalScoringOption: { + avg: 'Durchschnitt', + sum: 'Summe', + }, unlockError: 'Fehler beim Entsperren', unlockSuccess: 'Turnier entsperrt', }, diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index 5f00ae0..a4b681c 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -954,6 +954,7 @@ export default { never: 'Never', }, inviteBot: 'Invite bot', + limitScoreboard: 'Scoreboard size', locked: 'Lock rounds', lockError: 'Error locking tournament', lockSuccess: 'Tournament locked', @@ -963,6 +964,9 @@ export default { open: 'Open registration', openError: 'Error opening registration', openSuccess: 'Registration opened', + recalcButton: 'Recalculate scores', + recalcError: 'Error during calculation', + recalcSuccess: 'Scores updated', resultReveal: 'Early results', resultRevealDescription: 'When to show results for open rounds.', resultRevealOption: { @@ -977,12 +981,26 @@ export default { never: 'Only reveal results once the round has closed down.', participants: 'Show results to only registered tournament members.', }, + roundScoring: 'Points per round', + roundScoringLimit: 'across N results', + roundScoringOption: { + placement: 'Placement', + relative: 'Relative placement', + time: 'Time', + }, scoreboard: 'Scoreboard', scoreChart: 'Score chart', settings: 'Settings', settingsError: 'Error saving settings', settingsSuccess: 'Settings saved successfully', showNumbers: 'Show numbers', + showScoreboard: 'Show scoreboard', + totalScoring: 'Total score', + totalScoringLimit: 'across N rounds', + totalScoringOption: { + avg: 'Average', + sum: 'Sum', + }, unlockError: 'Error unlocking tournaments', unlockSuccess: 'Tournament unlocked', }, diff --git a/routes/api.php b/routes/api.php index abe4e6d..1cdb80b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -116,6 +116,7 @@ Route::post('tournaments/{tournament}/discord', 'App\Http\Controllers\Tournament Route::post('tournaments/{tournament}/discord-settings', 'App\Http\Controllers\TournamentController@discordSettings'); Route::post('tournaments/{tournament}/lock', 'App\Http\Controllers\TournamentController@lock'); Route::post('tournaments/{tournament}/open', 'App\Http\Controllers\TournamentController@open'); +Route::post('tournaments/{tournament}/recalc', 'App\Http\Controllers\TournamentController@recalc'); Route::post('tournaments/{tournament}/settings', 'App\Http\Controllers\TournamentController@settings'); Route::post('tournaments/{tournament}/self-assign-groups', 'App\Http\Controllers\TournamentController@selfAssignGroups'); Route::post('tournaments/{tournament}/swap-group', 'App\Http\Controllers\TournamentController@swapGroup'); -- 2.47.3
{getPlacementDisplay(participant)} @@ -91,7 +93,11 @@ const Scoreboard = ({ tournament }) => { : null} {participant.score} + {participant.score !== null ? + i18n.number(participant.score, getScoreboardFormatOptions(tournament)) + : null} +