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()) {
protected $casts = [
'forfeit' => 'boolean',
+ 'got_seed_at' => 'datetime',
'time' => 'double',
'user_id' => 'string',
+ 'verified_at' => 'datetime',
+ 'verified_by_id' => 'string',
];
protected $appends = [
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ */
+ public function up(): void
+ {
+ Schema::table('results', function (Blueprint $table) {
+ $table->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');
+ });
+ }
+};
+import moment from 'moment';
import PropTypes from 'prop-types';
import React from 'react';
import { Button, Col, Form, Modal, Row } from 'react-bootstrap';
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';
</Modal.Body>
{result?.verified_at || (mayVerify && actions?.verifyResult) ? (
<Modal.Body className="verification">
+ {mayVerify ?
+ <Row className="mb-3">
+ <Form.Group as={Col} sm={4}>
+ <Form.Label>{t('results.createdAt')}</Form.Label>
+ <div>{t('results.createdAtFormat', { date: new Date(result.created_at) })}</div>
+ </Form.Group>
+ <Form.Group as={Col} sm={4}>
+ <Form.Label>{t('results.gotSeedAt')}</Form.Label>
+ <div>
+ {result.got_seed_at
+ ? t('results.gotSeedAtFormat', { date: new Date(result.got_seed_at) })
+ : <em>{t('general.unknown')}</em>
+ }
+ </div>
+ </Form.Group>
+ <Form.Group as={Col} sm={4}>
+ <Form.Label>{t('general.difference')}</Form.Label>
+ <div>
+ {result.got_seed_at
+ ? formatTime({ time: moment(result.created_at).diff(result.got_seed_at, 'seconds') })
+ : '—'
+ }
+ </div>
+ </Form.Group>
+ </Row>
+ : null}
<Verification actions={actions} result={result} round={round} tournament={tournament} />
</Modal.Body>
) : null}
};
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) => {
import axios from 'axios';
+import moment from 'moment';
import Application from './Application';
import downloadBlob from './downloadBlob';
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),
]);
});
} 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),
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') })
+ : '—',
]);
});
});
appName: 'ALttP',
copied: 'Kopiert',
created_at: 'Erstellt am',
+ difference: 'Differenz',
exportError: 'Fehler beim Exportieren',
languages: {
de: 'Deutsch',
saveError: 'Fehler beim Speichern',
saveSuccess: 'Gespeichert',
summary: 'Zusammenfassung',
+ unknown: 'Unbekannt',
upload: 'Datei hochladen',
uploadError: 'Fehler beim Hochladen',
uploading: 'Am Hochladen...',
},
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',
appName: 'ALttP',
copied: 'Copied',
created_at: 'Created at',
+ difference: 'Difference',
exportError: 'Error during export',
languages: {
de: 'German',
saveError: 'Error saving',
saveSuccess: 'Saved successfully',
summary: 'Summary',
+ unknown: 'Unknown',
upload: 'Upload file',
uploadError: 'Error uploading',
uploading: 'Uploading...',
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',
.result-time,
.result-vod {
text-align: right;
- width: 15ex;
+ width: 18ex;
}
}