From cdc9ce49fec3496d851f852b8a429356a45a9daa Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Wed, 26 Nov 2025 16:44:39 +0100 Subject: [PATCH] lookup getseed on result post --- app/Http/Controllers/ResultController.php | 11 ++++ app/Models/Result.php | 3 + .../2025_11_26_142512_result_got_seed_at.php | 28 +++++++++ .../js/components/results/DetailDialog.jsx | 29 +++++++++- resources/js/helpers/Result.jsx | 10 ++-- resources/js/helpers/Tournament.js | 57 ++++++++++++------- resources/js/i18n/de.js | 6 ++ resources/js/i18n/en.js | 6 ++ resources/sass/results.scss | 2 +- 9 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 database/migrations/2025_11_26_142512_result_got_seed_at.php diff --git a/app/Http/Controllers/ResultController.php b/app/Http/Controllers/ResultController.php index 6a4eb59..ec732b6 100644 --- a/app/Http/Controllers/ResultController.php +++ b/app/Http/Controllers/ResultController.php @@ -42,6 +42,17 @@ class ResultController extends Controller if (!$result->verified_at) { $result->vod = !empty($validatedData['vod']) ? $validatedData['vod'] : null; } + if (!$result->got_seed_at) { + // try to find earliest access to the seed + $entry = $round->protocols() + ->where('user_id', '=', $result->user_id) + ->where('type', '=', 'round.getseed') + ->oldest() + ->first(); + if ($entry) { + $result->got_seed_at = $entry->created_at; + } + } $result->save(); if ($result->wasChanged()) { diff --git a/app/Models/Result.php b/app/Models/Result.php index 77cb7f9..a85dbc7 100644 --- a/app/Models/Result.php +++ b/app/Models/Result.php @@ -104,8 +104,11 @@ class Result extends Model protected $casts = [ 'forfeit' => 'boolean', + 'got_seed_at' => 'datetime', 'time' => 'double', 'user_id' => 'string', + 'verified_at' => 'datetime', + 'verified_by_id' => 'string', ]; protected $appends = [ diff --git a/database/migrations/2025_11_26_142512_result_got_seed_at.php b/database/migrations/2025_11_26_142512_result_got_seed_at.php new file mode 100644 index 0000000..d326c74 --- /dev/null +++ b/database/migrations/2025_11_26_142512_result_got_seed_at.php @@ -0,0 +1,28 @@ +timestamp('got_seed_at')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('results', function (Blueprint $table) { + $table->dropColumn('got_seed_at'); + }); + } +}; diff --git a/resources/js/components/results/DetailDialog.jsx b/resources/js/components/results/DetailDialog.jsx index 17141fb..388d57d 100644 --- a/resources/js/components/results/DetailDialog.jsx +++ b/resources/js/components/results/DetailDialog.jsx @@ -1,3 +1,4 @@ +import moment from 'moment'; import PropTypes from 'prop-types'; import React from 'react'; import { Button, Col, Form, Modal, Row } from 'react-bootstrap'; @@ -5,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import Verification from './Verification'; import Box from '../users/Box'; -import { getTime } from '../../helpers/Result'; +import { formatTime, getTime } from '../../helpers/Result'; import { formatNumberAlways } from '../../helpers/Round'; import { maySeeResult, mayVerifyResult } from '../../helpers/permissions'; import { findResult } from '../../helpers/User'; @@ -92,6 +93,32 @@ const DetailDialog = ({ {result?.verified_at || (mayVerify && actions?.verifyResult) ? ( + {mayVerify ? + + + {t('results.createdAt')} +
{t('results.createdAtFormat', { date: new Date(result.created_at) })}
+
+ + {t('results.gotSeedAt')} +
+ {result.got_seed_at + ? t('results.gotSeedAtFormat', { date: new Date(result.got_seed_at) }) + : {t('general.unknown')} + } +
+
+ + {t('general.difference')} +
+ {result.got_seed_at + ? formatTime({ time: moment(result.created_at).diff(result.got_seed_at, 'seconds') }) + : '—' + } +
+
+
+ : null}
) : null} diff --git a/resources/js/helpers/Result.jsx b/resources/js/helpers/Result.jsx index 0024e9f..833368a 100644 --- a/resources/js/helpers/Result.jsx +++ b/resources/js/helpers/Result.jsx @@ -27,16 +27,18 @@ export const compareResult = (a, b) => { }; export const formatTime = result => { - const hours = `${Math.floor(result.time / 60 / 60)}`; - let minutes = `${Math.floor((result.time / 60) % 60)}`; - let seconds = `${Math.floor(result.time % 60)}`; + const time = result?.time < 0 ? result.time * -1 : result.time; + const sign = result?.time < 0 ? '-' : ''; + const hours = `${Math.floor(time / 60 / 60)}`; + let minutes = `${Math.floor((time / 60) % 60)}`; + let seconds = `${Math.floor(time % 60)}`; while (minutes.length < 2) { minutes = `0${minutes}`; } while (seconds.length < 2) { seconds = `0${seconds}`; } - return `${hours}:${minutes}:${seconds}`; + return `${sign}${hours}:${minutes}:${seconds}`; }; export const getIcon = (result, maySee) => { diff --git a/resources/js/helpers/Tournament.js b/resources/js/helpers/Tournament.js index 206b15c..605b947 100644 --- a/resources/js/helpers/Tournament.js +++ b/resources/js/helpers/Tournament.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import moment from 'moment'; import Application from './Application'; import downloadBlob from './downloadBlob'; @@ -85,12 +86,15 @@ export const exportXlsx = async (tnmt) => { views: [{ state: 'frozen', xSplit: 1, ySplit: 1 }], }); if (users.length > tournament.rounds.length) { - summary.addRow([ - i18n.t('results.runner'), - i18n.t('users.discordId'), - i18n.t('users.discordTag'), - ...tournament.rounds.map((round) => round.title || Round.formatNumberAlways(tournament, round)), - ]); + summary.columns = [ + { header: i18n.t('results.runner'), width: 15 }, + { header: i18n.t('users.discordId'), width: 20 }, + { header: i18n.t('users.discordTag'), width: 15 }, + ...tournament.rounds.map((round) => ({ + header: round.title || Round.formatNumberAlways(tournament, round), + width: 15, + })), + ]; users.forEach((user) => { summary.addRow([ User.getUserName(user), @@ -100,10 +104,13 @@ export const exportXlsx = async (tnmt) => { ]); }); } else { - summary.addRow([ - i18n.t('results.round'), - ...users.map(User.getUserName), - ]); + summary.columns = [ + { header: i18n.t('results.round'), width: 20 }, + ...users.map((user) => ({ + header: User.getUserName(user), + width: 15, + })), + ]; tournament.rounds.forEach((round) => { summary.addRow([ round.title || Round.formatNumberAlways(tournament, round), @@ -116,26 +123,32 @@ export const exportXlsx = async (tnmt) => { const worksheet = workbook.addWorksheet(getRoundWorksheetTitle(tournament, round), { views: [{ state: 'frozen', xSplit: 1, ySplit: 1 }], }); - worksheet.addRow([ - i18n.t('results.runner'), - i18n.t('results.forfeit'), - i18n.t('results.reportTime'), - i18n.t('results.placement'), - i18n.t('results.score'), - i18n.t('results.comment'), - i18n.t('results.vod'), - i18n.t('general.created_at'), - ]); + worksheet.columns = [ + { header: i18n.t('results.runner'), width: 15 }, + { header: i18n.t('results.forfeit'), width: 12 }, + { header: i18n.t('results.reportTime'), width: 10 }, + { header: i18n.t('results.placement'), width: 12 }, + { header: i18n.t('results.score'), width: 8 }, + { header: i18n.t('results.comment'), width: 20 }, + { header: i18n.t('results.vod'), width: 20 }, + { header: i18n.t('general.created_at'), width: 18 }, + { header: i18n.t('results.gotSeedAt'), width: 18 }, + { header: i18n.t('general.difference'), width: 10 }, + ]; round.results.forEach((result) => { worksheet.addRow([ User.getUserName(result.user), result.forfeit ? 'x' : '-', - result.time, + Result.formatTime(result), result.placement, result.score, result.comment || '', result.vod || '', - result.created_at, + moment(result.created_at).format('L LT'), + result.got_seed_at ? moment(result.got_seed_at).format('L LT') : i18n.t('general.unknown'), + result.got_seed_at + ? Result.formatTime({ time: moment(result.created_at).diff(result.got_seed_at, 'seconds') }) + : '—', ]); }); }); diff --git a/resources/js/i18n/de.js b/resources/js/i18n/de.js index 6439379..10797cc 100644 --- a/resources/js/i18n/de.js +++ b/resources/js/i18n/de.js @@ -359,6 +359,7 @@ export default { appName: 'ALttP', copied: 'Kopiert', created_at: 'Erstellt am', + difference: 'Differenz', exportError: 'Fehler beim Exportieren', languages: { de: 'Deutsch', @@ -375,6 +376,7 @@ export default { saveError: 'Fehler beim Speichern', saveSuccess: 'Gespeichert', summary: 'Zusammenfassung', + unknown: 'Unbekannt', upload: 'Datei hochladen', uploadError: 'Fehler beim Hochladen', uploading: 'Am Hochladen...', @@ -609,11 +611,15 @@ export default { }, results: { addComment: 'Kommentieren', + createdAt: 'Eingetragen am', + createdAtFormat: '{{ date, L LT }}', comment: 'Kommentar', details: 'Details', edit: 'Ergebnis ändern', editComment: 'Kommentar ändern', forfeit: 'Aufgegeben', + gotSeedAt: 'Seed geladen', + gotSeedAtFormat: '{{ date, L LT }}', list: 'Liste', pending: 'Ausstehend', placement: 'Platzierung', diff --git a/resources/js/i18n/en.js b/resources/js/i18n/en.js index ae5442d..0491e31 100644 --- a/resources/js/i18n/en.js +++ b/resources/js/i18n/en.js @@ -359,6 +359,7 @@ export default { appName: 'ALttP', copied: 'Copied', created_at: 'Created at', + difference: 'Difference', exportError: 'Error during export', languages: { de: 'German', @@ -375,6 +376,7 @@ export default { saveError: 'Error saving', saveSuccess: 'Saved successfully', summary: 'Summary', + unknown: 'Unknown', upload: 'Upload file', uploadError: 'Error uploading', uploading: 'Uploading...', @@ -610,10 +612,14 @@ export default { results: { addComment: 'Comment', comment: 'Comment', + createdAt: 'Entry from', + createdAtFormat: '{{ date, L LT }}', details: 'Details', edit: 'Change result', editComment: 'Edit comment', forfeit: 'Forfeit', + gotSeedAt: 'Got seed at', + gotSeedAtFormat: '{{ date, L LT }}', list: 'List', pending: 'Pending', placement: 'Placement', diff --git a/resources/sass/results.scss b/resources/sass/results.scss index ea46587..00659a9 100644 --- a/resources/sass/results.scss +++ b/resources/sass/results.scss @@ -46,7 +46,7 @@ .result-time, .result-vod { text-align: right; - width: 15ex; + width: 18ex; } } -- 2.47.3