]> git.localhorst.tv Git - alttp.git/commitdiff
verification revocation
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 26 Nov 2025 16:20:12 +0000 (17:20 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Wed, 26 Nov 2025 16:20:12 +0000 (17:20 +0100)
app/Http/Controllers/ResultController.php
app/Models/Protocol.php
app/Policies/ResultPolicy.php
resources/js/components/protocol/Item.jsx
resources/js/components/results/Verification.jsx
resources/js/helpers/permissions.js
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/js/pages/Tournament.jsx
routes/api.php

index ec732b6d987d6d9c260dc8bfca79549bd5a0f17b..78a1c682f47ef67e489f7f7485475b87553b3669 100644 (file)
@@ -89,6 +89,24 @@ class ResultController extends Controller
                return $result->toJson();
        }
 
+       public function unverify(Request $request, Result $result) {
+               $this->authorize('unverify', $result);
+
+               $result->verified_at = null;
+               $result->verified_by()->associate(null);
+               $result->save();
+
+               Protocol::resultUnverified(
+                       $result->round->tournament,
+                       $result,
+                       $request->user(),
+               );
+
+               ResultChanged::dispatch($result);
+
+               return $result->toJson();
+       }
+
        public function verify(Request $request, Result $result) {
                $this->authorize('verify', $result);
 
index ed4ec164da5ed716ed585fbd645825e8662907c0..07a1491b0bfd8598c003f7e30329e5d9ca753519 100644 (file)
@@ -94,6 +94,21 @@ class Protocol extends Model
                ProtocolAdded::dispatch($protocol);
        }
 
