feat(tournament): enhance external participant management with email and address fields

- Added email and address fields to the external participant model, allowing for more comprehensive participant information.
- Updated the tournament service and controller to handle the new fields when adding external participants.
- Modified frontend components to include input fields for email and address, improving user experience and data collection.
- Updated localization strings to support the new fields, ensuring clarity in the user interface.
This commit is contained in:
Torsten Schulz (local)
2026-02-04 11:12:37 +01:00
parent 10e6d74d93
commit 673a3afbb5
8 changed files with 119 additions and 6 deletions

View File

@@ -545,9 +545,9 @@ export const setMatchActive = async (req, res) => {
// Externe Teilnehmer hinzufügen
export const addExternalParticipant = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId, firstName, lastName, club, birthDate, gender } = req.body;
const { clubId, tournamentId, classId, firstName, lastName, club, birthDate, gender, email, address } = req.body;
try {
await tournamentService.addExternalParticipant(token, clubId, classId, firstName, lastName, club, birthDate, gender);
await tournamentService.addExternalParticipant(token, clubId, classId, firstName, lastName, club, birthDate, gender, email, address);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Externer Teilnehmer hinzugefügt' });
} catch (error) {

View File

@@ -0,0 +1,6 @@
-- E-Mail und Adresse für externe Teilnehmer (für Weitermeldung)
-- Die Felder werden verschlüsselt gespeichert (siehe Model)
ALTER TABLE `external_tournament_participant`
ADD COLUMN `email` VARCHAR(500) NULL AFTER `club`,
ADD COLUMN `address` TEXT NULL AFTER `email`;

View File

@@ -53,6 +53,40 @@ const ExternalTournamentParticipant = sequelize.define('ExternalTournamentPartic
return decryptData(encryptedValue);
}
},
email: {
type: DataTypes.STRING(500),
allowNull: true,
set(value) {
if (!value) {
this.setDataValue('email', null);
return;
}
const encryptedValue = encryptData(value);
this.setDataValue('email', encryptedValue);
},
get() {
const encryptedValue = this.getDataValue('email');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
}
},
address: {
type: DataTypes.TEXT,
allowNull: true,
set(value) {
if (!value) {
this.setDataValue('address', null);
return;
}
const encryptedValue = encryptData(value);
this.setDataValue('address', encryptedValue);
},
get() {
const encryptedValue = this.getDataValue('address');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
}
},
birthDate: {
type: DataTypes.STRING,
allowNull: true,

View File

@@ -3369,7 +3369,7 @@ Ve // 2. Neues Turnier anlegen
}
// Externe Teilnehmer hinzufügen
async addExternalParticipant(userToken, clubId, classId, firstName, lastName, club, birthDate, gender) {
async addExternalParticipant(userToken, clubId, classId, firstName, lastName, club, birthDate, gender, email = null, address = null) {
await checkAccess(userToken, clubId);
if (!classId) {
throw new Error('Klasse ist erforderlich');
@@ -3432,6 +3432,8 @@ Ve // 2. Neues Turnier anlegen
firstName,
lastName,
club: club || null,
email: email || null,
address: address || null,
birthDate: birthDate || null,
gender: participantGender,
groupId: null

View File

@@ -172,9 +172,9 @@ export default {
this.playerData = {
name: `${externalParticipant.firstName || ''} ${externalParticipant.lastName || ''}`.trim(),
birthDate: externalParticipant.birthDate || null,
address: null, // Externe Teilnehmer haben keine Adresse
address: externalParticipant.address || null,
gender: externalParticipant.gender || null,
email: null, // Externe Teilnehmer haben keine E-Mail
email: externalParticipant.email || null,
phone: null // Externe Teilnehmer haben keine Telefonnummer
};
} else {

View File

@@ -77,6 +77,22 @@
{{ $t('tournaments.add') }}
</button>
</div>
<div class="add-participant-row add-participant-row-contact">
<input
type="email"
:value="newExternalParticipant.email"
@input="$emit('update:newExternalParticipant', { ...newExternalParticipant, email: $event.target.value })"
:placeholder="$t('tournaments.email') + ' (' + $t('tournaments.optional') + ', ' + $t('tournaments.forForwarding') + ')'"
class="external-input external-input-email"
/>
<input
type="text"
:value="newExternalParticipant.address"
@input="$emit('update:newExternalParticipant', { ...newExternalParticipant, address: $event.target.value })"
:placeholder="$t('tournaments.address') + ' (' + $t('tournaments.optional') + ', ' + $t('tournaments.forForwarding') + ')'"
class="external-input external-input-address"
/>
</div>
</div>
</div>
<div class="participants-table-container">
@@ -97,6 +113,8 @@
<th class="participant-name">{{ $t('tournaments.name') }}</th>
<th v-if="allowsExternal" class="participant-gender-cell">{{ $t('members.gender') }}</th>
<th v-if="allowsExternal" class="participant-club-cell">{{ $t('tournaments.club') }}</th>
<th v-if="allowsExternal" class="participant-email-cell">{{ $t('tournaments.email') }}</th>
<th v-if="allowsExternal" class="participant-address-cell">{{ $t('tournaments.address') }}</th>
<th v-if="isGroupTournament" class="participant-group-cell">{{ $t('tournaments.group') }}</th>
<th class="participant-action-cell">{{ $t('tournaments.action') }}</th>
</tr>
@@ -146,6 +164,14 @@
{{ participant.club || '' }}
</template>
</td>
<td v-if="allowsExternal" class="participant-email-cell">
<template v-if="participant.member"></template>
<template v-else>{{ participant.email || '' }}</template>
</td>
<td v-if="allowsExternal" class="participant-address-cell">
<template v-if="participant.member"></template>
<template v-else>{{ participant.address || '' }}</template>
</td>
<td v-if="isGroupTournament" class="participant-group-cell">
<select
:value="participant.groupNumber"
@@ -178,6 +204,8 @@
<th class="participant-name">{{ $t('tournaments.name') }}</th>
<th v-if="allowsExternal" class="participant-gender-cell">{{ $t('members.gender') }}</th>
<th v-if="allowsExternal" class="participant-club-cell">{{ $t('tournaments.club') }}</th>
<th v-if="allowsExternal" class="participant-email-cell">{{ $t('tournaments.email') }}</th>
<th v-if="allowsExternal" class="participant-address-cell">{{ $t('tournaments.address') }}</th>
<th class="participant-class-cell">{{ $t('tournaments.class') }}</th>
<th v-if="isGroupTournament" class="participant-group-cell">{{ $t('tournaments.group') }}</th>
<th class="participant-action-cell">{{ $t('tournaments.action') }}</th>
@@ -228,6 +256,14 @@
{{ participant.club || '' }}
</template>
</td>
<td v-if="allowsExternal" class="participant-email-cell">
<template v-if="participant.member"></template>
<template v-else>{{ participant.email || '' }}</template>
</td>
<td v-if="allowsExternal" class="participant-address-cell">
<template v-if="participant.member"></template>
<template v-else>{{ participant.address || '' }}</template>
</td>
<td class="participant-class-cell">
<select
:value="participant.classId"
@@ -571,6 +607,32 @@ export default {
min-width: 0;
}
.add-participant-row-contact {
margin-top: 0.5rem;
}
.external-input-email {
min-width: 180px;
}
.external-input-address {
min-width: 220px;
flex: 1;
}
.participants-table-header .participant-email-cell,
.participants-table-body .participant-email-cell {
min-width: 140px;
}
.participants-table-header .participant-address-cell,
.participants-table-body .participant-address-cell {
min-width: 160px;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 1024px) {
.participants-layout {
flex-direction: column;

View File

@@ -659,6 +659,8 @@
"startKORound": "K.o.-Runde starten",
"deleteKORound": "K.o.-Runde",
"address": "Adresse",
"email": "E-Mail",
"forForwarding": "für Weitermeldung",
"showPlayerDetails": "Spielerdetails anzeigen",
"noPlayerDataAvailable": "Keine Spielerdaten verfügbar",
"koRound": "K.-o.-Runde",

View File

@@ -377,6 +377,8 @@ export default {
firstName: '',
lastName: '',
club: '',
email: '',
address: '',
birthDate: null,
gender: 'unknown'
},
@@ -2839,18 +2841,23 @@ export default {
try {
await apiClient.post('/tournament/external-participant', {
clubId: this.currentClub,
tournamentId: this.selectedDate,
classId: classId,
firstName: this.newExternalParticipant.firstName,
lastName: this.newExternalParticipant.lastName,
club: this.newExternalParticipant.club || null,
birthDate: this.newExternalParticipant.birthDate || null,
gender: this.newExternalParticipant.gender || 'unknown'
gender: this.newExternalParticipant.gender || 'unknown',
email: this.newExternalParticipant.email || null,
address: this.newExternalParticipant.address || null
});
// Leere Eingabefelder
this.newExternalParticipant = {
firstName: '',
lastName: '',
club: '',
email: '',
address: '',
birthDate: null,
gender: 'unknown'
};