->toJson();
}
+ public function swapGroup(Request $request, Tournament $tournament) {
+ $this->authorize('swapGroups', $tournament);
+ $user = $request->user();
+
+ $validatedData = $request->validate([
+ 'round_number' => 'numeric|required',
+ ]);
+ $number = $validatedData['round_number'];
+
+ $existing = GroupAssignment::query()
+ ->whereBelongsTo($tournament)
+ ->whereBelongsTo($user)
+ ->where('round_number', '=', $number)
+ ->firstOrFail();
+
+ $round = $existing->getRound();
+ $this->authorize('swapGroup', $round);
+
+ $new_group = $tournament->pickGroup($number, $user, [$existing->group]);
+ $existing->group = $new_group;
+ $existing->save();
+
+ Protocol::groupSwap($tournament, $user, ['number' => $number, 'group' => $new_group], $user);
+
+ return GroupAssignment::query()
+ ->whereBelongsTo($tournament)
+ ->whereBelongsTo($user)
+ ->get()
+ ->toJson();
+ }
+
public function web(Request $request, Tournament $tournament) {
$view = view('app')
->with('title', $tournament->getTranslatedTitle())
return $this->belongsTo(User::class);
}
+ public function getRound(): Round {
+ return $this->tournament->rounds()
+ ->where('number', '=', $this->round_number)
+ ->where('group', '=', $this->group)
+ ->firstOrFail();
+ }
+
protected $casts = [
'user_id' => 'string',
];
ProtocolAdded::dispatch($protocol);
}
+ public static function groupSwap(Tournament $tournament, User $assignee, $pick, User $user) {
+ $protocol = static::create([
+ 'tournament_id' => $tournament->id,
+ 'user_id' => $user->id,
+ 'type' => 'group.swap',
+ 'details' => [
+ 'tournament' => static::tournamentMemo($tournament),
+ 'assignee' => static::userMemo($assignee),
+ 'picks' => [$pick],
+ ],
+ ]);
+ ProtocolAdded::dispatch($protocol);
+ }
+
public static function resultCommented(Tournament $tournament, Result $result, User $user) {
$protocol = static::create([
'tournament_id' => $tournament->id,
}
}
- public function pickGroup($number, User $user) {
- $available_rounds = $this->rounds()->where('number', '=', $number)->get();
- $assigned_groups = $this->group_assignments()->where('round_number', '=', $number)->get();
+ public function pickGroup($number, User $user, $exclude = []) {
+ $available_rounds = $this->rounds()
+ ->where('number', '=', $number)
+ ->whereNotIn('group', $exclude)
+ ->get();
+ $assigned_groups = $this->group_assignments()
+ ->where('round_number', '=', $number)
+ ->whereNotIn('group', $exclude)
+ ->get();
$weights = array();
foreach ($available_rounds as $round) {
$weights[$round->group] = $assigned_groups->count() + 1;
return $this->lock($user, $round);
}
+ /**
+ * Determine whether the user can swap groups within this round.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Models\Round $round
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function swapGroup(User $user, Round $round)
+ {
+ $result = $user->findResult($round);
+ return $result && !$round->locked;
+ }
+
}
}
/**
- * Determine whether the user self assign groups within the tournament.
+ * Determine whether the user can self assign groups within the tournament.
*
* @param \App\Models\User $user
* @param \App\Models\Tournament $tournament
return !$tournament->locked && !!$user;
}
+ /**
+ * Determine whether the user can swap groups within the tournament.
+ *
+ * @param \App\Models\User $user
+ * @param \App\Models\Tournament $tournament
+ * @return \Illuminate\Auth\Access\Response|bool
+ */
+ public function swapGroups(User $user, Tournament $tournament)
+ {
+ return !$tournament->locked && !!$user;
+ }
+
}
Icon.STEP_FORWARD = makePreset('StepForwardIcon', 'forward-step');
Icon.STOP = makePreset('StopIcon', 'stop');
Icon.STREAM = makePreset('StreamIcon', ['fab', 'twitch']);
+Icon.SWAP = makePreset('SwapIcon', 'rotate');
Icon.THIRD_PLACE = makePreset('ThirdPlaceIcon', 'award');
Icon.TIME_REVERSE = makePreset('TimeReverseIcon', 'clock-rotate-left');
Icon.TWITCH = makePreset('TwitchIcon', ['fab', 'twitch']);
import { Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
+import SwapButton from './SwapButton';
import Icon from '../common/Icon';
import Badge from '../results/Badge';
import ReportButton from '../results/ReportButton';
import SeedButton from '../rounds/SeedButton';
import SeedCode from '../rounds/SeedCode';
-import { mayReportResult } from '../../helpers/permissions';
+import { mayReportResult, maySwapGroup } from '../../helpers/permissions';
import { formatNumberAlways } from '../../helpers/Round';
import { findResult } from '../../helpers/User';
import { useUser } from '../../hooks/user';
}
const Item = ({
+ actions,
round,
tournament,
}) => {
</h3>
<div className="group-details">
{getStatusIcon(round, result, t)}
- <div className="group-seed">
+ <div className="button-bar group-seed">
{round.code && round.code.length ?
<SeedCode code={round.code} game={round.game || 'alttpr'} />
: null}
<SeedButton round={round} tournament={tournament} />
+ {actions.swapGroup && maySwapGroup(user, tournament, round, result) ?
+ <SwapButton onClick={() => actions.swapGroup(round)} />
+ : null}
</div>
- <div className="group-result">
+ <div className="button-bar group-result">
{mayReportResult(user, tournament) ?
<ReportButton round={round} tournament={tournament} user={user} />
: null}
Item.propTypes = {
actions: PropTypes.shape({
+ swapGroup: PropTypes.func,
}),
round: PropTypes.shape({
code: PropTypes.arrayOf(PropTypes.string),
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from 'react-bootstrap';
+
+import Icon from '../common/Icon';
+
+const SwapButton = ({ onClick }) => {
+ return <Button onClick={onClick} variant="outline-secondary">
+ <Icon.SWAP />
+ </Button>;
+};
+
+SwapButton.propTypes = {
+ onClick: PropTypes.func,
+};
+
+export default SwapButton;
},
);
case 'group.assign':
+ case 'group.swap':
return t(
`protocol.description.${entry.type}`,
{
...entry,
assignee: getEntryDetailsAssignee(entry),
+ count: (entry?.details?.picks?.length) || 0,
picks: getEntryDetailsPicks(entry),
},
);
import * as Episode from './Episode';
import Round from './Round';
+import User from './User';
export const hasGlobalRole = (user, role) =>
user && role && user.global_roles && user.global_roles.includes(role);
return maySeeResults(user, tournament, round);
};
+export const maySwapGroup = (user, tournament, round, result) => {
+ return user && result && tournament?.group_size > 1 && !tournament.locked && round && !round.locked;
+};
+
export const mayModifyResults = (user, tournament, round) => {
return isTournamentCrew(user, tournament);
};
selfAssignButton: 'Gruppen zuweisen',
selfAssignError: 'Fehler beim Zuweisen',
selfAssignSuccess: 'Gruppen zugewiesen',
+ swapGroup: 'Gruppe wechseln',
+ swapGroupError: 'Fehler beim Wechsel',
+ swapGroupSuccess: 'Gruppe gewechselt',
tournamentClosed: 'Dieses Turnier ist geschlossen.',
verified: 'Bestätigt',
},
},
group: {
assign: 'Gruppen {{picks}} für {{assignee}} zugewiesen',
+ assign_one: 'Gruppe {{picks}} für {{assignee}} zugewiesen',
+ swap: 'Gruppen {{picks}} für {{assignee}} gewechselt',
+ swap_one: 'Gruppe {{picks}} für {{assignee}} gewechselt',
},
result: {
comment: 'Ergebnis von Runde {{number}} kommentiert: <1>{{comment}}</1>',
selfAssignButton: 'Assign groups',
selfAssignError: 'Error assigning groups',
selfAssignSuccess: 'Groups assigned',
+ swapGroup: 'Swap group',
+ swapGroupError: 'Error swapping group',
+ swapGroupSuccess: 'Swapped',
tournamentClosed: 'This tournament has closed.',
verified: 'Verified',
},
},
group: {
assign: 'Assigned groups {{picks}} for {{assignee}}',
+ assign_one: 'Assigned group {{picks}} for {{assignee}}',
+ swap: 'Swapped groups {{picks}} for {{assignee}}',
+ swap_one: 'Swapped group {{picks}} for {{assignee}}',
},
result: {
comment: 'Result of round {{number}} commented: <1>{{comment}}</1>',
}
}, [id, t]);
+ const swapGroup = React.useCallback(async (round) => {
+ try {
+ const response = await axios.post(`/api/tournaments/${id}/swap-group`, { round_number: round.number });
+ toastr.success(t('groups.swapGroupSuccess'));
+ setTournament(tournament => ({
+ ...tournament,
+ group_assignments: response.data,
+ }));
+ } catch (e) {
+ toastr.error(t('groups.swapGroupError', e));
+ }
+ }, [id, t]);
+
const modifyResult = React.useCallback(async (result, values) => {
try {
const response = await axios.post(`/api/results/${result.id}/modify`, values);
modifyResult: mayModifyResults(user, tournament) ? modifyResult : null,
moreRounds: canLoadMoreRounds(tournament) ? moreRounds : null,
selfAssignGroups,
+ swapGroup,
unverifyResult: mayUnverifyResults(user, tournament) ? unverifyResult : null,
verifyResult: mayVerifyResults(user, tournament) ? verifyResult : null,
- }), [addRound, modifyResult, moreRounds, selfAssignGroups, tournament, unverifyResult, user, verifyResult]);
+ }), [addRound, modifyResult, moreRounds, selfAssignGroups, swapGroup, tournament, unverifyResult, user, verifyResult]);
useEffect(() => {
const cb = (e) => {
margin-left: auto;
text-align: right;
}
- .seed-code {
- margin-right: 1ex;
- }
- .result-badge {
- margin-left: 1ex;
- }
}
Route::post('tournaments/{tournament}/open', 'App\Http\Controllers\TournamentController@open');
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');
Route::post('tournaments/{tournament}/unlock', 'App\Http\Controllers\TournamentController@unlock');
Route::get('users', 'App\Http\Controllers\UserController@search');