]> git.localhorst.tv Git - alttp.git/commitdiff
group swap style settings
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 2 Jan 2026 16:47:03 +0000 (17:47 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Fri, 2 Jan 2026 16:47:03 +0000 (17:47 +0100)
18 files changed:
app/Http/Controllers/GroupAssignmentController.php [new file with mode: 0644]
app/Http/Controllers/RoundController.php
app/Http/Controllers/TournamentController.php
app/Models/GroupAssignment.php
app/Policies/GroupAssignmentPolicy.php [new file with mode: 0644]
app/Policies/RoundPolicy.php
database/migrations/2026_01_02_130509_tournament_group_swap_style.php [new file with mode: 0644]
resources/js/components/common/Icon.jsx
resources/js/components/rounds/GroupsButton.jsx [new file with mode: 0644]
resources/js/components/rounds/GroupsDialog.jsx [new file with mode: 0644]
resources/js/components/rounds/Item.jsx
resources/js/components/tournament/SettingsDialog.jsx
resources/js/helpers/permissions.js
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/js/pages/Tournament.jsx
routes/api.php
routes/channels.php

diff --git a/app/Http/Controllers/GroupAssignmentController.php b/app/Http/Controllers/GroupAssignmentController.php
new file mode 100644 (file)
index 0000000..13a0ec8
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\GroupAssignment;
+use App\Models\Protocol;
+use Illuminate\Http\Request;
+
+class GroupAssignmentController extends Controller {
+
+       public function changeAssignment(Request $request, GroupAssignment $assignment) {
+               $validatedData = $request->validate([
+                       'group' => 'string|required|in:A,B,C,D,E,F,G,H',
+               ]);
+               $this->authorize('changeAssignment', $assignment);
+
+               $assignment->group = $validatedData['group'];
+               $assignment->save();
+
+               Protocol::groupSwap(
+                       $assignment->tournament,
+                       $assignment->user,
+                       ['number' => $assignment->round_number, 'group' => $assignment->group],
+                       $request->user(),
+               );
+
+               return $assignment->toArray();
+       }
+
+}
index a33785bd256845756dd021b8455e12e92c7d92a1..288b3abdcbf389f11667d2bd3f57194bde021ae5 100644 (file)
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
 
 use App\Events\RoundAdded;
 use App\Events\RoundChanged;
+use App\Models\GroupAssignment;
 use App\Models\Protocol;
 use App\Models\Round;
 use App\Models\Tournament;
@@ -131,6 +132,28 @@ class RoundController extends Controller
                return redirect($round->seed);
        }
 