+       public static function resultUnverified(Tournament $tournament, Result $result, User $user) {
+               $protocol = static::create([
+                       'tournament_id' => $tournament->id,
+                       'user_id' => $user->id,
+                       'type' => 'result.unverify',
+                       'details' => [
+                               'tournament' => static::tournamentMemo($tournament),
+                               'result' => static::resultMemo($result),
+                               'runner' => static::userMemo($result->user),
+                               'round' => static::roundMemo($result->round),
+                       ],
+               ]);
+               ProtocolAdded::dispatch($protocol);
+       }
+
        public static function resultVerified(Tournament $tournament, Result $result, User $user) {
                $protocol = static::create([
                        'tournament_id' => $tournament->id,
index 3e0e4eabca3aa75c61560a49725c72b9715866dd..4c4bff50976dbc3c6ecd42b0935907d238ae025e 100644 (file)
@@ -92,6 +92,18 @@ class ResultPolicy
                return false;
        }
 
+       /**
+        * Determine whether the user can unverify the result.
+        *
+        * @param  \App\Models\User  $user
+        * @param  \App\Models\Result  $result
+        * @return \Illuminate\Auth\Access\Response|bool
+        */
+       public function unverify(User $user, Result $result)
+       {
+               return $user->isTournamentCrew($result->round->tournament);
+       }
+
        /**
         * Determine whether the user can verify the result.
         *
index b7d60528f95b1b62491cc7407a110003c0c66f52..e7134f9c4a2b4e8382d91d1a4a24a0857e169d5c 100644 (file)
@@ -94,6 +94,7 @@ const getEntryDescription = (entry, t) => {
                                <Spoiler>{{time}}</Spoiler>,
                        </Trans>;
                }
+               case 'result.unverify':
                case 'result.verify': {
                        const number = getEntryRoundNumber(entry);
                        const runner = getEntryResultRunner(entry);
@@ -141,6 +142,7 @@ const getEntryIcon = entry => {
                        return <Icon.VERIFIED />;
                case 'round.create':
                        return <Icon.ADD />;
+               case 'result.unverify':
                case 'round.delete':
                        return <Icon.REMOVE />;
                case 'round.edit':
index 281c979aeb708d470f6e0440ff2cff53cdae3e46..43113c514cd782ecf6caf0e805b776e91ec2d4ef 100644 (file)
@@ -5,10 +5,11 @@ import { Trans, useTranslation } from 'react-i18next';
 
 import Icon from '../common/Icon';
 import Box from '../users/Box';
-import { mayVerifyResult } from '../../helpers/permissions';
+import { mayUnverifyResult, mayVerifyResult } from '../../helpers/permissions';
 import { useUser } from '../../hooks/user';
 
 const Verification = ({ actions, result, round, tournament }) => {
+       const [unverifying, setUnverifying] = React.useState(false);
        const [verifying, setVerifying] = React.useState(false);
 
        const { t } = useTranslation();
@@ -19,6 +20,21 @@ const Verification = ({ actions, result, round, tournament }) => {
                [user, result, round, tournament],
        );
 
+       const mayUnverify = React.useMemo(
+               () => mayUnverifyResult(user, tournament, round, result),
+               [user, result, round, tournament],
+       );
+
+       const handleUnverifyClick = React.useCallback(async () => {
+               setUnverifying(true);
+               try {
+                       await actions.unverifyResult(result);
+               } catch (e) {
+                       console.error(e);
+               }
+               setUnverifying(false);
+       }, [actions, result]);
+
        const handleVerifyClick = React.useCallback(async () => {
                setVerifying(true);
                try {
@@ -32,15 +48,26 @@ const Verification = ({ actions, result, round, tournament }) => {
        const date = result?.verified_at ? new Date(result.verified_at) : null;
 
        if (result?.verified_at) {
-               return <div>
-                       {result.verified_by ?
-                               <Trans i18nKey="results.verifiedAtBy">
-                                       {{date}}
-                                       <Box user={result.verified_by} />
-                               </Trans>
-                       :
-                               t('results.verifiedAt', { date })
-                       }
+               return <div className="d-flex justify-content-between">
+                       <div>
+                               {result.verified_by ?
+                                       <Trans i18nKey="results.verifiedAtBy">
+                                               {{date}}
+                                               <Box user={result.verified_by} />
+                                       </Trans>
+                               :
+                                       t('results.verifiedAt', { date })
+                               }
+                       </div>
+                       {mayUnverify ?
+                               <Button
+                                       onClick={handleUnverifyClick}
+                                       title={t('results.unverify')}
+                                       variant="outline-danger"
+                               >
+                                       <Icon.REMOVE title="" />
+                               </Button>
+                       : null}
                </div>;
        }
 
@@ -62,6 +89,7 @@ const Verification = ({ actions, result, round, tournament }) => {
 
 Verification.propTypes = {
        actions: PropTypes.shape({
+               unverifyResult: PropTypes.func,
                verifyResult: PropTypes.func,
        }),
        result: PropTypes.shape({
index 9a004c1d33e4e0aca7480745150090bea7d30c9f..e6fc01c8b2ea43a4ab651f045e6765b43139892d 100644 (file)
@@ -221,6 +221,9 @@ export const mayVerifyResult = (user, tournament, round, result) => {
        return mayVerifyResults(user, tournament) && user && result && user.id !== result.user_id;
 };
 
+export const mayUnverifyResults = mayVerifyResults;
+export const mayUnverifyResult = mayVerifyResult;
+
 // Twitch
 
 export const mayManageTwitchBot = user => isAnyChannelAdmin(user);
index 10797ccbe587a799ee7831c32a59ce9eff40cacd..181f88b4f13529920120b0d528f30f1842a3bf0e 100644 (file)
@@ -585,6 +585,7 @@ export default {
                                result: {
                                        comment: 'Ergebnis von Runde {{number}} kommentiert: <1>{{comment}}</1>',
                                        report: 'Ergebnis von <1>{{time}}</1> bei Runde {{number}} eingetragen',
+                                       unverify: 'Verifikation von Ergebnis in Runde {{number}} von {{runner}} (<2>{{time}}</2>) zurückgezogen',
                                        verify: 'Ergebnis in Runde {{number}} von {{runner}} (<2>{{time}}</2>) verifiziert',
                                },
                                round: {
@@ -636,6 +637,9 @@ export default {
                        score: 'Punkte',
                        table: 'Tabelle',
                        time: 'Zeit: {{ time }}',
+                       unverify: 'Verifikation entziehen',
+                       unverifyError: 'Fehler beim Entziehen',
+                       unverifySuccess: 'Verifikation entzogen',
                        verification: 'Verifikation',
                        verificationPending: 'Noch nicht verifiziert',
                        verifiedAt: 'Verifiziert am {{ date, L }}',
index 0491e31087d00c56edf58b0e84ae2eadf8613f12..3b1f8759ae3bf85db871f1756389489f9d78b2d4 100644 (file)
@@ -585,6 +585,7 @@ export default {
                                result: {
                                        comment: 'Result of round {{number}} commented: <1>{{comment}}</1>',
                                        report: 'Result of <1>{{time}}</1> reported for round {{number}}',
+                                       unverify: 'Revoked verification for round {{number}} result of {{runner}} (<2>{{time}}</2>)',
                                        verify: 'Verified round {{number}} result of {{runner}} (<2>{{time}}</2>)',
                                },
                                round: {
@@ -636,6 +637,9 @@ export default {
                        score: 'Score',
                        table: 'Table',
                        time: 'Time: {{ time }}',
+                       unverify: 'Revoke verification',
+                       unverifyError: 'Error revoking verification',
+                       unverifySuccess: 'Verification revoked',
                        verification: 'Verification',
                        verificationPending: 'Pending verification',
                        verifiedAt: 'Verified on {{ date, L }}',
index d1ffaaebdce7cffe954db0677ee7609d9594c72d..54aa92fd74681148b09fff31a7f08be714799368 100644 (file)
@@ -14,6 +14,7 @@ import Dialog from '../components/techniques/Dialog';
 import Detail from '../components/tournament/Detail';
 import {
        mayEditContent,
+       mayUnverifyResults,
        mayVerifyResults,
 } from '../helpers/permissions';
 import { getTranslation } from '../helpers/Technique';
@@ -173,6 +174,16 @@ export const Component = () => {
                }
        }, [id, t]);
 
+       const unverifyResult = React.useCallback(async (result) => {
+               try {
+                       const response = await axios.post(`/api/results/${result.id}/unverify`);
+                       toastr.success(t('results.unverifySuccess'));
+                       setTournament(tournament => patchResult(tournament, response.data));
+               } catch (e) {
+                       toastr.error(t('results.unverifyError', e));
+               }
+       });
+
        const verifyResult = React.useCallback(async (result) => {
                try {
                        const response = await axios.post(`/api/results/${result.id}/verify`);
@@ -191,6 +202,7 @@ export const Component = () => {
                } : null,
                moreRounds: canLoadMoreRounds(tournament) ? moreRounds : null,
                selfAssignGroups,
+               unverifyResult: mayUnverifyResults(user, tournament) ? unverifyResult : null,
                verifyResult: mayVerifyResults(user, tournament) ? verifyResult : null,
        }), [addRound, moreRounds, selfAssignGroups, tournament, user, verifyResult]);
 
index b599d4c8a25987e86741f574489e7928cdacd24c..aacbf493c1caa7fbd4833f19ddecb086cf82c167 100644 (file)
@@ -86,6 +86,7 @@ Route::get('protocol/{tournament}/{round}', 'App\Http\Controllers\ProtocolContro
 
 Route::post('results', 'App\Http\Controllers\ResultController@create');
 Route::post('results/{result}/verify', 'App\Http\Controllers\ResultController@verify');
+Route::post('results/{result}/unverify', 'App\Http\Controllers\ResultController@unverify');
 
 Route::post('rounds', 'App\Http\Controllers\RoundController@create');
 Route::put('rounds/{round}', 'App\Http\Controllers\RoundController@update');