public function create(Request $request) {
$validatedData = $request->validate([
+ 'forfeit' => 'boolean',
'participant_id' => 'required|exists:App\\Models\\Participant,id',
'round_id' => 'required|exists:App\\Models\\Round,id',
- 'time' => 'required|numeric',
+ 'time' => 'required_if:forfeit,false|numeric',
]);
+ error_log(var_export($validatedData, true));
$participant = Participant::findOrFail($validatedData['participant_id']);
$round = Round::findOrFail($validatedData['round_id']);
'round_id' => $validatedData['round_id'],
'user_id' => $participant->user_id,
], [
- 'time' => $validatedData['time'],
+ 'forfeit' => $validatedData['forfeit'],
+ 'time' => isset($validatedData['time']) ? $validatedData['time'] : 0,
]);
Protocol::resultReported(
protected static function resultMemo(Result $result) {
return [
'id' => $result->id,
+ 'forfeit' => $result->forfeit,
'time' => $result->time,
];
}
}
public function getHasFinishedAttribute() {
- return $this->time > 0;
+ return $this->time > 0 || $this->forfeit;
}
protected $appends = [
];
protected $fillable = [
+ 'forfeit',
'round_id',
'time',
'user_id',
--- /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.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table('results', function(Blueprint $table) {
+ $table->boolean('forfeit')->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('results', function(Blueprint $table) {
+ $table->dropColumn('forfeit');
+ });
+ }
+};
},
"eslintConfig": {
"env": {
- "browser": true
+ "browser": true,
+ "node": true
},
"extends": [
"eslint:recommended",
"sourceType": "module"
},
"rules": {
+ "import/no-named-as-default-member": 0,
"max-len": [
"warn",
{
try {
require('bootstrap');
-} catch (e) {}
+} catch (e) {
+ // well...
+}
/**
* We'll load the axios HTTP library which allows us to easily issue requests
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faJsSquare } from '@fortawesome/free-brands-svg-icons';
import { fab } from '@fortawesome/free-brands-svg-icons';
import { fas } from '@fortawesome/free-solid-svg-icons';
import React from 'react';
import i18n from '../../i18n';
-library.add(faJsSquare);
library.add(fab);
library.add(fas);
Icon.EDIT = makePreset('EditIcon', 'edit');
Icon.FINISHED = makePreset('FinishedIcon', 'square-check');
Icon.FIRST_PLACE = makePreset('FirstPlaceIcon', 'trophy');
+Icon.FORFEIT = makePreset('ForfeitIcon', 'square-xmark');
Icon.LOGOUT = makePreset('LogoutIcon', 'sign-out-alt');
Icon.PENDING = makePreset('PendingIcon', 'clock');
Icon.PROTOCOL = makePreset('ProtocolIcon', 'file-alt');
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import Icon from './Icon';
+
+const LargeCheck = ({
+ className,
+ id,
+ name,
+ onBlur,
+ onChange,
+ value,
+}) => {
+ let clsn = className ? `${className} custom-check` : 'custom-check';
+ if (value) {
+ clsn += ' checked';
+ }
+ return <span
+ className={clsn}
+ id={id}
+ onBlur={onBlur ? () => onBlur({ target: { name, value } }) : null}
+ onClick={onChange ? () => onChange({ target: { name, value: !value } }) : null}
+ onKeyPress={onChange ? e => {
+ if (e.key == 'Enter' || e.key == ' ') {
+ e.preventDefault();
+ e.stopPropagation();
+ onChange({ target: { name, value: !value } });
+ }
+ } : null}
+ tabIndex="0"
+ >
+ <Icon name="check" />
+ </span>;
+};
+
+LargeCheck.propTypes = {
+ className: PropTypes.string,
+ id: PropTypes.string,
+ name: PropTypes.string,
+ onBlur: PropTypes.func,
+ onChange: PropTypes.func,
+ value: PropTypes.bool,
+};
+
+LargeCheck.defaultProps = {
+ className: '',
+ id: '',
+ name: '',
+ onBlur: null,
+ onChange: null,
+ value: false,
+};
+
+export default LargeCheck;
if (!result || !result.has_finished) {
return <Icon.PENDING className="text-muted" size="lg" />;
}
+ if (result.forfeit) {
+ return <Icon.FORFEIT className="text-danger" size="lg" />;
+ }
if (index === 0) {
return <Icon.FIRST_PLACE className="text-gold" size="lg" />;
}
import React from 'react';
import { Button, Col, Form, Modal, Row } from 'react-bootstrap';
import { withTranslation } from 'react-i18next';
+import toastr from 'toastr';
+import LargeCheck from '../common/LargeCheck';
import i18n from '../../i18n';
+import laravelErrorsToFormik from '../../helpers/laravelErrorsToFormik';
+import { findResult } from '../../helpers/Participant';
import { formatTime, parseTime } from '../../helpers/Result';
import yup from '../../schema/yup';
<Form noValidate onSubmit={handleSubmit}>
<Modal.Body>
<Row>
- <Form.Group as={Col} controlId="report.time">
+ <Form.Group as={Col} sm={9} controlId="report.time">
<Form.Label>{i18n.t('results.reportTime')}</Form.Label>
<Form.Control
isInvalid={!!(touched.time && errors.time)}
name="time"
onBlur={handleBlur}
onChange={handleChange}
- placeholder="1:22:59"
+ placeholder={values.forfeit ? 'DNF' : '1:22:59'}
type="text"
value={values.time || ''}
/>
</Form.Text>
}
</Form.Group>
+ <Form.Group as={Col} sm={3} controlId="report.forfeit">
+ <Form.Label>{i18n.t('results.forfeit')}</Form.Label>
+ <Form.Control
+ as={LargeCheck}
+ isInvalid={!!(touched.forfeit && errors.forfeit)}
+ name="forfeit"
+ onBlur={handleBlur}
+ onChange={handleChange}
+ value={!!values.forfeit}
+ />
+ </Form.Group>
</Row>
</Modal.Body>
<Modal.Footer>
ReportForm.propTypes = {
errors: PropTypes.shape({
+ forfeit: PropTypes.string,
time: PropTypes.string,
}),
handleBlur: PropTypes.func,
handleSubmit: PropTypes.func,
onCancel: PropTypes.func,
touched: PropTypes.shape({
+ forfeit: PropTypes.bool,
time: PropTypes.bool,
}),
values: PropTypes.shape({
+ forfeit: PropTypes.bool,
time: PropTypes.string,
}),
};
displayName: 'ReportForm',
enableReinitialize: true,
handleSubmit: async (values, actions) => {
- const { participant_id, round_id, time } = values;
+ const { forfeit, participant_id, round_id, time } = values;
+ const { setErrors } = actions;
const { onCancel } = actions.props;
- await axios.post('/api/results', {
- participant_id,
- round_id,
- time: parseTime(time),
- });
- if (onCancel) {
- onCancel();
+ try {
+ await axios.post('/api/results', {
+ forfeit,
+ participant_id,
+ round_id,
+ time: parseTime(time) || 0,
+ });
+ toastr.success(i18n.t('results.reportSuccess'));
+ if (onCancel) {
+ onCancel();
+ }
+ } catch (e) {
+ toastr.success(i18n.t('results.reportError'));
+ if (e.response && e.response.data && e.response.data.errors) {
+ setErrors(laravelErrorsToFormik(e.response.data.errors));
+ }
}
},
- mapPropsToValues: ({ participant, round }) => ({
- participant_id: participant.id,
- round_id: round.id,
- time: '',
- }),
+ mapPropsToValues: ({ participant, round }) => {
+ const result = findResult(participant, round);
+ console.log(result);
+ return {
+ forfeit: result ? !!result.forfeit : false,
+ participant_id: participant.id,
+ round_id: round.id,
+ time: result && result.time ? formatTime(result) : '',
+ };
+ },
validationSchema: yup.object().shape({
- time: yup.string().required().time(),
+ forfeit: yup.boolean().required(),
+ time: yup.string().time().when('forfeit', {
+ is: false,
+ then: yup.string().required().time(),
+ }),
}),
})(withTranslation()(ReportForm));
export const compareResult = round => (a, b) => {
const a_result = findResult(a, round);
const b_result = findResult(b, round);
- const a_time = a_result ? a_result.time : 0;
- const b_time = b_result ? b_result.time : 0;
+ const a_time = a_result && !a_result.forfeit ? a_result.time : 0;
+ const b_time = b_result && !b_result.forfeit ? b_result.time : 0;
if (a_time) {
if (b_time) {
if (a_time < b_time) return -1;
if (b_time) {
return 1;
}
+ const a_forfeit = a_result && a_result.forfeit;
+ const b_forfeit = b_result && b_result.forfeit;
+ if (a_forfeit) {
+ if (b_forfeit) {
+ return 0;
+ }
+ return -1;
+ }
+ if (b_forfeit) {
+ return 1;
+ }
return 0;
};
export const parseTime = str => {
if (!str) return null;
- return `${str}`.split(/[-\.: ]+/).reduce((acc,time) => (60 * acc) + +time, 0);
+ return `${str}`.split(/[-.: ]+/).reduce((acc,time) => (60 * acc) + +time, 0);
};
export default {
--- /dev/null
+const laravelErrorsToFormik = errors =>
+ Object.keys(errors || {}).reduce((result, key) => ({
+ ...result,
+ [key]: errors[key].join(', '),
+ }), {});
+
+export default laravelErrorsToFormik;
},
results: {
edit: 'Ergebnis ändern',
+ forfeit: 'Aufgegeben',
report: 'Ergebnis eintragen',
- reportTime: 'Zeit',
+ reportError: 'Fehler beim Eintragen :(',
reportPreview: 'Wird als {{ time }} festgehalten',
+ reportSuccess: 'Ergebnis festgehalten',
+ reportTime: 'Zeit',
time: 'Zeit: {{ time }}',
},
rounds: {
return this.test('test-time-format', errorMessage, function (value) {
const { path, createError } = this;
return (
+ !value ||
parseTime(value) ||
createError({ path, message: errorMessage || 'validation.error.time' })
);
// Custom
@import 'common';
+@import 'form';
@import 'participants';
@import 'results';
@import 'rounds';
--- /dev/null
+.custom-check {
+ display: table;
+ width: auto;
+ > * {
+ visibility: hidden;
+ }
+ &.checked > * {
+ visibility: visible;
+ }
+}