return $round->toJson();
}
+ public function delete(Request $request, Round $round) {
+ $this->authorize('delete', $round);
+ if (count($round->results) > 0) {
+ return response('Forbidden', 403);
+ }
+ $round->load('tournament');
+ $round->delete();
+ Protocol::roundDeleted(
+ $round->tournament,
+ $round,
+ $request->user(),
+ );
+ return $round->toJson();
+ }
+
public function update(Request $request, Round $round) {
$this->authorize('update', $round);
ProtocolAdded::dispatch($protocol);
}
+ public static function roundDeleted(Tournament $tournament, Round $round, User $user) {
+ $protocol = static::create([
+ 'tournament_id' => $tournament->id,
+ 'user_id' => $user->id,
+ 'type' => 'round.delete',
+ 'details' => [
+ 'tournament' => static::tournamentMemo($tournament),
+ 'round' => static::roundMemo($round),
+ ],
+ ]);
+ ProtocolAdded::dispatch($protocol);
+ }
+
public static function roundEdited(Tournament $tournament, Round $round, User $user) {
$protocol = static::create([
'tournament_id' => $tournament->id,
namespace App\Models;
+use Illuminate\Broadcasting\Channel;
+use Illuminate\Database\Eloquent\BroadcastsEvents;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Round extends Model
{
+ use BroadcastsEvents;
use HasFactory;
public function protocols() {
return $this->tournament->protocols()->where('details->round->id', '=', $this->id);
}
+ public function broadcastOn($event) {
+ return [
+ new Channel('Tournament.'.$this->tournament_id),
+ ];
+ }
+
public function isComplete() {
if (count($this->tournament->participants) == 0) return false;
*/
public function delete(User $user, Round $round)
{
- return false;
+ return !$round->tournament->locked && $user->isTournamentAdmin($round->tournament) && count($round->results) == 0;
}
/**
</Trans>;
}
case 'round.create':
+ case 'round.delete':
case 'round.edit':
case 'round.lock':
case 'round.seed':
return <Icon.RESULT />;
case 'round.create':
return <Icon.ADD />;
+ case 'round.delete':
+ return <Icon.REMOVE />;
case 'round.lock':
case 'tournament.close':
case 'tournament.lock':
--- /dev/null
+import PropTypes from 'prop-types';
+import React, { useState } from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import DeleteDialog from './DeleteDialog';
+import Icon from '../common/Icon';
+
+const DeleteButton = ({
+ round,
+ tournament,
+}) => {
+ const [showDialog, setShowDialog] = useState(false);
+
+ const { t } = useTranslation();
+
+ return <>
+ <DeleteDialog
+ onHide={() => setShowDialog(false)}
+ round={round}
+ show={showDialog}
+ tournament={tournament}
+ />
+ <Button
+ onClick={() => setShowDialog(true)}
+ size="sm"
+ title={t('rounds.delete')}
+ variant="outline-danger"
+ >
+ <Icon.REMOVE title="" />
+ </Button>
+ </>;
+};
+
+DeleteButton.propTypes = {
+ round: PropTypes.shape({
+ locked: PropTypes.bool,
+ }),
+ tournament: PropTypes.shape({
+ }),
+};
+
+export default DeleteButton;
--- /dev/null
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Alert, Button, Modal } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+import toastr from 'toastr';
+
+const EditDialog = ({
+ onHide,
+ round,
+ show,
+}) => {
+ const { t } = useTranslation();
+
+ const handleDelete = React.useCallback(async() => {
+ try {
+ await axios.delete(`/api/rounds/${round.id}`);
+ onHide();
+ } catch (e) {
+ toastr.error(t('rounds.deleteError'));
+ }
+ }, [onHide, round, t]);
+
+ return <Modal className="edit-dialog" onHide={onHide} show={show}>
+ <Modal.Header closeButton>
+ <Modal.Title>
+ {t('rounds.delete')}
+ </Modal.Title>
+ </Modal.Header>
+ <Modal.Body>
+ <Alert variant="danger">
+ {t('rounds.deleteConfirmMessage', {
+ ...round,
+ date: new Date(round.created_at),
+ })}
+ </Alert>
+ </Modal.Body>
+ <Modal.Footer>
+ <Button onClick={onHide} variant="secondary">
+ {t('button.cancel')}
+ </Button>
+ <Button onClick={handleDelete} variant="danger">
+ {t('button.delete')}
+ </Button>
+ </Modal.Footer>
+ </Modal>;
+};
+
+EditDialog.propTypes = {
+ onHide: PropTypes.func,
+ round: PropTypes.shape({
+ created_at: PropTypes.string,
+ id: PropTypes.number,
+ }),
+ show: PropTypes.bool,
+ tournament: PropTypes.shape({
+ }),
+};
+
+export default EditDialog;
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { Button } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import EditDialog from './EditDialog';
import Icon from '../common/Icon';
-import i18n from '../../i18n';
const EditButton = ({
round,
}) => {
const [showDialog, setShowDialog] = useState(false);
+ const { t } = useTranslation();
+
return <>
<EditDialog
onHide={() => setShowDialog(false)}
<Button
onClick={() => setShowDialog(true)}
size="sm"
- title={i18n.t('rounds.edit')}
+ title={t('rounds.edit')}
variant="outline-secondary"
>
<Icon.EDIT title="" />
}),
};
-export default withTranslation()(EditButton);
+export default EditButton;
import PropTypes from 'prop-types';
import React from 'react';
import { Modal } from 'react-bootstrap';
-import { withTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import EditForm from './EditForm';
-import i18n from '../../i18n';
const EditDialog = ({
onHide,
round,
show,
-}) =>
-<Modal className="edit-dialog" onHide={onHide} show={show}>
- <Modal.Header closeButton>
- <Modal.Title>
- {i18n.t('rounds.edit')}
- </Modal.Title>
- </Modal.Header>
- <EditForm
- onCancel={onHide}
- round={round}
- />
-</Modal>;
+}) => {
+ const { t } = useTranslation();
+
+ return <Modal className="edit-dialog" onHide={onHide} show={show}>
+ <Modal.Header closeButton>
+ <Modal.Title>
+ {t('rounds.edit')}
+ </Modal.Title>
+ </Modal.Header>
+ <EditForm
+ onCancel={onHide}
+ round={round}
+ />
+ </Modal>;
+};
EditDialog.propTypes = {
onHide: PropTypes.func,
}),
};
-export default withTranslation()(EditDialog);
+export default EditDialog;
import React from 'react';
import { useTranslation } from 'react-i18next';
+import DeleteButton from './DeleteButton';
import EditButton from './EditButton';
import LockButton from './LockButton';
import SeedButton from './SeedButton';
import List from '../results/List';
import ReportButton from '../results/ReportButton';
import {
+ mayDeleteRound,
mayEditRound,
mayReportResult,
mayViewProtocol,
{mayViewProtocol(user, tournament, round) ?
<RoundProtocol roundId={round.id} tournamentId={tournament.id} />
: null}
+ {mayDeleteRound(user, tournament, round) ?
+ <DeleteButton round={round} tournament={tournament} />
+ : null}
</div>
</div>
<List round={round} tournament={tournament} />
import Participant from './Participant';
import Tournament from './Tournament';
+export const hasResults = (round) => {
+ return round && round.results && round.results.length > 0;
+};
+
export const isComplete = (tournament, round) => {
if (!tournament || !tournament.participants) return false;
if (tournament.type === 'open-async') return false;
};
export default {
+ hasResults,
isComplete,
patchResult,
};
return isRunner(user, tournament);
};
+export const mayDeleteRound = (user, tournament, round) =>
+ !tournament.locked && isTournamentAdmin(user, tournament) && !Round.hasResults(round);
+
export const mayEditRound = (user, tournament) =>
!tournament.locked && isTournamentAdmin(user, tournament);
chart: 'Diagramm',
close: 'Schließen',
confirm: 'Bestätigen',
+ delete: 'Löschen',
edit: 'Bearbeiten',
filter: 'Filter',
generate: 'Generieren',
},
round: {
create: 'Runde #{{number}} hinzugefügt',
+ delete: 'Runde #{{number}} gelöscht',
edit: 'Runde #{{number}} bearbeitet',
lock: 'Runde #{{number}} gesperrt',
seed: 'Seed für Runde #{{number}} eingetragen',
rounds: {
code: 'Code',
date: '{{ date, L }}',
+ delete: 'Runde löschen',
+ deleteConfirmMessage: 'Runde #{{ number }} vom {{ date, L }} löschen?',
+ deleteError: 'Fehler beim Löschen',
+ deleteSuccess: 'Runde gelöscht',
edit: 'Runde bearbeiten',
editError: 'Fehler beim Speichern',
editSuccess: 'Gespeichert',
chart: 'Chart',
close: 'Close',
confirm: 'Confirm',
+ delete: 'Delete',
edit: 'Edit',
filter: 'Filter',
generate: 'Generate',
},
round: {
create: 'Added round #{{number}}',
+ delete: 'Deleted round #{{number}}',
edit: 'Edited round #{{number}}',
lock: 'Round #{{number}} locked',
seed: 'Set seed for round #{{number}}',
rounds: {
code: 'Code',
date: '{{ date, L }}',
+ delete: 'Delete round',
+ deleteConfirmMessage: 'Remove round #{{ number }} from {{ date, L }}?',
+ deleteError: 'Error deleting round',
+ deleteSuccess: 'Round deleted',
edit: 'Edit round',
editError: 'Error saving round',
editSuccess: 'Saved successfully',
setTournament(tournament => patchRound(tournament, e.round));
}
})
+ .listen('.RoundDeleted', e => {
+ if (e.model) {
+ setTournament(tournament => ({
+ ...tournament,
+ rounds: tournament.rounds.filter((r) => r.id !== e.model.id),
+ }));
+ }
+ })
.listen('TournamentChanged', e => {
if (e.tournament) {
setTournament(tournament => ({ ...tournament, ...e.tournament }));
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::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');