]> git.localhorst.tv Git - alttp.git/commitdiff
application admin UI
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 10 Apr 2022 22:54:55 +0000 (00:54 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 10 Apr 2022 22:54:55 +0000 (00:54 +0200)
21 files changed:
app/Events/ApplicationChanged.php [new file with mode: 0644]
app/Events/ApplicationRemoved.php [new file with mode: 0644]
app/Http/Controllers/ApplicationController.php [new file with mode: 0644]
app/Http/Controllers/TournamentController.php
app/Models/Participant.php
app/Models/Protocol.php
app/Policies/ApplicationPolicy.php
resources/js/components/applications/Button.js [new file with mode: 0644]
resources/js/components/applications/Dialog.js [new file with mode: 0644]
resources/js/components/applications/Item.js [new file with mode: 0644]
resources/js/components/applications/List.js [new file with mode: 0644]
resources/js/components/common/Icon.js
resources/js/components/pages/Tournament.js
resources/js/components/protocol/Item.js
resources/js/components/tournament/Detail.js
resources/js/helpers/Application.js [new file with mode: 0644]
resources/js/helpers/Tournament.js
resources/js/helpers/permissions.js
resources/js/i18n/de.js
resources/js/i18n/en.js
routes/api.php

diff --git a/app/Events/ApplicationChanged.php b/app/Events/ApplicationChanged.php
new file mode 100644 (file)
index 0000000..eb37074
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+namespace App\Events;
+
+use App\Models\Application;
+use Illuminate\Broadcasting\Channel;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Broadcasting\PresenceChannel;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+class ApplicationChanged implements ShouldBroadcast
+{
+       use Dispatchable, InteractsWithSockets, SerializesModels;
+
+       /**
+        * Create a new event instance.
+        *
+        * @return void
+        */
+       public function __construct(Application $application)
+       {
+               $this->application = $application;
+       }
+
+       /**
+        * Get the channels the event should broadcast on.
+        *
+        * @return \Illuminate\Broadcasting\Channel|array
+        */
+       public function broadcastOn()
+       {
+               return new Channel('Tournament.'.$this->application->tournament_id);
+       }
+
+       public $application;
+
+}
diff --git a/app/Events/ApplicationRemoved.php b/app/Events/ApplicationRemoved.php
new file mode 100644 (file)
index 0000000..26ba162
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Events;
+
+use Illuminate\Broadcasting\Channel;
+use Illuminate\Broadcasting\InteractsWithSockets;
+use Illuminate\Broadcasting\PresenceChannel;
+use Illuminate\Broadcasting\PrivateChannel;
+use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+use Illuminate\Foundation\Events\Dispatchable;
+use Illuminate\Queue\SerializesModels;
+
+class ApplicationRemoved implements ShouldBroadcast
+{
+       use Dispatchable, InteractsWithSockets, SerializesModels;
+
+       /**
+        * Create a new event instance.
+        *
+        * @return void
+        */
+       public function __construct($application_id, $tournament_id)
+       {
+               $this->application_id = $application_id;
+               $this->tournament_id = $tournament_id;
+       }
+
+       /**
+        * Get the channels the event should broadcast on.
+        *
+        * @return \Illuminate\Broadcasting\Channel|array
+        */
+       public function broadcastOn()
+       {
+               return new Channel('Tournament.'.$this->tournament_id);
+       }
+
+       public $application_id;
+       public $tournament_id;
+
+}
diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php
new file mode 100644 (file)
index 0000000..b0ab063
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Events\ApplicationChanged;
+use App\Events\ApplicationRemoved;
+use App\Models\Application;
+use App\Models\Participant;
+use App\Models\Protocol;
+use Illuminate\Http\Request;
+
+class ApplicationController extends Controller
+{
+
+       public function accept(Request $request, Application $application) {
+               $this->authorize('accept', $application);
+
+               $participant = Participant::firstOrCreate([
+                       'tournament_id' => $application->tournament_id,
+                       'user_id' => $application->user_id,
+               ]);
+               $participant->makeRunner();
+
+               $application->delete();
+               ApplicationRemoved::dispatch($application->id, $application->tournament_id);
+
+               Protocol::applicationAccepted($application->tournament, $application, $request->user());
+
+               return $participant->toJson();
+       }
+
+       public function reject(Request $request, Application $application) {
+               $this->authorize('reject', $application);
+
+               $application->denied = true;
+               $application->save();
+               ApplicationChanged::dispatch($application);
+
+               Protocol::applicationRejected($application->tournament, $application, $request->user());
+
+               return $application->toJson();
+       }
+
+}
index 9d62c23bac3475f2becac58caca85296756c68bc..6ef003837c1f5b99c1d4bf25003bec42706847ea 100644 (file)
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
 
 use App\Events\ApplicationAdded;
 use App\Models\Application;
+use App\Models\Protocol;
 use App\Models\Tournament;
 use Illuminate\Auth\Access\AuthorizationException;
 use Illuminate\Http\Request;
@@ -18,6 +19,7 @@ class TournamentController extends Controller
                $application->user_id = $request->user()->id;
                $application->save();
                ApplicationAdded::dispatch($application);
+               Protocol::applicationReceived($tournament, $application, $request->user());
                return $tournament->toJson();
        }
 
