From: Daniel Karbach Date: Tue, 3 Feb 2026 17:49:55 +0000 (+0100) Subject: tournament crew management X-Git-Url: https://git.localhorst.tv/?a=commitdiff_plain;h=11789c3b7d32c60240e18f46874bf26ea2ec509a;p=alttp.git tournament crew management --- diff --git a/app/Http/Controllers/TournamentController.php b/app/Http/Controllers/TournamentController.php index f4cacdd..2274045 100644 --- a/app/Http/Controllers/TournamentController.php +++ b/app/Http/Controllers/TournamentController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Events\ApplicationAdded; +use App\Events\ParticipantChanged; use App\Events\TournamentChanged; use App\Models\Application; use App\Models\GroupAssignment; @@ -33,17 +34,7 @@ class TournamentController extends Controller 'applications.user', 'description', ]); - $participants = $tournament->participants() - ->where(function ($query) use ($request, $tournament) { - $query->where('placement', '<=', $tournament->limit_scoreboard); - $query->orWhereJsonContains('roles', 'admin'); - $query->orWhereJsonContains('roles', 'monitor'); - if ($request->user()) { - $query->orWhere('user_id', '=', $request->user()->id); - } - }) - ->with(['user']) - ->get(); + $participants = $this->getParticipantsForSingle($request, $tournament); $rounds = $tournament->rounds() ->with(['results', 'results.user', 'results.verified_by']) ->limit($tournament->ceilRoundLimit(25)) @@ -102,6 +93,38 @@ class TournamentController extends Controller return $tournament->toArray(); } + public function manageCrew(Request $request, Tournament $tournament) { + $this->authorize('manageCrew', $tournament); + + $validatedData = $request->validate([ + 'toggle_role' => 'string|in:admin,monitor,member', + 'user_id' => 'numeric|exists:App\Models\User,id', + ]); + + $participant = $tournament->participants()->where('user_id', '=', $validatedData['user_id'])->firstOrNew(); + $roles = $participant->roles; + $key = array_search($validatedData['toggle_role'], $participant->roles); + if ($key === false) { + $roles[] = $validatedData['toggle_role']; + } else { + unset($roles[$key]); + } + $participant->roles = array_values($roles); + $participant->save(); + + if ($key === false) { + Protocol::crewRoleAdded($tournament, $participant->user, $validatedData['toggle_role'], $request->user()); + } else { + Protocol::crewRoleRemoved($tournament, $participant->user, $validatedData['toggle_role'], $request->user()); + } + ParticipantChanged::dispatch($participant); + + $participants = $this->getParticipantsForSingle($request, $tournament); + $json = $tournament->toArray(); + $json['participants'] = $participants->toArray(); + return $json; + } + public function moreRounds(Request $request, Tournament $tournament) { $this->authorize('view', $tournament); @@ -282,4 +305,18 @@ class TournamentController extends Controller return $view; } + private function getParticipantsForSingle(Request $request, Tournament $tournament) { + return $tournament->participants() + ->where(function ($query) use ($request, $tournament) { + $query->where('placement', '<=', $tournament->limit_scoreboard); + $query->orWhereJsonContains('roles', 'admin'); + $query->orWhereJsonContains('roles', 'monitor'); + if ($request->user()) { + $query->orWhere('user_id', '=', $request->user()->id); + } + }) + ->with(['user']) + ->get(); + } + } diff --git a/app/Models/Protocol.php b/app/Models/Protocol.php index 3ab422f..a8bfb0d 100644 --- a/app/Models/Protocol.php +++ b/app/Models/Protocol.php @@ -52,6 +52,34 @@ class Protocol extends Model ProtocolAdded::dispatch($protocol); } + public static function crewRoleAdded(Tournament $tournament, User $crew, $role, User $user) { + $protocol = static::create([ + 'tournament_id' => $tournament->id, + 'user_id' => $user->id, + 'type' => 'crew.add', + 'details' => [ + 'tournament' => static::tournamentMemo($tournament), + 'role' => $role, + 'user' => static::userMemo($crew), + ], + ]); + ProtocolAdded::dispatch($protocol); + } + + public static function crewRoleRemoved(Tournament $tournament, User $crew, $role, User $user) { + $protocol = static::create([ + 'tournament_id' => $tournament->id, + 'user_id' => $user->id, + 'type' => 'crew.remove', + 'details' => [ + 'tournament' => static::tournamentMemo($tournament), + 'role' => $role, + 'user' => static::userMemo($crew), + ], + ]); + ProtocolAdded::dispatch($protocol); + } + public static function groupAssignment(Tournament $tournament, User $assignee, $picks, User $user) { $protocol = static::create([ 'tournament_id' => $tournament->id, diff --git a/app/Policies/TournamentPolicy.php b/app/Policies/TournamentPolicy.php index 22854d5..a3770ca 100644 --- a/app/Policies/TournamentPolicy.php +++ b/app/Policies/TournamentPolicy.php @@ -116,6 +116,18 @@ class TournamentPolicy return $tournament->accept_applications && !$user->isRunner($tournament) && !$user->isApplicant($tournament); } + /** + * Determine whether the user can manage the crew for the tournament. + * + * @param \App\Models\User $user + * @param \App\Models\Tournament $tournament + * @return \Illuminate\Auth\Access\Response|bool + */ + public function manageCrew(User $user, Tournament $tournament) + { + return $user->isAdmin() || $user->isTournamentAdmin($tournament); + } + /** * Determine whether the user can view the tournament protocol. * diff --git a/resources/js/components/applications/Button.jsx b/resources/js/components/applications/Button.jsx index 2361b09..fa7251e 100644 --- a/resources/js/components/applications/Button.jsx +++ b/resources/js/components/applications/Button.jsx @@ -24,6 +24,7 @@ const ApplicationsButton = ({ tournament }) => { return <> + ))} + + + ))} + + {t('events.addCrew')} +
+ c.user_id)} + onChange={(e) => setNewUser(e.target.value)} + value={newUser} + /> +
+ {['admin', 'monitor'].map((role => + + ))} +
+
+
+ ; +}; + +Crew.propTypes = { + toggleRole: PropTypes.func, + tournament: PropTypes.shape({ + participants: PropTypes.arrayOf(PropTypes.shape({ + })), + }), +}; + +export default Crew; diff --git a/resources/js/components/tournament/CrewDialog.jsx b/resources/js/components/tournament/CrewDialog.jsx new file mode 100644 index 0000000..9c07fd4 --- /dev/null +++ b/resources/js/components/tournament/CrewDialog.jsx @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { Button, Modal } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import Crew from './Crew'; +import Loading from '../common/Loading'; + +const CrewDialog = ({ + onHide, + show, + toggleRole, + tournament, +}) => { + const { t } = useTranslation(); + + return + + + {t('tournaments.manageCrew')} + + + + }> + + + + + + + ; +}; + +CrewDialog.propTypes = { + onHide: PropTypes.func, + show: PropTypes.bool, + toggleRole: PropTypes.func, + tournament: PropTypes.shape({ + id: PropTypes.number, + }), +}; + +export default CrewDialog; diff --git a/resources/js/components/tournament/Detail.jsx b/resources/js/components/tournament/Detail.jsx index e90ab99..b764755 100644 --- a/resources/js/components/tournament/Detail.jsx +++ b/resources/js/components/tournament/Detail.jsx @@ -85,6 +85,7 @@ const Detail = ({ + : null} {mayExportTournament(user, tournament) ? : null} @@ -185,6 +196,7 @@ Detail.propTypes = { actions: PropTypes.shape({ addRound: PropTypes.func, editContent: PropTypes.func, + manageCrew: PropTypes.func, moreRounds: PropTypes.func, selfAssignGroups: PropTypes.func, }).isRequired, diff --git a/resources/js/components/tournament/ExportButton.jsx b/resources/js/components/tournament/ExportButton.jsx index b0c796f..ce0cf5e 100644 --- a/resources/js/components/tournament/ExportButton.jsx +++ b/resources/js/components/tournament/ExportButton.jsx @@ -36,6 +36,7 @@ const ExportButton = ({ tournament }) => {