Erlaube das Hinzufügen von Teilnehmern ohne Klasse und normalisiere die Anzahl der Gruppen auf mindestens 1 in der Turnierverwaltung
This commit is contained in:
@@ -36,23 +36,36 @@ export const addTournament = async (req, res) => {
|
||||
// 3. Teilnehmer hinzufügen - klassengebunden
|
||||
export const addParticipant = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, classId, participant: participantId } = req.body;
|
||||
const { clubId, classId, participant: participantId, tournamentId } = req.body;
|
||||
try {
|
||||
// Payloads:
|
||||
// - Mit Klasse (klassengebunden): { clubId, classId, participant }
|
||||
// - Ohne Klasse (turnierweit): { clubId, tournamentId, participant, classId: null }
|
||||
if (!participantId) {
|
||||
return res.status(400).json({ error: 'Teilnehmer-ID ist erforderlich' });
|
||||
}
|
||||
if (!classId) {
|
||||
return res.status(400).json({ error: 'Klasse ist erforderlich' });
|
||||
// Allow adding a participant either to a specific class (classId) or to the whole tournament (no class)
|
||||
if (!classId && !tournamentId) {
|
||||
return res.status(400).json({ error: 'Klasse oder tournamentId ist erforderlich' });
|
||||
}
|
||||
await tournamentService.addParticipant(token, clubId, classId, participantId);
|
||||
// Hole tournamentId über die Klasse
|
||||
const tournamentClass = await TournamentClass.findByPk(classId);
|
||||
if (!tournamentClass) {
|
||||
return res.status(404).json({ error: 'Klasse nicht gefunden' });
|
||||
|
||||
// Pass through to service. If classId is present it will be used, otherwise the service should add the participant with classId = null for the given tournamentId
|
||||
await tournamentService.addParticipant(token, clubId, classId || null, participantId, tournamentId || null);
|
||||
|
||||
// Determine tournamentId for response and event emission
|
||||
let respTournamentId = tournamentId;
|
||||
if (classId && !respTournamentId) {
|
||||
const tournamentClass = await TournamentClass.findByPk(classId);
|
||||
if (!tournamentClass) {
|
||||
return res.status(404).json({ error: 'Klasse nicht gefunden' });
|
||||
}
|
||||
respTournamentId = tournamentClass.tournamentId;
|
||||
}
|
||||
const participants = await tournamentService.getParticipants(token, clubId, tournamentClass.tournamentId, classId);
|
||||
|
||||
// Fetch updated participants for the (optional) class or whole tournament
|
||||
const participants = await tournamentService.getParticipants(token, clubId, respTournamentId, classId || null);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentClass.tournamentId);
|
||||
if (respTournamentId) emitTournamentChanged(clubId, respTournamentId);
|
||||
res.status(200).json(participants);
|
||||
} catch (error) {
|
||||
console.error('[addParticipant] Error:', error);
|
||||
@@ -93,7 +106,29 @@ export const createGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, numberOfGroups } = req.body;
|
||||
try {
|
||||
await tournamentService.createGroups(token, clubId, tournamentId, numberOfGroups);
|
||||
// DEBUG: Eingehende Daten sichtbar machen (temporär)
|
||||
console.log('[tournamentController.createGroups] body:', req.body);
|
||||
console.log('[tournamentController.createGroups] types:', {
|
||||
clubId: typeof clubId,
|
||||
tournamentId: typeof tournamentId,
|
||||
numberOfGroups: typeof numberOfGroups,
|
||||
});
|
||||
|
||||
// Turniere ohne Klassen: `numberOfGroups: 0` kommt aus der UI (Default) vor.
|
||||
// Statt „nichts passiert“ normalisieren wir auf mindestens 1 Gruppe.
|
||||
let normalizedNumberOfGroups = numberOfGroups;
|
||||
if (normalizedNumberOfGroups !== undefined && normalizedNumberOfGroups !== null) {
|
||||
const n = Number(normalizedNumberOfGroups);
|
||||
console.log('[tournamentController.createGroups] parsed numberOfGroups:', n);
|
||||
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
|
||||
return res.status(400).json({ error: 'numberOfGroups muss eine ganze Zahl >= 0 sein' });
|
||||
}
|
||||
normalizedNumberOfGroups = Math.max(1, n);
|
||||
}
|
||||
|
||||
console.log('[tournamentController.createGroups] normalizedNumberOfGroups:', normalizedNumberOfGroups);
|
||||
|
||||
await tournamentService.createGroups(token, clubId, tournamentId, normalizedNumberOfGroups);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
|
||||
@@ -85,74 +85,113 @@ class TournamentService {
|
||||
}
|
||||
|
||||
// 3. Teilnehmer hinzufügen (kein Duplikat) - klassengebunden
|
||||
async addParticipant(userToken, clubId, classId, participantId) {
|
||||
async addParticipant(userToken, clubId, classId, participantId, tournamentId = null) {
|
||||
await checkAccess(userToken, clubId);
|
||||
if (!classId) {
|
||||
throw new Error('Klasse ist erforderlich');
|
||||
|
||||
// Normalize case: the third parameter may be either a classId or a tournamentId
|
||||
if (classId) {
|
||||
const maybeClass = await TournamentClass.findByPk(classId);
|
||||
if (!maybeClass) {
|
||||
const maybeTournament = await Tournament.findByPk(classId);
|
||||
if (maybeTournament) {
|
||||
// caller passed tournamentId in this slot
|
||||
tournamentId = classId;
|
||||
classId = null;
|
||||
} else {
|
||||
throw new Error('Klasse nicht gefunden');
|
||||
}
|
||||
}
|
||||
}
|
||||
const tournamentClass = await TournamentClass.findByPk(classId);
|
||||
if (!tournamentClass) {
|
||||
throw new Error('Klasse nicht gefunden');
|
||||
|
||||
// If classId is provided (and resolved to a valid class), do class-specific validations and add
|
||||
if (classId) {
|
||||
const tournamentClass = await TournamentClass.findByPk(classId);
|
||||
if (!tournamentClass) throw new Error('Klasse nicht gefunden');
|
||||
const tournament = await Tournament.findByPk(tournamentClass.tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
|
||||
// Prüfe Geschlecht: Lade Mitglied
|
||||
const member = await Member.findByPk(participantId);
|
||||
if (!member) {
|
||||
throw new Error('Mitglied nicht gefunden');
|
||||
}
|
||||
const memberGender = member.gender || 'unknown';
|
||||
|
||||
// Validierung: Geschlecht muss zur Klasse passen
|
||||
if (tournamentClass.gender) {
|
||||
if (tournamentClass.gender === 'male' && memberGender !== 'male') {
|
||||
throw new Error('Dieser Teilnehmer kann nicht in einer männlichen Klasse spielen');
|
||||
}
|
||||
if (tournamentClass.gender === 'female' && memberGender !== 'female') {
|
||||
throw new Error('Dieser Teilnehmer kann nicht in einer weiblichen Klasse spielen');
|
||||
}
|
||||
if (tournamentClass.gender === 'mixed' && memberGender === 'unknown') {
|
||||
throw new Error('Teilnehmer mit unbekanntem Geschlecht können nicht in einer Mixed-Klasse spielen');
|
||||
}
|
||||
}
|
||||
|
||||
// Validierung: Geburtsjahr muss zur Klasse passen (geboren im Jahr X oder später, also >=)
|
||||
if (tournamentClass.minBirthYear && member.birthDate) {
|
||||
// Parse das Geburtsdatum (Format: YYYY-MM-DD oder DD.MM.YYYY)
|
||||
let birthYear = null;
|
||||
if (member.birthDate.includes('-')) {
|
||||
birthYear = parseInt(member.birthDate.split('-')[0]);
|
||||
} else if (member.birthDate.includes('.')) {
|
||||
const parts = member.birthDate.split('.');
|
||||
if (parts.length === 3) {
|
||||
birthYear = parseInt(parts[2]);
|
||||
}
|
||||
}
|
||||
if (birthYear && !isNaN(birthYear)) {
|
||||
if (birthYear < tournamentClass.minBirthYear) {
|
||||
throw new Error(`Dieser Teilnehmer ist zu alt für diese Klasse. Erlaubt: geboren ${tournamentClass.minBirthYear} oder später`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe, ob Teilnehmer bereits in dieser Klasse ist
|
||||
const exists = await TournamentMember.findOne({ where: { classId, clubMemberId: participantId } });
|
||||
if (exists) {
|
||||
throw new Error('Teilnehmer bereits hinzugefügt');
|
||||
}
|
||||
await TournamentMember.create({
|
||||
tournamentId: tournamentClass.tournamentId,
|
||||
classId,
|
||||
clubMemberId: participantId,
|
||||
groupId: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
const tournament = await Tournament.findByPk(tournamentClass.tournamentId);
|
||||
|
||||
// If classId is not provided, allow adding participant to the tournament without class
|
||||
// tournamentId must be provided in this case
|
||||
if (!tournamentId) {
|
||||
throw new Error('Klasse oder Turnier-ID ist erforderlich');
|
||||
}
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
|
||||
// Prüfe Geschlecht: Lade Mitglied
|
||||
|
||||
// Verify member exists
|
||||
const member = await Member.findByPk(participantId);
|
||||
if (!member) {
|
||||
throw new Error('Mitglied nicht gefunden');
|
||||
}
|
||||
const memberGender = member.gender || 'unknown';
|
||||
|
||||
// Validierung: Geschlecht muss zur Klasse passen
|
||||
if (tournamentClass.gender) {
|
||||
if (tournamentClass.gender === 'male' && memberGender !== 'male') {
|
||||
throw new Error('Dieser Teilnehmer kann nicht in einer männlichen Klasse spielen');
|
||||
}
|
||||
if (tournamentClass.gender === 'female' && memberGender !== 'female') {
|
||||
throw new Error('Dieser Teilnehmer kann nicht in einer weiblichen Klasse spielen');
|
||||
}
|
||||
if (tournamentClass.gender === 'mixed' && memberGender === 'unknown') {
|
||||
throw new Error('Teilnehmer mit unbekanntem Geschlecht können nicht in einer Mixed-Klasse spielen');
|
||||
}
|
||||
// mixed erlaubt alle Geschlechter (male, female, diverse)
|
||||
}
|
||||
|
||||
// Validierung: Geburtsjahr muss zur Klasse passen (geboren im Jahr X oder später, also >=)
|
||||
if (tournamentClass.minBirthYear && member.birthDate) {
|
||||
// Parse das Geburtsdatum (Format: YYYY-MM-DD oder DD.MM.YYYY)
|
||||
let birthYear = null;
|
||||
if (member.birthDate.includes('-')) {
|
||||
// Format: YYYY-MM-DD
|
||||
birthYear = parseInt(member.birthDate.split('-')[0]);
|
||||
} else if (member.birthDate.includes('.')) {
|
||||
// Format: DD.MM.YYYY
|
||||
const parts = member.birthDate.split('.');
|
||||
if (parts.length === 3) {
|
||||
birthYear = parseInt(parts[2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (birthYear && !isNaN(birthYear)) {
|
||||
// Geboren im Jahr X oder später bedeutet: birthYear >= minBirthYear
|
||||
if (birthYear < tournamentClass.minBirthYear) {
|
||||
throw new Error(`Dieser Teilnehmer ist zu alt für diese Klasse. Erlaubt: geboren ${tournamentClass.minBirthYear} oder später`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe, ob Teilnehmer bereits in dieser Klasse ist
|
||||
const exists = await TournamentMember.findOne({
|
||||
where: { classId, clubMemberId: participantId }
|
||||
|
||||
// Prüfe, ob Teilnehmer bereits im Turnier (ohne Klasse) ist
|
||||
const existsNoClass = await TournamentMember.findOne({
|
||||
where: { tournamentId, classId: null, clubMemberId: participantId }
|
||||
});
|
||||
if (exists) {
|
||||
throw new Error('Teilnehmer bereits in dieser Klasse hinzugefügt');
|
||||
if (existsNoClass) {
|
||||
throw new Error('Teilnehmer bereits hinzugefügt');
|
||||
}
|
||||
|
||||
await TournamentMember.create({
|
||||
tournamentId: tournamentClass.tournamentId,
|
||||
classId,
|
||||
tournamentId,
|
||||
classId: null,
|
||||
clubMemberId: participantId,
|
||||
groupId: null
|
||||
});
|
||||
@@ -197,22 +236,54 @@ class TournamentService {
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
const desired = numberOfGroups !== null ? numberOfGroups : tournament.numberOfGroups;
|
||||
// Semantik:
|
||||
// - null/undefined: nimm den im Turnier gespeicherten Wert
|
||||
// - 0: UI-Default -> normalisiere auf mindestens 1
|
||||
// - <0 oder nicht-integer: Fehler
|
||||
let desired = tournament.numberOfGroups;
|
||||
if (numberOfGroups !== null && numberOfGroups !== undefined) {
|
||||
const n = Number(numberOfGroups);
|
||||
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
|
||||
throw new Error('numberOfGroups muss eine ganze Zahl >= 0 sein');
|
||||
}
|
||||
desired = Math.max(1, n);
|
||||
}
|
||||
|
||||
// DEBUG: Gruppen-Planung sichtbar machen (temporär)
|
||||
console.log('[tournamentService.createGroups] input:', {
|
||||
tournamentId,
|
||||
clubId,
|
||||
numberOfGroups,
|
||||
tournamentStoredNumberOfGroups: tournament.numberOfGroups,
|
||||
desired,
|
||||
});
|
||||
const existing = await TournamentGroup.findAll({
|
||||
where: {
|
||||
tournamentId,
|
||||
classId: null // Nur Gruppen ohne Klasse
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[tournamentService.createGroups] existing groups (classId=null):', existing.map(g => g.id));
|
||||
|
||||
// zu viele Gruppen löschen
|
||||
if (existing.length > desired) {
|
||||
const toRemove = existing.slice(desired);
|
||||
console.log('[tournamentService.createGroups] removing groups:', toRemove.map(g => g.id));
|
||||
await Promise.all(toRemove.map(g => g.destroy()));
|
||||
}
|
||||
// fehlende Gruppen anlegen
|
||||
let createdCount = 0;
|
||||
for (let i = existing.length; i < desired; i++) {
|
||||
await TournamentGroup.create({ tournamentId, classId: null });
|
||||
createdCount++;
|
||||
}
|
||||
|
||||
const finalGroups = await TournamentGroup.findAll({
|
||||
where: { tournamentId, classId: null },
|
||||
order: [['id', 'ASC']],
|
||||
});
|
||||
console.log('[tournamentService.createGroups] createdCount:', createdCount, 'finalCount:', finalGroups.length, 'finalIds:', finalGroups.map(g => g.id));
|
||||
}
|
||||
|
||||
async createGroupsPerClass(userToken, clubId, tournamentId, groupsPerClass) {
|
||||
@@ -433,7 +504,9 @@ class TournamentService {
|
||||
}).length
|
||||
);
|
||||
|
||||
for (let round = 0; round < 10; round++) {
|
||||
// Wichtig: Gruppengrößen dürfen dabei NICHT aus dem Gleichgewicht geraten.
|
||||
// Deshalb nur 1:1 Swaps (seeded <-> non-seeded) zwischen Gruppen.
|
||||
for (let round = 0; round < 20; round++) {
|
||||
let changed = false;
|
||||
let maxSeededIdx = 0;
|
||||
let maxSeededCount = seededCounts[0];
|
||||
@@ -459,15 +532,30 @@ class TournamentService {
|
||||
const memberData = memberDataMap.get(item.memberId);
|
||||
return memberData && memberData.seeded;
|
||||
});
|
||||
if (seededInMax.length > 0) {
|
||||
const itemToMove = seededInMax[0];
|
||||
groupAssignments[maxSeededIdx] = groupAssignments[maxSeededIdx].filter(item =>
|
||||
!(item.type === 'member' && item.memberId === itemToMove.memberId)
|
||||
const nonSeededInMin = groupAssignments[minSeededIdx].filter(item => {
|
||||
if (item.type !== 'member') return false;
|
||||
const memberData = memberDataMap.get(item.memberId);
|
||||
return memberData && !memberData.seeded;
|
||||
});
|
||||
|
||||
if (seededInMax.length > 0 && nonSeededInMin.length > 0) {
|
||||
const seededToSwap = seededInMax[0];
|
||||
const nonSeededToSwap = nonSeededInMin[0];
|
||||
|
||||
// Swap: seeded wandert in Min, non-seeded wandert in Max
|
||||
groupAssignments[maxSeededIdx] = groupAssignments[maxSeededIdx].map(item =>
|
||||
(item.type === 'member' && item.memberId === seededToSwap.memberId) ? nonSeededToSwap : item
|
||||
);
|
||||
seededCounts[maxSeededIdx]--;
|
||||
groupAssignments[minSeededIdx].push(itemToMove);
|
||||
seededCounts[minSeededIdx]++;
|
||||
groupAssignments[minSeededIdx] = groupAssignments[minSeededIdx].map(item =>
|
||||
(item.type === 'member' && item.memberId === nonSeededToSwap.memberId) ? seededToSwap : item
|
||||
);
|
||||
|
||||
seededCounts[maxSeededIdx] -= 1;
|
||||
seededCounts[minSeededIdx] += 1;
|
||||
changed = true;
|
||||
} else {
|
||||
// Ohne passenden Swap-Kandidaten können wir die Seeded-Verteilung nicht weiter verbessern.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ vi.mock('../utils/userUtils.js', async () => {
|
||||
});
|
||||
|
||||
import sequelize from '../database.js';
|
||||
import { Op } from 'sequelize';
|
||||
import '../models/index.js';
|
||||
|
||||
import tournamentService from '../services/tournamentService.js';
|
||||
@@ -73,4 +74,93 @@ describe('tournamentService', () => {
|
||||
const matches = await tournamentService.getTournamentMatches('token', club.id, tournament.id);
|
||||
expect(matches.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('erlaubt Teilnehmer ohne Klasse', async () => {
|
||||
const club = await Club.create({ name: 'Tournament Club' });
|
||||
const memberA = await createMember(club.id, {
|
||||
firstName: 'Clara',
|
||||
lastName: 'C',
|
||||
email: 'clara@example.com',
|
||||
gender: 'female',
|
||||
});
|
||||
|
||||
const tournament = await tournamentService.addTournament('token', club.id, 'Sommercup', '2025-06-01');
|
||||
|
||||
// ohne Klasse: legacy-Aufruf (3. Argument = tournamentId)
|
||||
await tournamentService.addParticipant('token', club.id, tournament.id, memberA.id);
|
||||
await expect(
|
||||
tournamentService.addParticipant('token', club.id, tournament.id, memberA.id)
|
||||
).rejects.toThrow('Teilnehmer bereits hinzugefügt');
|
||||
|
||||
const participantsNoClass = await tournamentService.getParticipants('token', club.id, tournament.id, null);
|
||||
expect(participantsNoClass).toHaveLength(1);
|
||||
expect(participantsNoClass[0].classId).toBe(null);
|
||||
expect(participantsNoClass[0].clubMemberId).toBe(memberA.id);
|
||||
});
|
||||
|
||||
it('normalisiert numberOfGroups=0 auf mindestens 1 Gruppe', async () => {
|
||||
const club = await Club.create({ name: 'Tournament Club' });
|
||||
const tournament = await tournamentService.addTournament('token', club.id, 'Gruppen-Test', '2025-07-01');
|
||||
|
||||
await tournamentService.createGroups('token', club.id, tournament.id, 0);
|
||||
const groups = await TournamentGroup.findAll({ where: { tournamentId: tournament.id } });
|
||||
expect(groups).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('legt bei numberOfGroups=4 genau 4 Gruppen an (ohne Klassen)', async () => {
|
||||
const club = await Club.create({ name: 'Tournament Club' });
|
||||
const tournament = await tournamentService.addTournament('token', club.id, 'Gruppen-4er', '2025-08-01');
|
||||
|
||||
await tournamentService.createGroups('token', club.id, tournament.id, 4);
|
||||
const groups = await TournamentGroup.findAll({ where: { tournamentId: tournament.id } });
|
||||
expect(groups).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('verteilt bei "zufällig verteilen" möglichst gleichmäßig (Differenz <= 1)', async () => {
|
||||
const club = await Club.create({ name: 'Tournament Club' });
|
||||
const tournament = await tournamentService.addTournament('token', club.id, 'Fill-Groups-Balanced', '2025-10-01');
|
||||
|
||||
// 10 Teilnehmer, 4 Gruppen => erwartete Größen: 3/3/2/2 (beliebige Reihenfolge)
|
||||
const members = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// createMember Factory braucht eindeutige Emails
|
||||
members.push(
|
||||
await createMember(club.id, {
|
||||
firstName: `P${i}`,
|
||||
lastName: 'T',
|
||||
email: `p${i}@example.com`,
|
||||
gender: i % 2 === 0 ? 'male' : 'female',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (const m of members) {
|
||||
await tournamentService.addParticipant('token', club.id, tournament.id, m.id);
|
||||
}
|
||||
|
||||
// Seeded-Balancing triggern: markiere mehrere als gesetzt
|
||||
// (wir testen hier explizit, dass diese Optimierung die Größen-Balance NICHT kaputt machen darf)
|
||||
const tmRows = await TournamentMember.findAll({ where: { tournamentId: tournament.id } });
|
||||
const seededIds = tmRows.slice(0, 5).map(r => r.id);
|
||||
await TournamentMember.update({ seeded: true }, { where: { id: { [Op.in]: seededIds } } });
|
||||
|
||||
await tournamentService.setModus('token', club.id, tournament.id, 'groups', 4, 1);
|
||||
await tournamentService.createGroups('token', club.id, tournament.id, 4);
|
||||
await tournamentService.fillGroups('token', club.id, tournament.id);
|
||||
|
||||
const groups = await TournamentGroup.findAll({ where: { tournamentId: tournament.id } });
|
||||
expect(groups).toHaveLength(4);
|
||||
|
||||
const membersWithGroups = await TournamentMember.findAll({ where: { tournamentId: tournament.id } });
|
||||
const countsByGroupId = membersWithGroups.reduce((m, tm) => {
|
||||
m[tm.groupId] = (m[tm.groupId] || 0) + 1;
|
||||
return m;
|
||||
}, {});
|
||||
|
||||
const sizes = groups.map(g => countsByGroupId[g.id] || 0);
|
||||
const min = Math.min(...sizes);
|
||||
const max = Math.max(...sizes);
|
||||
expect(max - min).toBeLessThanOrEqual(1);
|
||||
expect(sizes.reduce((a, b) => a + b, 0)).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,7 +44,13 @@
|
||||
<div v-else class="groups-per-class">
|
||||
<label>
|
||||
{{ $t('tournaments.numberOfGroups') }}:
|
||||
<input type="number" :value="numberOfGroups" @input="$emit('update:numberOfGroups', parseInt($event.target.value))" min="1" @change="$emit('group-count-change')" />
|
||||
<input
|
||||
type="number"
|
||||
:value="numberOfGroups"
|
||||
min="1"
|
||||
@input="$emit('update:numberOfGroups', Math.max(1, parseInt($event.target.value || '1', 10) || 1))"
|
||||
@change="$emit('group-count-change')"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1045,7 +1045,11 @@ export default {
|
||||
this.currentTournamentDate = tournament.date || '';
|
||||
this.currentWinningSets = tournament.winningSets || 3;
|
||||
this.isGroupTournament = tournament.type === 'groups';
|
||||
this.numberOfGroups = tournament.numberOfGroups;
|
||||
// Defensive: Backend/DB kann (historisch/UI-default) 0/null liefern.
|
||||
// Für gruppenbasierte Turniere ohne Klassen brauchen wir hier aber eine sinnvolle Zahl,
|
||||
// sonst sendet die UI später wieder `0` an `/tournament/groups`.
|
||||
const loadedGroups = Number(tournament.numberOfGroups);
|
||||
this.numberOfGroups = Number.isFinite(loadedGroups) && loadedGroups > 0 ? loadedGroups : 1;
|
||||
this.advancingPerGroup = tournament.advancingPerGroup;
|
||||
|
||||
// Prüfe, ob es einen Trainingstag für das Turnierdatum gibt
|
||||
@@ -1429,6 +1433,9 @@ export default {
|
||||
}, {});
|
||||
const r = await apiClient.post('/tournament/participant', {
|
||||
clubId: this.currentClub,
|
||||
// Wenn ohne Klasse hinzugefügt wird, braucht das Backend die Turnier-ID.
|
||||
// (Bei klassengebundenen Teilnehmern ist sie optional, schadet aber nicht.)
|
||||
tournamentId: this.selectedDate,
|
||||
classId: classId,
|
||||
participant: this.selectedMember
|
||||
});
|
||||
@@ -1464,8 +1471,10 @@ export default {
|
||||
|
||||
async createGroups() {
|
||||
try {
|
||||
// Wenn Klassen vorhanden sind, verwende groupsPerClass
|
||||
if (this.tournamentClasses.length > 0) {
|
||||
// Wenn Klassen vorhanden sind, verwende groupsPerClass.
|
||||
// Achtung: Auch für "Ohne Klasse" (selectedViewClass='__none__') ist das der richtige Pfad,
|
||||
// sonst fällt die UI in den alten Fallback und sendet `numberOfGroups` (häufig 1).
|
||||
if (this.tournamentClasses.length > 0 || this.selectedViewClass === '__none__') {
|
||||
await apiClient.post('/tournament/groups/create', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
@@ -1473,10 +1482,11 @@ export default {
|
||||
});
|
||||
} else {
|
||||
// Fallback: Verwende numberOfGroups wie bisher
|
||||
const desired = Math.max(1, parseInt(String(this.numberOfGroups), 10) || 1);
|
||||
await apiClient.put('/tournament/groups', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
numberOfGroups: this.numberOfGroups
|
||||
numberOfGroups: desired
|
||||
});
|
||||
}
|
||||
await this.loadTournamentData();
|
||||
@@ -1666,11 +1676,12 @@ export default {
|
||||
|
||||
async onModusChange() {
|
||||
const type = this.isGroupTournament ? 'groups' : 'knockout';
|
||||
const desired = Math.max(1, parseInt(String(this.numberOfGroups), 10) || 1);
|
||||
await apiClient.post('/tournament/modus', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
type,
|
||||
numberOfGroups: this.numberOfGroups,
|
||||
numberOfGroups: desired,
|
||||
advancingPerGroup: this.advancingPerGroup
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
@@ -1725,7 +1736,8 @@ export default {
|
||||
|
||||
async onGroupCountChange() {
|
||||
// Wenn Klassen vorhanden sind, speichere groupsPerClass, sonst numberOfGroups
|
||||
if (this.tournamentClasses.length > 0) {
|
||||
// Hinweis: Bei "Ohne Klasse" wird groupsPerClass['null'] genutzt, auch wenn Klassen existieren.
|
||||
if (this.tournamentClasses.length > 0 || this.selectedViewClass === '__none__') {
|
||||
// Speichere groupsPerClass für die aktuelle Klasse
|
||||
// Die Werte sind bereits in this.groupsPerClass gespeichert durch den setter von groupsPerClassInput
|
||||
// Wir müssen nichts speichern, da groupsPerClass nur lokal verwendet wird
|
||||
@@ -1733,11 +1745,12 @@ export default {
|
||||
return;
|
||||
} else {
|
||||
// Fallback: Verwende numberOfGroups wie bisher
|
||||
const desired = Math.max(1, parseInt(String(this.numberOfGroups), 10) || 1);
|
||||
await apiClient.post('/tournament/modus', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
type: this.isGroupTournament ? 'groups' : 'knockout',
|
||||
numberOfGroups: this.numberOfGroups
|
||||
numberOfGroups: desired
|
||||
});
|
||||
await this.loadTournamentData();
|
||||
}
|
||||
@@ -2407,6 +2420,7 @@ export default {
|
||||
}
|
||||
await apiClient.post('/tournament/participant', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
classId: classId,
|
||||
participant: participant.clubMemberId
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user