index 91b946d887fe726b2e066faf77d40991b8682485..38ecfdadf458a413ce71043a22ff69cb9a655ca1 100644 (file)
@@ -82,6 +82,18 @@ class Participant extends Model
                return in_array('admin', $this->roles);
        }
 
+       public function makeRunner() {
+               if (!is_array($this->roles)) {
+                       $this->roles = ['runner'];
+               } else if (!in_array('runner', $this->roles)) {
+                       $newRoles = array_values($this->roles);
+                       $newRoles[] = 'runner';
+                       $this->roles = $newRoles;
+               }
+               $this->save();
+               ParticipantChanged::dispatch($this);
+       }
+
 
        public function tournament() {
                return $this->belongsTo(Tournament::class);
@@ -96,6 +108,11 @@ class Participant extends Model
                'roles' => 'array',
        ];
 
+       protected $fillable = [
+               'tournament_id',
+               'user_id',
+       ];
+
        protected $with = [
                'user',
        ];
index 995f9c02ab8707f33825d88147ba0191efa54cad..7a90851a706c43b0d0b5e0ce417eb30fdbe6df35 100644 (file)
@@ -10,6 +10,48 @@ class Protocol extends Model
 {
        use HasFactory;
 
+       public static function applicationAccepted(Tournament $tournament, Application $application, User $user) {
+               $protocol = static::create([
+                       'tournament_id' => $tournament->id,
+                       'user_id' => $user->id,
+                       'type' => 'application.accepted',
+                       'details' => [
+                               'tournament' => static::tournamentMemo($tournament),
+                               'application' => static::applicationMemo($application),
+                               'user' => static::userMemo($application->user),
+                       ],
+               ]);
+               ProtocolAdded::dispatch($protocol);
+       }
+
+       public static function applicationReceived(Tournament $tournament, Application $application, User $user) {
+               $protocol = static::create([
+                       'tournament_id' => $tournament->id,
+                       'user_id' => $user->id,
+                       'type' => 'application.received',
+                       'details' => [
+                               'tournament' => static::tournamentMemo($tournament),
+                               'application' => static::applicationMemo($application),
+                               'user' => static::userMemo($application->user),
+                       ],
+               ]);
+               ProtocolAdded::dispatch($protocol);
+       }
+
+       public static function applicationRejected(Tournament $tournament, Application $application, User $user) {
+               $protocol = static::create([
+                       'tournament_id' => $tournament->id,
+                       'user_id' => $user->id,
+                       'type' => 'application.rejected',
+                       'details' => [
+                               'tournament' => static::tournamentMemo($tournament),
+                               'application' => static::applicationMemo($application),
+                               'user' => static::userMemo($application->user),
+                       ],
+               ]);
+               ProtocolAdded::dispatch($protocol);
+       }
+
        public static function resultCommented(Tournament $tournament, Result $result, User $user) {
                $protocol = static::create([
                        'tournament_id' => $tournament->id,
@@ -115,6 +157,13 @@ class Protocol extends Model
        }
 
 
+       protected static function applicationMemo(Application $application) {
+               return [
+                       'id' => $application->id,
+                       'denied' => $application->denied,
+               ];
+       }
+
        protected static function resultMemo(Result $result) {
                return [
                        'id' => $result->id,
@@ -137,6 +186,7 @@ class Protocol extends Model
        protected static function tournamentMemo(Tournament $tournament) {
                return [
                        'id' => $tournament->id,
+                       'accept_applications' => $tournament->accept_applications,
                        'locked' => $tournament->locked,
                        'no_record' => $tournament->no_record,
                        'title' => $tournament->title,
@@ -149,6 +199,7 @@ class Protocol extends Model
                        'username' => $user->username,
                        'discriminator' => $user->discriminator,
                        'avatar' => $user->avatar,
+                       'nickname' => $user->nickname,
                ];
        }
 
index 3dc9ad8c54cc11a89c74c9924b38696abef71465..a8160518074e6202f24641c8d749b3d863d59a6d 100644 (file)
@@ -94,4 +94,30 @@ class ApplicationPolicy
                return false;
        }
 
+       /**
+        * Determine whether the user can accept the application.
+        *
+        * @param  \App\Models\User  $user
+        * @param  \App\Models\Application  $application
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function accept(User $user, Application $application)
+       {
+               return $user->isAdmin()
+                       || $user->isTournamentAdmin($application->tournament);
+       }
+
+       /**
+        * Determine whether the user can accept the application.
+        *
+        * @param  \App\Models\User  $user
+        * @param  \App\Models\Application  $application
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function reject(User $user, Application $application)
+       {
+               return $user->isAdmin()
+                       || $user->isTournamentAdmin($application->tournament);
+       }
+
 }
diff --git a/resources/js/components/applications/Button.js b/resources/js/components/applications/Button.js
new file mode 100644 (file)
index 0000000..415042b
--- /dev/null
@@ -0,0 +1,53 @@
+import PropTypes from 'prop-types';
+import React, { useState } from 'react';
+import { Badge, Button } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+
+import Dialog from './Dialog';
+import Icon from '../common/Icon';
+import { mayHandleApplications } from '../../helpers/permissions';
+import { getPendingApplications } from '../../helpers/Tournament';
+import { withUser } from '../../helpers/UserContext';
+import i18n from '../../i18n';
+
+const ApplicationsButton = ({ tournament, user }) => {
+       const [showDialog, setShowDialog] = useState(false);
+
+       if (!user || !tournament.accept_applications || !mayHandleApplications(user, tournament)) {
+               return null;
+       }
+
+       const pending = getPendingApplications(tournament);
+
+       return <>
+               <Button
+                       onClick={() => setShowDialog(true)}
+                       title={i18n.t('tournaments.applications')}
+                       variant="primary"
+               >
+                       <Icon.APPLICATIONS title="" />
+                       {pending.length ?
+                               <>
+                                       {' '}
+                                       <Badge>{pending.length}</Badge>
+                               </>
+                       : null}
+               </Button>
+               <Dialog
+                       onHide={() => setShowDialog(false)}
+                       show={showDialog}
+                       tournament={tournament}
+               />
+       </>;
+};
+
+ApplicationsButton.propTypes = {
+       tournament: PropTypes.shape({
+               accept_applications: PropTypes.bool,
+               id: PropTypes.number,
+       }),
+       user: PropTypes.shape({
+       }),
+};
+
+export default withTranslation()(withUser(ApplicationsButton));
diff --git a/resources/js/components/applications/Dialog.js b/resources/js/components/applications/Dialog.js
new file mode 100644 (file)
index 0000000..a42fb3d
--- /dev/null
@@ -0,0 +1,41 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Alert, Button, Modal } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+
+import List from './List';
+import i18n from '../../i18n';
+
+const Dialog = ({ onHide, show, tournament }) =>
+<Modal className="applications-dialog" onHide={onHide} show={show}>
+       <Modal.Header closeButton>
+               <Modal.Title>
+                       {i18n.t('tournaments.applications')}
+               </Modal.Title>
+       </Modal.Header>
+       <Modal.Body className="p-0">
+               {tournament.applications && tournament.applications.length ?
+                       <List tournament={tournament} />
+               :
+                       <Alert variant="info">
+                               {i18n.t('tournaments.noApplications')}
+                       </Alert>
+               }
+       </Modal.Body>
+       <Modal.Footer>
+               <Button onClick={onHide} variant="secondary">
+                       {i18n.t('button.close')}
+               </Button>
+       </Modal.Footer>
+</Modal>;
+
+Dialog.propTypes = {
+       onHide: PropTypes.func,
+       show: PropTypes.bool,
+       tournament: PropTypes.shape({
+               applications: PropTypes.arrayOf(PropTypes.shape({
+               }))
+       }),
+};
+
+export default withTranslation()(Dialog);
diff --git a/resources/js/components/applications/Item.js b/resources/js/components/applications/Item.js
new file mode 100644 (file)
index 0000000..8f6aef1
--- /dev/null
@@ -0,0 +1,60 @@
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, ListGroup } from 'react-bootstrap';
+import { withTranslation } from 'react-i18next';
+import toastr from 'toastr';
+
+import Icon from '../common/Icon';
+import Box from '../users/Box';
+import i18n from '../../i18n';
+
+const accept = async (tournament, application) => {
+       try {
+               await axios.post(`/api/application/${application.id}/accept`);
+               toastr.success(i18n.t('applications.acceptSuccess'));
+       } catch (e) {
+               toastr.error(i18n.t('applications.acceptError'));
+       }
+};
+
+const reject = async (tournament, application) => {
+       try {
+               await axios.post(`/api/application/${application.id}/reject`);
+               toastr.success(i18n.t('applications.rejectSuccess'));
+       } catch (e) {
+               toastr.error(i18n.t('applications.rejectError'));
+       }
+};
+
+const Item = ({ application, tournament }) =>
+<ListGroup.Item className="d-flex justify-content-between align-items-center">
+       <Box discriminator user={application.user} />
+       <div className="button-bar">
+               <Button
+                       onClick={() => accept(tournament, application)}
+                       title={i18n.t('applications.accept')}
+                       variant="success"
+               >
+                       <Icon.ACCEPT title="" />
+               </Button>
+               <Button
+                       onClick={() => reject(tournament, application)}
+                       title={i18n.t('applications.reject')}
+                       variant="danger"
+               >
+                       <Icon.REJECT title="" />
+               </Button>
+       </div>
+</ListGroup.Item>;
+
+Item.propTypes = {
+       application: PropTypes.shape({
+               user: PropTypes.shape({
+               }),
+       }),
+       tournament: PropTypes.shape({
+       }),
+};
+
+export default withTranslation()(Item);
diff --git a/resources/js/components/applications/List.js b/resources/js/components/applications/List.js
new file mode 100644 (file)
index 0000000..6460be3
--- /dev/null
@@ -0,0 +1,21 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { ListGroup } from 'react-bootstrap';
+
+import Item from './Item';
+
+const List = ({ tournament }) =>
+<ListGroup variant="flush">
+       {tournament.applications.map(application =>
+               <Item application={application} key={application.id} tournament={tournament} />
+       )}
+</ListGroup>;
+
+List.propTypes = {
+       tournament: PropTypes.shape({
+               applications: PropTypes.arrayOf(PropTypes.shape({
+               })),
+       }),
+};
+
+export default List;
index 607deb9b0af436e3525e3921bbea969163e58a6b..6d050902d584cbd55293fa8b7859b72f95df580f 100644 (file)
@@ -57,8 +57,10 @@ const makePreset = (presetDisplayName, presetName) => {
        return withTranslation()(preset);
 };
 
+Icon.ACCEPT = makePreset('AcceptIcon', 'square-check');
 Icon.ADD = makePreset('AddIcon', 'circle-plus');
 Icon.APPLY = makePreset('ApplyIcon', 'right-to-bracket');
+Icon.APPLICATIONS = makePreset('ApplicationsIcon', 'person-running');
 Icon.DISCORD = makePreset('DiscordIcon', ['fab', 'discord']);
 Icon.EDIT = makePreset('EditIcon', 'edit');
 Icon.FINISHED = makePreset('FinishedIcon', 'square-check');
@@ -69,6 +71,7 @@ Icon.LOCKED = makePreset('LockedIcon', 'lock');
 Icon.LOGOUT = makePreset('LogoutIcon', 'sign-out-alt');
 Icon.PENDING = makePreset('PendingIcon', 'clock');
 Icon.PROTOCOL = makePreset('ProtocolIcon', 'file-alt');
+Icon.REJECT = makePreset('RejectIcon', 'square-xmark');
 Icon.RESULT = makePreset('ResultIcon', 'clock');
 Icon.SECOND_PLACE = makePreset('SecondPlaceIcon', 'medal');
 Icon.STREAM = makePreset('StreamIcon', ['fab', 'twitch']);
index 729e55d138d856d2da8491a0a20f177831d05cee..00a19343c11b1c99c59d59937946f6ce85f89b17 100644 (file)
@@ -13,6 +13,7 @@ import {
        patchResult,
        patchRound,
        patchUser,
+       removeApplication,
        sortParticipants,
 } from '../../helpers/Tournament';
 
@@ -53,7 +54,13 @@ const Tournament = () => {
                                        setTournament(tournament => patchApplication(tournament, e.application));
                                }
                        })
+                       .listen('ApplicationRemoved', e => {
+                               if (e.application_id) {
+                                       setTournament(tournament => removeApplication(tournament, e.application_id));
+                               }
+                       })
                        .listen('ParticipantChanged', e => {
+                               console.log(e);
                                if (e.participant) {
                                        setTournament(tournament => patchParticipant(tournament, e.participant));
                                }
index 74bfa50039cf82ab62c3395d2e2683d2ec1072df..9fcc7e5c6e09b539b837a2056513714dae8af359 100644 (file)
@@ -7,6 +7,7 @@ import { Trans, withTranslation } from 'react-i18next';
 import Icon from '../common/Icon';
 import Spoiler from '../common/Spoiler';
 import { formatTime } from '../../helpers/Result';
+import { getUserName } from '../../helpers/User';
 import i18n from '../../i18n';
 
 const getEntryDate = entry => {
@@ -16,6 +17,11 @@ const getEntryDate = entry => {
                : dateStr;
 };
 
+const getEntryDetailsUsername = entry => {
+       if (!entry || !entry.details || !entry.details.user) return 'Anonymous';
+       return getUserName(entry.details.user);
+};
+
 const getEntryRoundNumber = entry =>
        (entry && entry.details && entry.details.round && entry.details.round.number) || '?';
 
@@ -28,6 +34,16 @@ const getEntryResultTime = entry => {
 
 const getEntryDescription = entry => {
        switch (entry.type) {
+               case 'application.accepted':
+               case 'application.received':
+               case 'application.rejected':
+                       return i18n.t(
+                               `protocol.description.${entry.type}`,
+                               {
+                                       ...entry,
+                                       username: getEntryDetailsUsername(entry),
+                               },
+                       );
                case 'result.report': {
                        const time = getEntryResultTime(entry);
                        return <Trans i18nKey={`protocol.description.${entry.type}`}>
index b20f2082c36ca2273584f55a1cb6890c194801fc..cdc9b601bd3b3c81448b1e3015e57b312f3922bc 100644 (file)
@@ -5,6 +5,7 @@ import { withTranslation } from 'react-i18next';
 
 import ApplyButton from './ApplyButton';
 import Scoreboard from './Scoreboard';
+import ApplicationsButton from '../applications/Button';
 import Protocol from '../protocol/Protocol';
 import Rounds from '../rounds/List';
 import Box from '../users/Box';
@@ -46,6 +47,7 @@ const Detail = ({
                        <div className="d-flex align-items-center justify-content-between">
                                <h1>{tournament.title}</h1>
                                <div className="button-bar">
+                                       <ApplicationsButton tournament={tournament} />
                                        <ApplyButton tournament={tournament} />
                                        {mayViewProtocol(user, tournament) ?
                                                <Protocol id={tournament.id} />
diff --git a/resources/js/helpers/Application.js b/resources/js/helpers/Application.js
new file mode 100644 (file)
index 0000000..ee0b96d
--- /dev/null
@@ -0,0 +1,17 @@
+import User from './User';
+
+export const compareUsername = (a, b) => {
+       const a_name = a && a.user ? User.getUserName(a.user) : '';
+       const b_name = b && b.user ? User.getUserName(b.user) : '';
+       return a_name.localeCompare(b_name);
+};
+
+export const isDenied = a => a && a.denied;
+
+export const isPending = a => a && !a.denied;
+
+export default {
+       compareUsername,
+       isDenied,
+       isPending,
+};
index 25981d87fe0c8e11d58a82a342218d47a2dd2864..60354c2b6bdeae9d927a0cd32714b74e6941d830 100644 (file)
@@ -1,3 +1,4 @@
+import Application from './Application';
 import Participant from './Participant';
 import Round from './Round';
 
@@ -15,6 +16,13 @@ export const findParticipant = (tournament, user) => {
        return tournament.participants.find(p => p.user_id == user.id);
 };
 
+export const getPendingApplications = tournament => {
+       if (!tournament || !tournament.applications || !tournament.applications.length) return [];
+       return tournament.applications
+               .filter(Application.isPending)
+               .sort(Application.compareUsername);
+};
+
 export const getRunners = tournament => {
        if (!tournament || !tournament.participants || !tournament.participants.length) return [];
        return tournament.participants
@@ -132,6 +140,16 @@ export const patchUser = (tournament, user) => {
        };
 };
 
+export const removeApplication = (tournament, id) => {
+       if (!tournament || !tournament.applications || !tournament.applications.find(a => a.id == id)) {
+               return tournament;
+       }
+       return {
+               ...tournament,
+               applications: tournament.applications.filter(a => a.id != id),
+       };
+};
+
 export const sortParticipants = tournament => {
        if (!tournament || !tournament.participants || !tournament.participants.length) {
                return tournament;
index cde0d05d7fd293f7114b6d7553ffb1144a9ffaa6..18d03bffce1c6be3f56d04de783b42d734578588 100644 (file)
@@ -58,6 +58,9 @@ export const mayApply = (user, tournament) =>
        user && tournament && tournament.accept_applications &&
                !isRunner(user, tournament) && !isApplicant(user, tournament);
 
+export const mayHandleApplications = (user, tournament) =>
+       tournament && tournament.accept_applications && isTournamentAdmin(user, tournament);
+
 export const mayLockRound = (user, tournament) =>
        !tournament.locked && isTournamentAdmin(user, tournament);
 
index 76a597332c2862c8b1fdb92d4be95b316785b1e7..cc98394db01a204ce211b8f84dcf35eb5046236f 100644 (file)
@@ -1,6 +1,14 @@
 /* eslint-disable max-len */
 export default {
        translation: {
+               applications: {
+                       accept: 'Annehmen',
+                       acceptError: 'Fehler beim Annehmen',
+                       acceptSuccess: 'Angenommen',
+                       reject: 'Ablehnen',
+                       rejectSuccess: 'Abgelehnt',
+                       rejectError: 'Fehler beim Ablehnen',
+               },
                button: {
                        add: 'Hinzufügen',
                        back: 'Zurück',
@@ -125,6 +133,11 @@ export default {
                },
                protocol: {
                        description: {
+                               application: {
+                                       accepted: 'Anmeldung von {{username}} bestätigt',
+                                       received: 'Anmeldung von {{username}} erhalten',
+                                       rejected: 'Anmeldung von {{username}} abgelehnt',
+                               },
                                result: {
                                        comment: 'Ergebnis kommentiert: "{{details.result.comment}}"',
                                        report: 'Ergebnis von <0>{{time}}</0> eingetragen',
@@ -190,10 +203,12 @@ export default {
                        admins: 'Organisation',
                        applicationDenied: 'Antrag wurde abgelehnt',
                        applicationPending: 'Antrag wurde abgeschickt',
+                       applications: 'Anmeldungen',
                        apply: 'Beitreten',
                        applyError: 'Fehler beim Abschicken der Anfrage',
                        applySuccess: 'Anfrage gestellt',
                        monitors: 'Monitore',
+                       noApplications: 'Derzeit keine Anmeldungen',
                        noRecord: 'Turnier wird nicht gewertet',
                        scoreboard: 'Scoreboard',
                },
index 0b526ba743a2e2a010b1e386a2b963b20955d45d..cd6dea0c9c723665e97e2744c9439332cbdbd1ec 100644 (file)
@@ -1,6 +1,14 @@
 /* eslint-disable max-len */
 export default {
        translation: {
+               applications: {
+                       accept: 'Accept',
+                       acceptError: 'Error accepting',
+                       acceptSuccess: 'Accepted',
+                       reject: 'Reject',
+                       rejectSuccess: 'Rejected',
+                       rejectError: 'Error rejecting',
+               },
                button: {
                        add: 'Add',
                        back: 'Back',
@@ -125,6 +133,11 @@ export default {
                },
                protocol: {
                        description: {
+                               application: {
+                                       accepted: 'Application from {{username}} accepted',
+                                       received: 'Application from {{username}} received',
+                                       rejected: 'Application from {{username}} rejected',
+                               },
                                result: {
                                        comment: 'Result commented: "{{details.result.comment}}"',
                                        report: 'Result of {{time}} reported',
@@ -190,10 +203,12 @@ export default {
                        admins: 'Admins',
                        applicationDenied: 'Application denied',
                        applicationPending: 'Application pending',
+                       applications: 'Applications',
                        apply: 'Apply',
                        applyError: 'Error submitting application',
                        applySuccess: 'Application sent',
                        monitors: 'Monitors',
+                       noApplications: 'No applications at this point',
                        noRecord: 'Tournament set to not be recorded',
                        scoreboard: 'Scoreboard',
                },
index b9a4614eb322e8d12a629628c7c160b5dad52547..de8cd6c4836b27eea5be94c6568ee93724302dbc 100644 (file)
@@ -18,6 +18,9 @@ Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
        return $request->user();
 });
 
+Route::post('application/{application}/accept', 'App\Http\Controllers\ApplicationController@accept');
+Route::post('application/{application}/reject', 'App\Http\Controllers\ApplicationController@reject');
+
 Route::get('protocol/{tournament}', 'App\Http\Controllers\ProtocolController@forTournament');
 
 Route::post('results', 'App\Http\Controllers\ResultController@create');