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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user