feat(TournamentService, TournamentResultsTab): enhance knockout match handling and UI interactions
- Introduced new functions for determining knockout round order and building preferred knockout matches based on qualifiers. - Updated TournamentResultsTab to include collapsible sections for group matches and knockout rounds, improving user experience. - Added data properties and methods to manage the visibility of match sections and handle tournament class checks. - Refined UI elements for better interaction, including toggle buttons and improved styling for match sections.
This commit is contained in:
@@ -203,6 +203,16 @@ function getLoserId(match) {
|
||||
return (w1 > w2) ? match.player2Id : match.player1Id;
|
||||
}
|
||||
|
||||
function getKnockoutRoundOrder(roundName) {
|
||||
if (!roundName || typeof roundName !== 'string') return null;
|
||||
if (roundName === THIRD_PLACE_ROUND) return 98;
|
||||
if (roundName.includes('Achtelfinale')) return 10;
|
||||
if (roundName.includes('Viertelfinale')) return 20;
|
||||
if (roundName.includes('Halbfinale')) return 30;
|
||||
if (roundName.includes('Finale')) return 40;
|
||||
return null;
|
||||
}
|
||||
|
||||
function shuffleInPlace(arr) {
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
@@ -249,6 +259,49 @@ function compareAdvancementCandidates(a, b) {
|
||||
return Number(a.id || 0) - Number(b.id || 0);
|
||||
}
|
||||
|
||||
function compareQualifierGroups(a, b) {
|
||||
const aNum = Number(a);
|
||||
const bNum = Number(b);
|
||||
const aFinite = Number.isFinite(aNum);
|
||||
const bFinite = Number.isFinite(bNum);
|
||||
if (aFinite && bFinite && aNum !== bNum) return aNum - bNum;
|
||||
return String(a).localeCompare(String(b), 'de');
|
||||
}
|
||||
|
||||
function buildPreferredLegacyKnockoutMatches(qualifiersByGroup, advancingPerGroup) {
|
||||
if (Number(advancingPerGroup) !== 2) return null;
|
||||
|
||||
const groups = Object.keys(qualifiersByGroup).sort(compareQualifierGroups);
|
||||
if (groups.length < 2) return null;
|
||||
|
||||
const winners = [];
|
||||
const runners = [];
|
||||
for (const groupKey of groups) {
|
||||
const ranked = Array.isArray(qualifiersByGroup[groupKey]) ? qualifiersByGroup[groupKey] : [];
|
||||
const winner = ranked.find(q => Number(q.position) === 1) || ranked[0];
|
||||
const runnerUp = ranked.find(q => Number(q.position) === 2) || ranked[1];
|
||||
if (!winner || !runnerUp) return null;
|
||||
winners.push(winner);
|
||||
runners.push(runnerUp);
|
||||
}
|
||||
|
||||
const runnerOrder = groups.length % 2 === 0
|
||||
? [...runners].reverse()
|
||||
: [...runners.slice(1), runners[0]];
|
||||
|
||||
const matches = [];
|
||||
for (let i = 0; i < winners.length; i++) {
|
||||
const player1 = winners[i];
|
||||
const player2 = runnerOrder[i];
|
||||
if (!player1 || !player2 || !player1.id || !player2.id) continue;
|
||||
if (player1.id === player2.id) return null;
|
||||
if (String(player1.groupId) === String(player2.groupId)) return null;
|
||||
matches.push({ player1, player2 });
|
||||
}
|
||||
|
||||
return matches.length > 0 ? matches : null;
|
||||
}
|
||||
|
||||
const THIRD_PLACE_ROUND = 'Spiel um Platz 3';
|
||||
class TournamentService {
|
||||
/**
|
||||
@@ -3011,77 +3064,74 @@ Ve // 2. Neues Turnier anlegen
|
||||
qualifiersByGroup[groupKey].sort((a, b) => a.position - b.position);
|
||||
});
|
||||
|
||||
// Erstelle eine flache Liste aller Qualifiers, gruppiert nach Gruppen
|
||||
const groups = Object.keys(qualifiersByGroup);
|
||||
const advancingPerGroup = t.advancingPerGroup;
|
||||
|
||||
// Erstelle Paarungen: 1. gegen letzter weitergekommener, 2. gegen vorletzter, etc.
|
||||
// Wichtig: Niemand darf gegen jemanden aus der eigenen Gruppe spielen
|
||||
const matches = [];
|
||||
const usedQualifiers = new Set();
|
||||
|
||||
// Für jede Position (1., 2., 3., etc.)
|
||||
for (let pos = 1; pos <= advancingPerGroup; pos++) {
|
||||
// Finde alle Qualifiers mit dieser Position
|
||||
const qualifiersAtPosition = [];
|
||||
groups.forEach(groupKey => {
|
||||
const groupQualifiers = qualifiersByGroup[groupKey];
|
||||
const qualifierAtPos = groupQualifiers.find(q => q.position === pos);
|
||||
if (qualifierAtPos && !usedQualifiers.has(qualifierAtPos.id)) {
|
||||
qualifiersAtPosition.push(qualifierAtPos);
|
||||
}
|
||||
});
|
||||
|
||||
// Paare jeden Qualifier dieser Position mit dem entsprechenden Gegner
|
||||
// 1. Platz spielt gegen letzter weitergekommener Platz (z.B. bei 2 weiterkommenden: 1. gegen 2.)
|
||||
// 2. Platz spielt gegen vorletzter weitergekommener Platz (z.B. bei 2 weiterkommenden: 2. gegen 1.)
|
||||
const opponentPosition = advancingPerGroup - pos + 1;
|
||||
|
||||
qualifiersAtPosition.forEach(qualifier => {
|
||||
// Finde Gegner mit opponentPosition aus einer anderen Gruppe
|
||||
let opponent = null;
|
||||
for (const groupKey of groups) {
|
||||
if (groupKey === qualifier.groupId.toString()) continue; // Nicht aus derselben Gruppe
|
||||
|
||||
let matches = buildPreferredLegacyKnockoutMatches(qualifiersByGroup, advancingPerGroup);
|
||||
|
||||
if (!matches) {
|
||||
const groups = Object.keys(qualifiersByGroup).sort(compareQualifierGroups);
|
||||
|
||||
// Fallback für Sonderfälle jenseits des Standardfalls "Top 2 aus jeder Gruppe".
|
||||
// Wichtig: Niemand darf gegen jemanden aus der eigenen Gruppe spielen.
|
||||
matches = [];
|
||||
const usedQualifiers = new Set();
|
||||
|
||||
for (let pos = 1; pos <= advancingPerGroup; pos++) {
|
||||
const qualifiersAtPosition = [];
|
||||
groups.forEach(groupKey => {
|
||||
const groupQualifiers = qualifiersByGroup[groupKey];
|
||||
const opponentCandidate = groupQualifiers.find(q =>
|
||||
q.position === opponentPosition && !usedQualifiers.has(q.id)
|
||||
);
|
||||
|
||||
if (opponentCandidate) {
|
||||
opponent = opponentCandidate;
|
||||
break;
|
||||
const qualifierAtPos = groupQualifiers.find(q => q.position === pos);
|
||||
if (qualifierAtPos && !usedQualifiers.has(qualifierAtPos.id)) {
|
||||
qualifiersAtPosition.push(qualifierAtPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Falls kein Gegner gefunden, suche nach einem beliebigen Gegner aus einer anderen Gruppe
|
||||
if (!opponent) {
|
||||
});
|
||||
|
||||
const opponentPosition = advancingPerGroup - pos + 1;
|
||||
|
||||
qualifiersAtPosition.forEach(qualifier => {
|
||||
let opponent = null;
|
||||
for (const groupKey of groups) {
|
||||
if (groupKey === qualifier.groupId.toString()) continue;
|
||||
|
||||
|
||||
const groupQualifiers = qualifiersByGroup[groupKey];
|
||||
const opponentCandidate = groupQualifiers.find(q => !usedQualifiers.has(q.id));
|
||||
|
||||
const opponentCandidate = groupQualifiers.find(q =>
|
||||
q.position === opponentPosition && !usedQualifiers.has(q.id)
|
||||
);
|
||||
|
||||
if (opponentCandidate) {
|
||||
opponent = opponentCandidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opponent) {
|
||||
matches.push({ player1: qualifier, player2: opponent });
|
||||
usedQualifiers.add(qualifier.id);
|
||||
usedQualifiers.add(opponent.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (!opponent) {
|
||||
for (const groupKey of groups) {
|
||||
if (groupKey === qualifier.groupId.toString()) continue;
|
||||
|
||||
const groupQualifiers = qualifiersByGroup[groupKey];
|
||||
const opponentCandidate = groupQualifiers.find(q => !usedQualifiers.has(q.id));
|
||||
|
||||
if (opponentCandidate) {
|
||||
opponent = opponentCandidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (opponent) {
|
||||
matches.push({ player1: qualifier, player2: opponent });
|
||||
usedQualifiers.add(qualifier.id);
|
||||
usedQualifiers.add(opponent.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Falls Qualifiers übrig bleiben (ungerade Teilnehmerzahl / keine gültige Paarung möglich):
|
||||
// Freilos vergeben. Wir erzeugen KEIN Match mit doppelten Spielern.
|
||||
// Der Spieler mit Freilos wird in späteren Runden berücksichtigt, sobald dort (durch Ergebnisse)
|
||||
// echte Gegner feststehen. (Passt zur Vorgabe: keine Placeholder-Matches ohne bekannte Spieler.)
|
||||
const unusedQualifiers = classQualifiers.filter(q => q && q.id && !usedQualifiers.has(q.id));
|
||||
const usedQualifierIds = new Set(matches.flatMap(match => [match.player1?.id, match.player2?.id]).filter(id => Number.isFinite(id) && id > 0));
|
||||
const unusedQualifiers = classQualifiers.filter(q => q && q.id && !usedQualifierIds.has(q.id));
|
||||
if (unusedQualifiers.length > 0) {
|
||||
devLog(`[startKnockout] Assigning ${unusedQualifiers.length} bye(s) for class ${classKey}:`, unusedQualifiers.map(q => q.id));
|
||||
}
|
||||
@@ -3551,8 +3601,43 @@ Ve // 2. Neues Turnier anlegen
|
||||
throw new Error('Spiele mit aufgegebenen Spielern können nicht wieder geöffnet werden.');
|
||||
}
|
||||
|
||||
const currentRoundOrder = getKnockoutRoundOrder(match.round);
|
||||
if (currentRoundOrder != null) {
|
||||
const dependentWhere = {
|
||||
tournamentId,
|
||||
classId: match.classId ?? null,
|
||||
id: { [Op.ne]: match.id }
|
||||
};
|
||||
|
||||
if (match.stageId) {
|
||||
dependentWhere.stageId = match.stageId;
|
||||
} else {
|
||||
dependentWhere.stageId = null;
|
||||
}
|
||||
|
||||
if (match.groupId) {
|
||||
dependentWhere.groupId = match.groupId;
|
||||
} else {
|
||||
dependentWhere.groupId = null;
|
||||
}
|
||||
|
||||
const dependentMatches = await TournamentMatch.findAll({ where: dependentWhere });
|
||||
const matchesToDelete = dependentMatches.filter(candidate => {
|
||||
const candidateOrder = getKnockoutRoundOrder(candidate.round);
|
||||
return candidateOrder != null && candidateOrder > currentRoundOrder;
|
||||
});
|
||||
|
||||
if (matchesToDelete.length > 0) {
|
||||
const dependentIds = matchesToDelete.map(candidate => candidate.id);
|
||||
await TournamentResult.destroy({ where: { matchId: { [Op.in]: dependentIds } } });
|
||||
await TournamentMatch.destroy({ where: { id: { [Op.in]: dependentIds } } });
|
||||
}
|
||||
}
|
||||
|
||||
// Nur den Abschluss‑Status zurücksetzen, nicht die Einzelsätze
|
||||
match.isFinished = false;
|
||||
match.isActive = false;
|
||||
match.tableNumber = null;
|
||||
match.result = null; // optional: entfernt die zusammengefasste Ergebnis‑Spalte
|
||||
await match.save();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user