+       public function groups(Round $round) {
+               $this->authorize('seeGroups', $round);
+
+               $assignments = GroupAssignment::query()
+                       ->where('tournament_id', '=', $round->tournament_id)
+                       ->where('round_number', '=', $round->number)
+                       ->with('user')
+                       ->get()
+                       ->toArray();
+               $groups = Round::query()
+                       ->where('tournament_id', '=', $round->tournament_id)
+                       ->where('number', '=', $round->number)
+                       ->get()
+                       ->pluck('group')
+                       ->toArray();
+
+               return [
+                       'groups' => $groups,
+                       'assignments' => $assignments,
+               ];
+       }
+
        public function uploadSeed(Request $request, Round $round) {
                $this->authorize('update', $round);
 
index b3561eee8c0d24d1c9989c9e47f5d875fadaa389..3649ae87b1901bf8d4a50784ddc7eec18a3d48fa 100644 (file)
@@ -115,12 +115,16 @@ class TournamentController extends Controller
                $this->authorize('update', $tournament);
                $validatedData = $request->validate([
                        'group_size' => 'integer|nullable',
+                       'group_swap_style' => 'string|nullable|in:admin,always,finished,never',
                        'result_reveal' => 'string|nullable|in:always,finishers,never,participants',
                        'show_numbers' => 'boolean|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'];
                }
index ef384c7502c28a1579858a61d2d1cffab7b164c1..35b93e310e133c143d1dc6d13adf88755213098b 100644 (file)
@@ -2,10 +2,26 @@
 
 namespace App\Models;
 
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Database\Eloquent\BroadcastsEvents;
 use Illuminate\Database\Eloquent\Model;
 
 class GroupAssignment extends Model {
 
+       use BroadcastsEvents;
+
+       public function broadcastOn($event) {
+               $channels = [
+                       new PrivateChannel('Tournament.'.$this->tournament_id.'.'.$this->user_id),
+               ];
+               return $channels;
+       }
+
+       public function broadcastWith($event) {
+               $this->load(['user']);
+       }
+
+
        public function tournament() {
                return $this->belongsTo(Tournament::class);
        }
diff --git a/app/Policies/GroupAssignmentPolicy.php b/app/Policies/GroupAssignmentPolicy.php
new file mode 100644 (file)
index 0000000..3b2147a
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Policies;
+
+use App\Models\GroupAssignment;
+use App\Models\User;
+use Illuminate\Auth\Access\HandlesAuthorization;
+
+class GroupAssignmentPolicy {
+
+       use HandlesAuthorization;
+
+       /**
+        * Determine whether the user can change this assignment.
+        *
+        * @param  \App\Models\User  $user
+        * @param  \App\Models\GroupAssignment  $assignment
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function changeAssignment(User $user, GroupAssignment $assignment): bool {
+               return $user->isTournamentAdmin($assignment->tournament);
+       }
+
+}
index ba8c3ad42d001675bcdabe267c357348eff8e456..3afff4aebfe839389df095b63ef454eea94855d4 100644 (file)
@@ -150,6 +150,18 @@ class RoundPolicy
                return !!$user;
        }
 
+       /**
+        * Determine whether the user can see al group assignments for this round.
+        *
+        * @param  \App\Models\User  $user
+        * @param  \App\Models\Round  $round
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function seeGroups(User $user = null, Round $round): bool
+       {
+               return $user->isTournamentCrew($round->tournament);
+       }
+
        /**
         * Determine whether the user can lock this round.
         *
@@ -183,8 +195,14 @@ class RoundPolicy
         */
        public function swapGroup(User $user, Round $round)
        {
+               if ($round->locked || $round->tournament->locked) {
+                       return false;
+               }
+               if (in_array($round->tournament->group_swap_style, ['admin', 'never'])) {
+                       return false;
+               }
                $result = $user->findResult($round);
-               if (!$result || $round->locked || $round->tournament->locked) {
+               if ($round->tournament->group_swap_style == 'finished' && !$result) {
                        return false;
                }
                $remaining = $round->tournament->rounds()
diff --git a/database/migrations/2026_01_02_130509_tournament_group_swap_style.php b/database/migrations/2026_01_02_130509_tournament_group_swap_style.php
new file mode 100644 (file)
index 0000000..7949fef
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration {
+
+       /**
+        * Run the migrations.
+        */
+       public function up(): void
+       {
+               Schema::table('tournaments', function (Blueprint $table) {
+                       $table->string('group_swap_style')->default('finished');
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        */
+       public function down(): void
+       {
+               Schema::table('tournaments', function (Blueprint $table) {
+                       $table->dropColumn('group_swap_style');
+               });
+       }
+
+};
index 2aebeb59fe522dd355549f190d9354a9d613f03a..7ea4a01db735917ac7486abb38a939095621f595 100644 (file)
@@ -71,6 +71,7 @@ Icon.FINISHED = makePreset('FinishedIcon', 'square-check');
 Icon.FIRST_PLACE = makePreset('FirstPlaceIcon', 'trophy');
 Icon.FORBIDDEN = makePreset('ForbiddenIcon', 'square-xmark');
 Icon.FORFEIT = makePreset('ForfeitIcon', 'square-minus');
+Icon.GROUPS = makePreset('GroupsIcon', 'group-arrows-rotate');
 Icon.HASH = makePreset('HashIcon', 'hashtag');
 Icon.INFO = makePreset('Info', 'circle-info');
 Icon.INVERT = makePreset('InvertIcon', 'circle-half-stroke');
diff --git a/resources/js/components/rounds/GroupsButton.jsx b/resources/js/components/rounds/GroupsButton.jsx
new file mode 100644 (file)
index 0000000..03b4bd3
--- /dev/null
@@ -0,0 +1,43 @@
+import PropTypes from 'prop-types';
+import React, { useState } from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import GroupsDialog from './GroupsDialog';
+import Icon from '../common/Icon';
+
+const GroupsButton = ({
+       round,
+       tournament,
+}) => {
+       const [showDialog, setShowDialog] = useState(false);
+
+       const { t } = useTranslation();
+
+       return <>
+               <GroupsDialog
+                       onHide={() => setShowDialog(false)}
+                       round={round}
+                       show={showDialog}
+                       tournament={tournament}
+               />
+               <Button
+                       onClick={() => setShowDialog(true)}
+                       size="sm"
+                       title={t('rounds.groups')}
+                       variant="outline-secondary"
+               >
+                       <Icon.GROUPS title="" />
+               </Button>
+       </>;
+};
+
+GroupsButton.propTypes = {
+       round: PropTypes.shape({
+               locked: PropTypes.bool,
+       }),
+       tournament: PropTypes.shape({
+       }),
+};
+
+export default GroupsButton;
diff --git a/resources/js/components/rounds/GroupsDialog.jsx b/resources/js/components/rounds/GroupsDialog.jsx
new file mode 100644 (file)
index 0000000..42ca01e
--- /dev/null
@@ -0,0 +1,125 @@
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Form, Modal, Table } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+import toastr from 'toastr';
+
+import Loading from '../common/Loading';
+import UserBox from '../users/Box';
+import { mayModifyGroups } from '../../helpers/permissions';
+import { useUser } from '../../hooks/user';
+
+const GroupsDialog = ({
+       onHide,
+       round,
+       show,
+       tournament,
+}) => {
+       const [groups, setGroups] = React.useState([]);
+       const [loading, setLoading] = React.useState(false);
+
+       const { t } = useTranslation();
+       const { user } = useUser();
+
+       const mayModify = React.useMemo(() => mayModifyGroups(user, tournament, round), [round, tournament, user]);
+
+       React.useEffect(() => {
+               if (!show || !round?.id) return;
+               setLoading(true);
+               const ctrl = new AbortController();
+               axios
+                       .get(`/api/rounds/${round.id}/groups`, { signal: ctrl.signal })
+                       .then(response => {
+                               setGroups(response.data);
+                               setLoading(false);
+                       })
+                       .catch((e) => {
+                               console.error(e);
+                               setLoading(false);
+                       });
+               return () => {
+                       ctrl.abort();
+               };
+       }, [round?.id, show]);
+
+       const changeAssignment = React.useCallback(async (asgn_id, group) => {
+               try {
+                       const response = await axios.post(`/api/group-assignments/${asgn_id}/change`, { group });
+                       setGroups((oldGroups) => ({
+                               assignments: oldGroups.assignments.map((asgn) => {
+                                       if (asgn.id === asgn_id) {
+                                               return response.data;
+                                       }
+                                       return asgn;
+                               }),
+                               groups: oldGroups.groups,
+                       }));
+                       toastr.success(t('groups.changeSuccess'));
+               } catch (e) {
+                       console.error(e);
+                       toastr.error(t('groups.changeError'));
+               }
+       }, [t]);
+
+       return <Modal className="groups-dialog" onHide={onHide} show={show}>
+               <Modal.Header closeButton>
+                       <Modal.Title>
+                               {t('rounds.groups')}
+                       </Modal.Title>
+               </Modal.Header>
+               {loading ?
+                       <Loading />
+               :
+                       <Table>
+                               <thead>
+                                       <tr>
+                                               <th className="ps-3">{t('results.runner')}</th>
+                                               <th>{t('groups.group')}</th>
+                                       </tr>
+                               </thead>
+                               <tbody>
+                                       {(groups?.assignments || []).map((asgn) => (
+                                               <tr key={asgn.id}>
+                                                       <td>
+                                                               <UserBox user={asgn.user} />
+                                                       </td>
+                                                       <td>
+                                                               {mayModify ?
+                                                                       <Form.Select
+                                                                               onChange={({ target: { value } }) =>
+                                                                                       changeAssignment(asgn.id, value)}
+                                                                               value={asgn.group}
+                                                                       >
+                                                                               {groups.groups.map((group) => (
+                                                                                       <option key={group} value={group}>{group}</option>
+                                                                               ))}
+                                                                       </Form.Select>
+                                                               :
+                                                                       asgn.group
+                                                               }
+                                                       </td>
+                                               </tr>
+                                       ))}
+                               </tbody>
+                       </Table>
+               }
+               <Modal.Footer>
+                       <Button onClick={onHide} variant="secondary">
+                               {t('button.close')}
+                       </Button>
+               </Modal.Footer>
+       </Modal>;
+};
+
+GroupsDialog.propTypes = {
+       onHide: PropTypes.func,
+       round: PropTypes.shape({
+               id: PropTypes.number,
+       }),
+       show: PropTypes.bool,
+       tournament: PropTypes.shape({
+       }),
+};
+
+export default GroupsDialog;
index 5212394cce722a57f4a9e61ea8389fd300d1a3fa..b37b34b34728057c2b94b92d083378f77783a012 100644 (file)
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
 
 import DeleteButton from './DeleteButton';
 import EditButton from './EditButton';
+import GroupsButton from './GroupsButton';
 import LockButton from './LockButton';
 import SeedButton from './SeedButton';
 import SeedCode from './SeedCode';
@@ -15,6 +16,7 @@ import Table from '../results/Table';
 import {
        mayDeleteRound,
        mayEditRound,
+       maySeeGroups,
        mayReportResult,
        mayVerifyResults,
        mayViewProtocol,
@@ -109,6 +111,9 @@ const Item = ({
                                                {mayEditRound(user, tournament, round) ?
                                                        <EditButton round={round} tournament={tournament} />
                                                : null}
+                                               {maySeeGroups(user, tournament, round) ?
+                                                       <GroupsButton round={round} tournament={tournament} />
+                                               : null}
                                                {mayViewProtocol(user, tournament, round) ?
                                                        <RoundProtocol roundId={round.id} tournamentId={tournament.id} />
                                                : null}
index a24f383c5c873b54aedd1a6a70dc3491a14fee30..ec38a9ab6a1fd99b767ca69f88f944adebc6fc53 100644 (file)
@@ -112,7 +112,7 @@ const SettingsDialog = ({
                                                value={tournament.show_numbers}
                                        />
                                </div>
-                               {Tournament.hasAssignedGroups(tournament) ? (
+                               {Tournament.hasAssignedGroups(tournament) ? <>
                                        <div className="d-flex align-items-center justify-content-between mb-3">
                                                <span>{i18n.t('tournaments.groupSize')}</span>
                                                <Form.Control
@@ -129,15 +129,34 @@ const SettingsDialog = ({
                                                        value={tournament.group_size}
                                                />
                                        </div>
-                               ) : null}
+                                       <div className="d-flex align-items-center justify-content-between mb-3">
+                                               <span>{i18n.t('tournaments.groupSwapStyle')}</span>
+                                               <Form.Select
+                                                       className="w-50"
+                                                       onChange={({ target: { value } }) =>
+                                                               settings(tournament, { group_swap_style: value })}
+                                                       value={tournament.group_swap_style}
+                                               >
+                                                       {['always', 'finished', 'admin', 'never'].map((key) =>
+                                                               <option
+                                                                       key={key}
+                                                                       title={i18n.t(`tournaments.groupSwapStyleDescription.${key}`)}
+                                                                       value={key}
+                                                               >
+                                                                       {i18n.t(`tournaments.groupSwapStyles.${key}`)}
+                                                               </option>
+                                                       )}
+                                               </Form.Select>
+                                       </div>
+                               </> : null}
                                <div className="d-flex align-items-center justify-content-between mb-3">
                                        <span title={i18n.t('tournaments.resultRevealDescription')}>
                                                {i18n.t('tournaments.resultReveal')}
                                        </span>
                                        <Form.Select
+                                               className="w-50"
                                                onChange={({ target: { value } }) =>
                                                        settings(tournament, { result_reveal: value })}
-                                               style={{ width: '50%' }}
                                                value={tournament.result_reveal}
                                        >
                                                {['never', 'finishers', 'participants', 'always'].map((key) =>
@@ -194,8 +213,9 @@ SettingsDialog.propTypes = {
        tournament: PropTypes.shape({
                accept_applications: PropTypes.bool,
                discord: PropTypes.string,
-               locked: PropTypes.bool,
                group_size: PropTypes.number,
+               group_swap_style: PropTypes.string,
+               locked: PropTypes.bool,
                result_reveal: PropTypes.string,
                show_numbers: PropTypes.bool,
        }),
index 542d5a6c07ae14dc4b9488527e4949e7a1e93c71..2308b47491020d5f9341cce9887bc535bf249b5a 100644 (file)
@@ -214,7 +214,14 @@ export const maySeeResult = (user, tournament, round, result) => {
 };
 
 export const maySwapGroup = (user, tournament, round, result) => {
-       if (!user || !result || tournament?.group_size <= 1 || tournament.locked || !tournament?.rounds || !round || round.locked) {
+       if (!user || tournament?.group_size <= 1 || tournament.locked || !tournament.rounds || !round || round.locked) {
+               return false;
+       }
+       const style = tournament.group_swap_style || 'finished';
+       if (['admin', 'never'].includes(style)) {
+               return false;
+       }
+       if (style === 'finished' && !result) {
                return false;
        }
        const remaining_rounds = tournament.rounds.filter(
@@ -223,10 +230,18 @@ export const maySwapGroup = (user, tournament, round, result) => {
        return remaining_rounds.length > 0;
 };
 
-export const mayModifyResults = (user, tournament, round) => {
+export const maySeeGroups = (user, tournament, round) => {
        return isTournamentCrew(user, tournament);
 };
 
+export const mayModifyGroups = (user, tournament, round) => {
+       return tournament?.group_swap_style !== 'never' && !round?.locked && isTournamentAdmin(user, tournament);
+};
+
+export const mayModifyResults = (user, tournament, round) => {
+       return !round?.locked && isTournamentCrew(user, tournament);
+};
+
 export const mayModifyResult = (user, tournament, round, result) => {
        return mayModifyResults(user, tournament) && user && result && !result.verified_at && user.id !== result.user_id;
 };
index ae56a373ad9a3519edf358e1f6215ddcefa66310..db4c5763bf85364f945fd18321a2a08603077330 100644 (file)
@@ -384,8 +384,11 @@ export default {
                        uploading: 'Am Hochladen...',
                },
                groups: {
+                       changeError: 'Fehler beim Gruppenwechsel',
+                       changeSuccess: 'Gruppe gewechselt',
                        complete: 'Abgeschlossen',
                        empty: 'Noch keine Gruppen verfügbar',
+                       group: 'Gruppe',
                        heading: 'Gruppen',
                        loginRequired: 'Dieses Turnier nutzt Gruppenzuweisung. Bitte melde dich an, um deine Seeds zu laden.',
                        missingAssignments: 'Dieses Turnier nutzt Gruppenzuweisung. Falls du teilnehmen möchtest, hol dir bitter hier deine Zuweisungen ab.',
@@ -682,6 +685,7 @@ export default {
                        editError: 'Fehler beim Speichern',
                        editSuccess: 'Gespeichert',
                        empty: 'Noch keine Runde gestartet',
+                       groups: 'Gruppenzuweisung',
                        heading: 'Runden',
                        new: 'Neue Runde',
                        noSeed: 'Noch kein Seed',
@@ -925,6 +929,13 @@ export default {
                        discordSettingsSuccess: 'Discord Einstellungen gespeichert',
                        discordSuccess: 'Discord verknüpft',
                        groupSize: 'Seeds pro Runde',
+                       groupSwapStyle: 'Seed-Wechsel',
+                       groupSwapStyles: {
+                               admin: 'Durch Admins',
+                               always: 'Immer',
+                               finished: 'Wenn beendet',
+                               never: 'Nie',
+                       },
                        inviteBot: 'Bot einladen',
                        locked: 'Turnier sperren',
                        lockError: 'Fehler beim Sperren',
index d633cae5e99334078093445bcfb63648e209bffa..ca922e4ccc57a4575833939530b08c5db5127ee1 100644 (file)
@@ -384,8 +384,11 @@ export default {
                        uploading: 'Uploading...',
                },
                groups: {
+                       changeError: 'Error changing groups',
+                       changeSuccess: 'Group assignment changed',
                        complete: 'Complete',
                        empty: 'No groups available yet',
+                       group: 'Group',
                        heading: 'Groups',
                        loginRequired: 'This tournament uses assigned groups. Please sign in to obtain your seeds.',
                        missingAssignments: 'This tournament uses assigned groups. If you want to participate, please grab your assignments here.',
@@ -683,6 +686,7 @@ export default {
                        editError: 'Error saving round',
                        editSuccess: 'Saved successfully',
                        empty: 'No rounds yet',
+                       groups: 'Group assignments',
                        heading: 'Rounds',
                        new: 'New round',
                        noSeed: 'No seed set',
@@ -926,6 +930,13 @@ export default {
                        discordSettingsSuccess: 'Discord settings saved',
                        discordSuccess: 'Discord associated',
                        groupSize: 'Seeds per Round',
+                       groupSwapStyle: 'Seed swapping',
+                       groupSwapStyles: {
+                               admin: 'By admins',
+                               always: 'Always',
+                               finished: 'After finishing',
+                               never: 'Never',
+                       },
                        inviteBot: 'Invite bot',
                        locked: 'Lock rounds',
                        lockError: 'Error locking tournament',
index 3b65a147577929165f29194e3acba91e2a90125e..87c7a488fa09aedb931dcf6e7f32c60b8e4ce84a 100644 (file)
@@ -254,6 +254,26 @@ export const Component = () => {
                };
        }, []);
 
+       useEffect(() => {
+               if (!tournament?.id || !user?.id) return;
+               window.Echo.private(`Tournament.${tournament.id}.${user.id}`)
+                       .listen('.GroupAssignmentUpdated', ({ model }) => {
+                               console.log(model);
+                               setTournament((t) => ({
+                                       ...t,
+                                       group_assignments: t.group_assignments.map((asgn) => {
+                                               if (asgn.id === model.id) {
+                                                       return model;
+                                               }
+                                               return asgn;
+                                       }),
+                               }));
+                       });
+               return () => {
+                       window.Echo.leave(`Tournament.${tournament.id}.${user.id}`);
+               };
+       }, [tournament?.id, user?.id]);
+
        if (loading) {
                return <Loading />;
        }
index 5afc0efc98ffff629925f49a1b863e7a0a4ab262..e32b4504b5bc61fdad66645bf0c145b65f141975 100644 (file)
@@ -76,6 +76,8 @@ Route::get('events', 'App\Http\Controllers\EventController@search');
 Route::get('events/{event:name}', 'App\Http\Controllers\EventController@single');
 Route::post('events/{event}/add-episode', 'App\Http\Controllers\EpisodeController@create');
 
+Route::post('group-assignments/{assignment}/change', 'App\Http\Controllers\GroupAssignmentController@changeAssignment');
+
 Route::get('markers/{map}', 'App\Http\Controllers\TechniqueController@forMap');
 
 Route::get('pages/{type}', 'App\Http\Controllers\TechniqueController@byType');
@@ -93,6 +95,7 @@ Route::post('results/{result}/unverify', 'App\Http\Controllers\ResultController@
 Route::post('rounds', 'App\Http\Controllers\RoundController@create');
 Route::put('rounds/{round}', 'App\Http\Controllers\RoundController@update');
 Route::delete('rounds/{round}', 'App\Http\Controllers\RoundController@delete');
+Route::get('rounds/{round}/groups', 'App\Http\Controllers\RoundController@groups');
 Route::post('rounds/{round}/lock', 'App\Http\Controllers\RoundController@lock');
 Route::post('rounds/{round}/setSeed', 'App\Http\Controllers\RoundController@setSeed');
 Route::post('rounds/{round}/unlock', 'App\Http\Controllers\RoundController@unlock');
index 0aee0d295bce50074ae97375a7ff7ebc93776bcb..cea1e319db424543eece892f7ae321c7e107d5fc 100644 (file)
@@ -43,3 +43,8 @@ Broadcast::channel('Tournament.{id}', function ($user, $id) {
        $tournament = Tournament::findOrFail($id);
        return true;
 });
+
+Broadcast::channel('Tournament.{id}.{user_id}', function ($user, $id, $user_id) {
+       $tournament = Tournament::findOrFail($id);
+       return $user->id == $user_id;
+});