Fixed tournament - groups in end round and place 3
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 47s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 47s
This commit is contained in:
@@ -124,6 +124,86 @@ function compareKnockoutEntrants(a, b) {
|
||||
|
||||
const THIRD_PLACE_ROUND = 'Spiel um Platz 3';
|
||||
class TournamentService {
|
||||
async createRoundRobinMatchesForGroups(tournamentId, groups, stageId = null) {
|
||||
for (const group of groups) {
|
||||
const internalMembers = await TournamentMember.findAll({ where: { groupId: group.id } });
|
||||
const externalMembers = await ExternalTournamentParticipant.findAll({ where: { groupId: group.id } });
|
||||
|
||||
const allGroupMembers = [
|
||||
...internalMembers.map(m => ({ id: m.id, key: `internal-${m.id}` })),
|
||||
...externalMembers.map(m => ({ id: m.id, key: `external-${m.id}` })),
|
||||
];
|
||||
if (allGroupMembers.length < 2) continue;
|
||||
|
||||
const memberMap = new Map(allGroupMembers.map(m => [m.key, m]));
|
||||
const rounds = this.generateRoundRobinSchedule(allGroupMembers.map(m => ({ id: m.key })));
|
||||
for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
|
||||
for (const [p1Key, p2Key] of rounds[roundIndex]) {
|
||||
const p1 = memberMap.get(p1Key);
|
||||
const p2 = memberMap.get(p2Key);
|
||||
if (!p1 || !p2) continue;
|
||||
await TournamentMatch.create({
|
||||
tournamentId,
|
||||
stageId,
|
||||
groupId: group.id,
|
||||
round: 'group',
|
||||
player1Id: p1.id,
|
||||
player2Id: p2.id,
|
||||
groupRound: roundIndex + 1,
|
||||
classId: group.classId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async assignEntrantsToStageGroups(tournamentId, items, groups, classId) {
|
||||
if (!groups.length || !items.length) return;
|
||||
const orderedItems = [...items].sort(compareKnockoutEntrants);
|
||||
const assignments = groups.map(() => []);
|
||||
orderedItems.forEach((item, index) => {
|
||||
assignments[index % groups.length].push(item);
|
||||
});
|
||||
|
||||
for (let groupIndex = 0; groupIndex < assignments.length; groupIndex++) {
|
||||
const group = groups[groupIndex];
|
||||
for (const item of assignments[groupIndex]) {
|
||||
if (item.isExternal) {
|
||||
const original = await ExternalTournamentParticipant.findByPk(Number(item.id));
|
||||
if (!original) continue;
|
||||
await ExternalTournamentParticipant.create({
|
||||
tournamentId,
|
||||
groupId: group.id,
|
||||
firstName: original.firstName,
|
||||
lastName: original.lastName,
|
||||
club: original.club,
|
||||
email: original.email,
|
||||
address: original.address,
|
||||
birthDate: original.birthDate,
|
||||
gender: original.gender,
|
||||
seeded: original.seeded || false,
|
||||
classId,
|
||||
outOfCompetition: original.outOfCompetition || false,
|
||||
gaveUp: original.gaveUp || false,
|
||||
});
|
||||
} else {
|
||||
const clubMemberId = Number(item.clubMemberId ?? item.member?.id ?? item.id);
|
||||
const member = Number.isFinite(clubMemberId) ? await Member.findByPk(clubMemberId) : null;
|
||||
if (!member) continue;
|
||||
await TournamentMember.create({
|
||||
tournamentId,
|
||||
groupId: group.id,
|
||||
clubMemberId,
|
||||
seeded: item.seeded || false,
|
||||
classId,
|
||||
outOfCompetition: item.outOfCompetition || false,
|
||||
gaveUp: item.gaveUp || false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Multi-Stage (Runden) V1 --------
|
||||
async getTournamentStages(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
@@ -257,24 +337,35 @@ class TournamentService {
|
||||
}
|
||||
|
||||
const config = normalizeJsonConfig(adv.config, 'Advancement-Konfiguration');
|
||||
const pools = Array.isArray(config.pools) ? config.pools : [];
|
||||
let pools = Array.isArray(config.pools) ? config.pools : [];
|
||||
if (pools.length === 0) {
|
||||
const keys = Object.keys(config);
|
||||
throw new Error(
|
||||
`Advancement-Konfiguration ist leer (keine Pools). ` +
|
||||
`advancementId=${adv.id}, fromStageId=${fromStage.id}, toStageId=${toStage.id}, ` +
|
||||
`configKeys=${keys.join(',') || '(none)'}`
|
||||
);
|
||||
const advancingCount = Math.max(1, Number(toStage.advancingPerGroup || t.advancingPerGroup || 2));
|
||||
const fromPlaces = Array.from({ length: advancingCount }, (_, index) => index + 1);
|
||||
pools = [{
|
||||
fromPlaces,
|
||||
target: toStage.type === 'knockout'
|
||||
? { type: 'knockout', singleField: true }
|
||||
: { type: 'groups', groupCount: Math.max(1, Number(toStage.numberOfGroups || 1)) },
|
||||
}];
|
||||
}
|
||||
|
||||
// Cleanup Stage2
|
||||
// Cleanup Zielrunde inklusive dort duplizierter Teilnehmer.
|
||||
const existingTargetGroups = await TournamentGroup.findAll({ where: { tournamentId, stageId: toStage.id } });
|
||||
const existingTargetGroupIds = existingTargetGroups.map(g => g.id);
|
||||
await TournamentMatch.destroy({ where: { tournamentId, stageId: toStage.id } });
|
||||
if (existingTargetGroupIds.length > 0) {
|
||||
await TournamentMember.destroy({ where: { tournamentId, groupId: { [Op.in]: existingTargetGroupIds } } });
|
||||
await ExternalTournamentParticipant.destroy({ where: { tournamentId, groupId: { [Op.in]: existingTargetGroupIds } } });
|
||||
}
|
||||
await TournamentGroup.destroy({ where: { tournamentId, stageId: toStage.id } });
|
||||
|
||||
// Stage1 Gruppen mit Teilnehmern (Ranking aus existierender Logik)
|
||||
const stage1Groups = await this.getGroupsWithParticipants(userToken, clubId, tournamentId);
|
||||
const relevantStage1Groups = stage1Groups.filter(g => (g.stageId == null) || (g.stageId === fromStage.id));
|
||||
if (relevantStage1Groups.length === 0) throw new Error('Keine Gruppen in Runde 1 gefunden');
|
||||
const relevantStage1Groups = stage1Groups.filter(g => {
|
||||
if (Number(fromStage.index) === 1 && g.stageId == null) return true;
|
||||
return Number(g.stageId) === Number(fromStage.id);
|
||||
});
|
||||
if (relevantStage1Groups.length === 0) throw new Error('Keine Gruppen in der Ausgangsrunde gefunden');
|
||||
|
||||
const perGroupRanked = relevantStage1Groups.map(g => ({
|
||||
groupId: g.groupId,
|
||||
@@ -370,10 +461,8 @@ class TournamentService {
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
|
||||
// WICHTIG: Um bestehende Gruppen (Stage 1) nicht zu überschreiben,
|
||||
// nehmen wir hier KEINE groupId-Zuordnung für Teilnehmer vor.
|
||||
// Die Stage-2-Gruppen werden angelegt und durch Matches befüllt,
|
||||
// ohne den groupId-Wert der Teilnehmer zu verändern.
|
||||
await this.assignEntrantsToStageGroups(tournamentId, shuffled, poolGroups, poolClassId);
|
||||
await this.createRoundRobinMatchesForGroups(tournamentId, poolGroups, toStage.id);
|
||||
|
||||
createdGroups.push(...poolGroups);
|
||||
} else if (target.type === 'knockout') {
|
||||
@@ -406,22 +495,6 @@ class TournamentService {
|
||||
const bracketSize = nextPowerOfTwo(entrants.length);
|
||||
const byes = bracketSize - entrants.length;
|
||||
const roundName = getRoundName(bracketSize);
|
||||
if (wantsThirdPlace && bracketSize >= 4) {
|
||||
// Platzhalter-Match; Teilnehmer werden später nach den Halbfinals gesetzt.
|
||||
await TournamentMatch.create({
|
||||
tournamentId,
|
||||
stageId: toStage.id,
|
||||
groupId: containerGroup.id,
|
||||
classId: poolClassId,
|
||||
groupRound: null,
|
||||
round: THIRD_PLACE_ROUND,
|
||||
player1Id: null,
|
||||
player2Id: null,
|
||||
isFinished: false,
|
||||
isActive: false,
|
||||
result: null,
|
||||
});
|
||||
}
|
||||
// Erzeuge zunächst BYE‑Matches für die ersten 'byes' Teilnehmer
|
||||
let remaining = [...entrants];
|
||||
if (byes > 0) {
|
||||
@@ -540,22 +613,6 @@ class TournamentService {
|
||||
const reversedSecondHalf = [...secondHalf].reverse(); // Umgekehrte Reihenfolge für Pairing
|
||||
|
||||
const roundName = getRoundName(bracketSize);
|
||||
if (thirdPlace && bracketSize >= 4) {
|
||||
await TournamentMatch.create({
|
||||
tournamentId,
|
||||
stageId: toStage.id,
|
||||
groupId: containerGroup.id,
|
||||
classId,
|
||||
groupRound: null,
|
||||
round: THIRD_PLACE_ROUND,
|
||||
player1Id: null,
|
||||
player2Id: null,
|
||||
isFinished: false,
|
||||
isActive: false,
|
||||
result: null,
|
||||
});
|
||||
}
|
||||
|
||||
// BYE‑Matches zuerst (für die ersten Teilnehmer, wenn nötig)
|
||||
if (byes > 0) {
|
||||
// Die besten Spieler bekommen BYEs
|
||||
@@ -769,7 +826,12 @@ class TournamentService {
|
||||
if (classId !== null) {
|
||||
whereClause.classId = classId;
|
||||
}
|
||||
return await TournamentMember.findAll({
|
||||
const stageGroups = await TournamentGroup.findAll({
|
||||
where: { tournamentId, stageId: { [Op.ne]: null } },
|
||||
attributes: ['id'],
|
||||
});
|
||||
const stageGroupIds = new Set(stageGroups.map(g => Number(g.id)));
|
||||
const participants = await TournamentMember.findAll({
|
||||
where: whereClause,
|
||||
include: [{
|
||||
model: Member,
|
||||
@@ -778,6 +840,7 @@ class TournamentService {
|
||||
}],
|
||||
order: [[{ model: Member, as: 'member' }, 'firstName', 'ASC']]
|
||||
});
|
||||
return participants.filter(p => !p.groupId || !stageGroupIds.has(Number(p.groupId)));
|
||||
}
|
||||
|
||||
// 5. Modus setzen (Gruppen / KO‑Runde)
|
||||
@@ -1545,6 +1608,7 @@ class TournamentService {
|
||||
// Filtere nur KO‑Container‑Gruppen heraus: erkenne sie über Stage‑Typen.
|
||||
// Vorrunden‑Gruppen bleiben sichtbar, auch wenn (noch) keine Matches existieren.
|
||||
const stages = await TournamentStage.findAll({ where: { tournamentId } });
|
||||
const stageById = new Map(stages.map(s => [Number(s.id), s]));
|
||||
const knockoutStageIds = new Set(stages.filter(s => s.type === 'knockout').map(s => s.id));
|
||||
groups = groups.filter(g => !(g.stageId && knockoutStageIds.has(g.stageId)));
|
||||
|
||||
@@ -1634,6 +1698,7 @@ class TournamentService {
|
||||
for (const tm of g.tournamentGroupMembers || []) {
|
||||
stats[tm.id] = {
|
||||
id: tm.id,
|
||||
clubMemberId: tm.clubMemberId,
|
||||
name: `${tm.member.firstName} ${tm.member.lastName}`,
|
||||
seeded: tm.seeded || false,
|
||||
isExternal: false,
|
||||
@@ -1870,10 +1935,22 @@ class TournamentService {
|
||||
};
|
||||
});
|
||||
|
||||
const stage = g.stageId ? stageById.get(Number(g.stageId)) : null;
|
||||
const sameStageGroups = classGroups.filter(other => (other.stageId ?? null) === (g.stageId ?? null));
|
||||
const stageGroupIndex = sameStageGroups.findIndex(other => Number(other.id) === Number(g.id));
|
||||
const groupNumber = stageGroupIndex >= 0 ? stageGroupIndex + 1 : idx + 1;
|
||||
const stageLabel = stage
|
||||
? (stage.name || (Number(stage.index) === 3 ? 'Endrunde' : 'Zwischenrunde'))
|
||||
: null;
|
||||
|
||||
result.push({
|
||||
groupId: g.id,
|
||||
classId: g.classId,
|
||||
groupNumber: idx + 1, // Nummer innerhalb der Klasse
|
||||
stageId: g.stageId ?? null,
|
||||
stageIndex: stage?.index ?? null,
|
||||
stageName: stageLabel,
|
||||
groupNumber,
|
||||
groupLabel: stageLabel ? `${stageLabel}-Gruppe ${groupNumber}` : null,
|
||||
participants: participantsWithPosition
|
||||
});
|
||||
});
|
||||
@@ -2150,9 +2227,20 @@ class TournamentService {
|
||||
}
|
||||
});
|
||||
|
||||
if (!existingThirdPlace) {
|
||||
if (existingThirdPlace) {
|
||||
if ((existingThirdPlace.player1Id == null || existingThirdPlace.player1Id === 0) && (existingThirdPlace.player2Id == null || existingThirdPlace.player2Id === 0)) {
|
||||
existingThirdPlace.player1Id = losers[0];
|
||||
existingThirdPlace.player2Id = losers[1];
|
||||
existingThirdPlace.stageId = existingThirdPlace.stageId || match.stageId || null;
|
||||
existingThirdPlace.groupId = existingThirdPlace.groupId || match.groupId || null;
|
||||
existingThirdPlace.isActive = true;
|
||||
await existingThirdPlace.save();
|
||||
}
|
||||
} else {
|
||||
await TournamentMatch.create({
|
||||
tournamentId,
|
||||
stageId: match.stageId || null,
|
||||
groupId: match.groupId || null,
|
||||
round: THIRD_PLACE_ROUND,
|
||||
player1Id: losers[0],
|
||||
player2Id: losers[1],
|
||||
@@ -2170,7 +2258,7 @@ class TournamentService {
|
||||
// Gilt nur für neue Stage-KO-Matches (stageId + groupId gesetzt).
|
||||
// Wichtig: Halbfinal-Rundennamen können Suffixe haben (z.B. "Halbfinale (3)").
|
||||
if (match.stageId && match.groupId && match.round && String(match.round).includes('Halbfinale')) {
|
||||
const thirdPlaceMatch = await TournamentMatch.findOne({
|
||||
let thirdPlaceMatch = await TournamentMatch.findOne({
|
||||
where: {
|
||||
tournamentId,
|
||||
stageId: match.stageId,
|
||||
@@ -2179,25 +2267,37 @@ class TournamentService {
|
||||
}
|
||||
});
|
||||
|
||||
if (thirdPlaceMatch) {
|
||||
const semifinals = await TournamentMatch.findAll({
|
||||
where: {
|
||||
tournamentId,
|
||||
stageId: match.stageId,
|
||||
groupId: match.groupId,
|
||||
}
|
||||
});
|
||||
const semifinals = await TournamentMatch.findAll({
|
||||
where: {
|
||||
tournamentId,
|
||||
stageId: match.stageId,
|
||||
groupId: match.groupId,
|
||||
}
|
||||
});
|
||||
|
||||
const semiMatches = semifinals.filter(m => m.round && String(m.round).includes('Halbfinale'));
|
||||
const semiMatches = semifinals.filter(m => m.round && String(m.round).includes('Halbfinale'));
|
||||
const finishedSemis = semiMatches.filter(m => m.isFinished && m.result);
|
||||
if (finishedSemis.length >= 2) {
|
||||
const losers = finishedSemis
|
||||
.map(getLoserId)
|
||||
.filter(id => Number.isFinite(id) && id > 0);
|
||||
|
||||
const finishedSemis = semiMatches.filter(m => m.isFinished && m.result);
|
||||
if (finishedSemis.length >= 2) {
|
||||
const losers = finishedSemis
|
||||
.map(getLoserId)
|
||||
.filter(id => Number.isFinite(id) && id > 0);
|
||||
|
||||
// Nur setzen, wenn wir genau 2 Verlierer haben und das Match noch "leer" ist.
|
||||
if (losers.length === 2 && (thirdPlaceMatch.player1Id == null || thirdPlaceMatch.player1Id === 0) && (thirdPlaceMatch.player2Id == null || thirdPlaceMatch.player2Id === 0)) {
|
||||
if (losers.length === 2) {
|
||||
if (!thirdPlaceMatch) {
|
||||
thirdPlaceMatch = await TournamentMatch.create({
|
||||
tournamentId,
|
||||
stageId: match.stageId,
|
||||
groupId: match.groupId,
|
||||
classId: match.classId,
|
||||
groupRound: null,
|
||||
round: THIRD_PLACE_ROUND,
|
||||
player1Id: losers[0],
|
||||
player2Id: losers[1],
|
||||
isFinished: false,
|
||||
isActive: true,
|
||||
result: null,
|
||||
});
|
||||
} else if ((thirdPlaceMatch.player1Id == null || thirdPlaceMatch.player1Id === 0) && (thirdPlaceMatch.player2Id == null || thirdPlaceMatch.player2Id === 0)) {
|
||||
thirdPlaceMatch.player1Id = losers[0];
|
||||
thirdPlaceMatch.player2Id = losers[1];
|
||||
thirdPlaceMatch.isActive = true;
|
||||
@@ -2208,14 +2308,17 @@ class TournamentService {
|
||||
}
|
||||
|
||||
// Prüfe, ob alle Matches dieser Runde UND Klasse abgeschlossen sind
|
||||
const roundWhere = { tournamentId, round: match.round, classId: match.classId };
|
||||
if (match.stageId) roundWhere.stageId = match.stageId;
|
||||
if (match.groupId) roundWhere.groupId = match.groupId;
|
||||
const allFinished = await TournamentMatch.count({
|
||||
where: { tournamentId, round: match.round, isFinished: false, classId: match.classId }
|
||||
where: { ...roundWhere, isFinished: false }
|
||||
}) === 0;
|
||||
|
||||
if (allFinished) {
|
||||
// Lade alle Matches dieser Runde UND Klasse
|
||||
// Lade alle Matches dieser Runde UND Klasse innerhalb desselben KO-Felds
|
||||
const sameRound = await TournamentMatch.findAll({
|
||||
where: { tournamentId, round: match.round, classId: match.classId }
|
||||
where: roundWhere
|
||||
});
|
||||
|
||||
// Gruppiere nach Klasse
|
||||
@@ -2262,6 +2365,8 @@ class TournamentService {
|
||||
}
|
||||
await TournamentMatch.create({
|
||||
tournamentId,
|
||||
stageId: match.stageId || null,
|
||||
groupId: match.groupId || null,
|
||||
round: nextName,
|
||||
player1Id: p1,
|
||||
player2Id: p2,
|
||||
@@ -3053,15 +3158,50 @@ class TournamentService {
|
||||
|
||||
async resetKnockout(userToken, clubId, tournamentId, classId = null) {
|
||||
await checkAccess(userToken, clubId);
|
||||
// lösche alle Matches außer Gruppenphase
|
||||
const where = {
|
||||
|
||||
const stages = await TournamentStage.findAll({ where: { tournamentId } });
|
||||
const nonPreliminaryStageIds = stages
|
||||
.filter(stage => Number(stage.index) !== 1)
|
||||
.map(stage => Number(stage.id));
|
||||
|
||||
const stageGroupWhere = { tournamentId };
|
||||
if (nonPreliminaryStageIds.length > 0) {
|
||||
stageGroupWhere.stageId = { [Op.in]: nonPreliminaryStageIds };
|
||||
} else {
|
||||
stageGroupWhere.stageId = { [Op.ne]: null };
|
||||
}
|
||||
if (classId != null) {
|
||||
stageGroupWhere.classId = Number(classId);
|
||||
}
|
||||
|
||||
const stageGroups = await TournamentGroup.findAll({ where: stageGroupWhere });
|
||||
const stageGroupIds = stageGroups.map(group => Number(group.id));
|
||||
|
||||
const knockoutWhere = {
|
||||
tournamentId,
|
||||
round: { [Op.ne]: "group" }
|
||||
};
|
||||
if (classId != null) {
|
||||
where.classId = Number(classId);
|
||||
knockoutWhere.classId = Number(classId);
|
||||
}
|
||||
await TournamentMatch.destroy({ where: knockoutWhere });
|
||||
|
||||
if (stageGroupIds.length > 0) {
|
||||
const stageMatchWhere = {
|
||||
tournamentId,
|
||||
[Op.or]: [
|
||||
{ groupId: { [Op.in]: stageGroupIds } },
|
||||
{ stageId: { [Op.in]: nonPreliminaryStageIds } },
|
||||
],
|
||||
};
|
||||
if (classId != null) {
|
||||
stageMatchWhere.classId = Number(classId);
|
||||
}
|
||||
await TournamentMatch.destroy({ where: stageMatchWhere });
|
||||
await TournamentMember.destroy({ where: { tournamentId, groupId: { [Op.in]: stageGroupIds } } });
|
||||
await ExternalTournamentParticipant.destroy({ where: { tournamentId, groupId: { [Op.in]: stageGroupIds } } });
|
||||
await TournamentGroup.destroy({ where: { id: { [Op.in]: stageGroupIds } } });
|
||||
}
|
||||
await TournamentMatch.destroy({ where });
|
||||
}
|
||||
|
||||
// Externe Teilnehmer hinzufügen
|
||||
@@ -3143,10 +3283,16 @@ class TournamentService {
|
||||
if (classId !== null) {
|
||||
whereClause.classId = classId;
|
||||
}
|
||||
return await ExternalTournamentParticipant.findAll({
|
||||
const stageGroups = await TournamentGroup.findAll({
|
||||
where: { tournamentId, stageId: { [Op.ne]: null } },
|
||||
attributes: ['id'],
|
||||
});
|
||||
const stageGroupIds = new Set(stageGroups.map(g => Number(g.id)));
|
||||
const participants = await ExternalTournamentParticipant.findAll({
|
||||
where: whereClause,
|
||||
order: [['firstName', 'ASC'], ['lastName', 'ASC']]
|
||||
});
|
||||
return participants.filter(p => !p.groupId || !stageGroupIds.has(Number(p.groupId)));
|
||||
}
|
||||
|
||||
// Externe Teilnehmer löschen
|
||||
|
||||
@@ -563,16 +563,34 @@ export default {
|
||||
})
|
||||
.filter(p => p.fromPlaces.length > 0);
|
||||
},
|
||||
defaultPoolsForTarget(targetType, groupCount = 1, thirdPlace = false) {
|
||||
return [{
|
||||
fromPlaces: [1, 2],
|
||||
target: targetType === 'knockout'
|
||||
? { type: 'knockout', singleField: true, thirdPlace }
|
||||
: { type: 'groups', groupCount: Math.max(1, Number(groupCount || 1)) }
|
||||
}];
|
||||
},
|
||||
buildPayload() {
|
||||
const pools12 = this.stageConfig.useIntermediateStage
|
||||
let pools12 = this.stageConfig.useIntermediateStage
|
||||
? this.buildPoolsPayload(this.stageConfig.pools12, this.stageConfig.stage2GroupCount || 2, false)
|
||||
: [];
|
||||
const poolsFinal = this.buildPoolsPayload(
|
||||
if (this.stageConfig.useIntermediateStage && pools12.length === 0) {
|
||||
pools12 = this.defaultPoolsForTarget(this.stageConfig.stage2Type, this.stageConfig.stage2GroupCount || 2, false);
|
||||
}
|
||||
let poolsFinal = this.buildPoolsPayload(
|
||||
this.stageConfig.poolsFinal,
|
||||
this.stageConfig.finalStageGroupCount || 1,
|
||||
true,
|
||||
this.stageConfig.finalStageThirdPlace === true
|
||||
);
|
||||
if (poolsFinal.length === 0) {
|
||||
poolsFinal = this.defaultPoolsForTarget(
|
||||
this.stageConfig.finalStageType,
|
||||
this.stageConfig.finalStageGroupCount || 1,
|
||||
this.stageConfig.finalStageThirdPlace === true
|
||||
);
|
||||
}
|
||||
|
||||
const stages = [
|
||||
{ index: 1, type: 'groups', name: 'Vorrunde' },
|
||||
@@ -637,15 +655,6 @@ export default {
|
||||
try {
|
||||
const { stages, advancements } = this.buildPayload();
|
||||
|
||||
// Validierung: Für jeden Übergang müssen Pools vorhanden sein
|
||||
for (const adv of advancements) {
|
||||
const hasPools = Array.isArray(adv?.config?.pools) && adv.config.pools.length > 0;
|
||||
if (!hasPools) {
|
||||
const label = `${adv.fromStageIndex}→${adv.toStageIndex}`;
|
||||
throw new Error(`Bitte mindestens eine Pool-Regel für ${label} anlegen (z.B. Plätze 1,2).`);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await apiClient.put('/tournament/stages', {
|
||||
clubId: Number(this.clubId),
|
||||
tournamentId: Number(this.tournamentId),
|
||||
@@ -684,13 +693,8 @@ export default {
|
||||
clubId: Number(this.clubId),
|
||||
tournamentId: Number(this.tournamentId)
|
||||
};
|
||||
if (from?.stageId && to?.stageId) {
|
||||
payload.fromStageId = from.stageId;
|
||||
payload.toStageId = to.stageId;
|
||||
} else {
|
||||
payload.fromStageIndex = Number(fromStageIndex);
|
||||
payload.toStageIndex = Number(toStageIndex);
|
||||
}
|
||||
payload.fromStageIndex = Number(fromStageIndex);
|
||||
payload.toStageIndex = Number(toStageIndex);
|
||||
|
||||
const res = await apiClient.post('/tournament/stages/advance', payload);
|
||||
if (res.status >= 400) throw new Error(res.data?.error || 'Fehler beim Erstellen der Runde');
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</h4>
|
||||
</div>
|
||||
<div v-for="group in classGroups" :key="group.groupId" class="group-table">
|
||||
<h4>{{ $t('tournaments.groupNumber') }} {{ group.groupNumber }}</h4>
|
||||
<h4>{{ group.groupLabel || ($t('tournaments.groupNumber') + ' ' + group.groupNumber) }}</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</span>
|
||||
</h4>
|
||||
<div class="group-table" v-for="(g, gi) in classGroups" :key="`group-${classId}-${gi}`">
|
||||
<h5>{{ $t('tournaments.group') }} {{ g.groupNumber }}</h5>
|
||||
<h5>{{ g.groupLabel || ($t('tournaments.group') + ' ' + g.groupNumber) }}</h5>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -401,23 +401,53 @@ export default {
|
||||
byClass[classKey] = (byClass[classKey] || []).sort((a, b) => Number(a.position) - Number(b.position));
|
||||
});
|
||||
|
||||
const finalStageGroups = this.finalStageGroups;
|
||||
finalStageGroups.forEach(group => {
|
||||
const classKey = group.classId != null ? String(group.classId) : 'null';
|
||||
const cid = group.classId == null ? null : Number(group.classId);
|
||||
const rankings = this.groupRankings[group.groupId] || [];
|
||||
if (rankings.length === 0) return;
|
||||
byClass[classKey] = rankings.map(r => ({
|
||||
position: r.position,
|
||||
member: {
|
||||
id: r.clubMemberId || r.memberId || r.id,
|
||||
firstName: (r.name || '').split(' ').slice(0, -1).join(' '),
|
||||
lastName: (r.name || '').split(' ').slice(-1).join(' '),
|
||||
},
|
||||
displayName: r.name,
|
||||
classId: cid,
|
||||
isExternal: r.isExternal || false,
|
||||
})).sort((a, b) => Number(a.position) - Number(b.position));
|
||||
});
|
||||
|
||||
Object.keys(byClass).forEach(k => {
|
||||
if (!byClass[k] || byClass[k].length === 0) delete byClass[k];
|
||||
});
|
||||
|
||||
return byClass;
|
||||
},
|
||||
finalStageGroups() {
|
||||
const stageGroups = (this.groups || []).filter(g => g.stageId !== null && g.stageId !== undefined);
|
||||
if (stageGroups.length === 0) return [];
|
||||
const maxStageIndex = Math.max(...stageGroups.map(g => Number(g.stageIndex || 0)));
|
||||
return stageGroups.filter(g => Number(g.stageIndex || 0) === maxStageIndex);
|
||||
},
|
||||
groupPlacements() {
|
||||
const placements = [];
|
||||
// Primär: aus groups + groupRankings
|
||||
if ((this.groups || []).length > 0) {
|
||||
this.groups.forEach(group => {
|
||||
this.groups
|
||||
.filter(group => group.stageId === null || group.stageId === undefined)
|
||||
.forEach(group => {
|
||||
const rankings = this.groupRankings[group.groupId] || [];
|
||||
if (rankings.length > 0) {
|
||||
placements.push({
|
||||
groupId: group.groupId,
|
||||
groupNumber: group.groupNumber,
|
||||
classId: group.classId,
|
||||
groupLabel: group.groupLabel || null,
|
||||
stageId: group.stageId || null,
|
||||
stageIndex: group.stageIndex || null,
|
||||
rankings: rankings.map(r => ({
|
||||
id: r.id,
|
||||
position: r.position,
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
:selected-date="selectedDate"
|
||||
@update:modelValue="$emit('update:selectedViewClass', $event)"
|
||||
/>
|
||||
<section v-if="filteredGroupMatches.length" class="group-matches">
|
||||
<section v-for="section in groupMatchSections" :key="section.key" class="group-matches">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||
<h4>{{ $t('tournaments.groupMatches') }}</h4>
|
||||
<h4>{{ section.title }}</h4>
|
||||
<div>
|
||||
<button v-if="numberOfTables" @click="onDistributeTables" class="btn-secondary">{{ $t('tournaments.distributeTables') }}</button>
|
||||
</div>
|
||||
@@ -27,14 +27,14 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in filteredGroupMatches" :key="m.id" :data-match-id="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="$emit('update:activeMatchId', m.id)">
|
||||
<tr v-for="m in section.matches" :key="m.id" :data-match-id="m.id" :class="{ 'active-match': activeMatchId === m.id, 'match-finished': m.isFinished, 'match-live': m.isActive }" @click="$emit('update:activeMatchId', m.id)">
|
||||
<td>{{ m.groupRound }}</td>
|
||||
<td>
|
||||
<template v-if="getGroupClassName(m.groupId)">
|
||||
{{ getGroupClassName(m.groupId) }} - {{ $t('tournaments.groupNumber') }} {{ m.groupNumber }}
|
||||
{{ getGroupClassName(m.groupId) }} - {{ m.groupLabel || ($t('tournaments.groupNumber') + ' ' + m.groupNumber) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('tournaments.groupNumber') }} {{ m.groupNumber }}
|
||||
{{ m.groupLabel || ($t('tournaments.groupNumber') + ' ' + m.groupNumber) }}
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
@@ -125,12 +125,12 @@
|
||||
</div>
|
||||
<div v-if="canStartKnockout && !showKnockout && numberOfGroupsForSelectedClass > 1" class="ko-start">
|
||||
<button @click="$emit('start-knockout')">
|
||||
{{ $t('tournaments.startKORound') }}
|
||||
{{ $t('tournaments.startFinalRound') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="showKnockout && canResetKnockout && numberOfGroupsForSelectedClass > 1" class="ko-reset" style="margin-top:1rem">
|
||||
<button @click="$emit('reset-knockout')" class="trash-btn">
|
||||
🗑️ {{ $t('tournaments.deleteKORound') }}
|
||||
🗑️ {{ $t('tournaments.deleteFinalRound') }}
|
||||
</button>
|
||||
</div>
|
||||
<section v-if="showKnockout && numberOfGroupsForSelectedClass > 1 && filteredKnockoutMatches.length" class="ko-round">
|
||||
@@ -369,6 +369,18 @@ export default {
|
||||
filteredGroupMatches() {
|
||||
return this.filterMatchesByClass(this.groupMatches);
|
||||
},
|
||||
preliminaryGroupMatches() {
|
||||
return this.filteredGroupMatches.filter(m => !this.isStageMatch(m));
|
||||
},
|
||||
finalGroupMatches() {
|
||||
return this.filteredGroupMatches.filter(m => this.isStageMatch(m));
|
||||
},
|
||||
groupMatchSections() {
|
||||
return [
|
||||
{ key: 'preliminary', title: 'Vorrunde', matches: this.preliminaryGroupMatches },
|
||||
{ key: 'final', title: 'Endrunde', matches: this.finalGroupMatches },
|
||||
].filter(section => section.matches.length > 0);
|
||||
},
|
||||
filteredKnockoutMatches() {
|
||||
return this.filterMatchesByClass(this.knockoutMatches);
|
||||
},
|
||||
@@ -398,9 +410,8 @@ export default {
|
||||
return result;
|
||||
},
|
||||
numberOfGroupsForSelectedClass() {
|
||||
// Zähle direkt die Gruppen für die ausgewählte Klasse
|
||||
// Nur Stage 1 Gruppen (stageId null/undefined) zählen
|
||||
// Und nur Gruppen mit mindestens einem Teilnehmer
|
||||
// Zähle direkt die Vorrunden-Gruppen für die ausgewählte Klasse.
|
||||
// Endrunden-Gruppen dürfen den Start-Button nicht erneut aktivieren.
|
||||
let groupsToCount = this.groups.filter(g =>
|
||||
(!g.stageId || g.stageId === null || g.stageId === undefined) &&
|
||||
g.participants && Array.isArray(g.participants) && g.participants.length > 0
|
||||
@@ -517,6 +528,9 @@ export default {
|
||||
const confirmed = this.$root && this.$root.showConfirm ? await this.$root.showConfirm(title || '', message || '') : confirm(message);
|
||||
if (confirmed) this.$emit('delete-set', match, set);
|
||||
},
|
||||
isStageMatch(match) {
|
||||
return match && match.stageId !== null && match.stageId !== undefined;
|
||||
},
|
||||
filterMatchesByClass(matches) {
|
||||
// Wenn keine Klasse ausgewählt ist (null), zeige alle
|
||||
if (this.selectedViewClass === null || this.selectedViewClass === undefined) {
|
||||
|
||||
@@ -976,7 +976,9 @@
|
||||
"setDiff": "Satzdifferenz",
|
||||
"createMatches": "Spiele erstellen",
|
||||
"startKORound": "K.o.-Runde starten",
|
||||
"startFinalRound": "Endrunde starten",
|
||||
"deleteKORound": "K.o.-Runde",
|
||||
"deleteFinalRound": "Endrunde löschen",
|
||||
"email": "E-Mail",
|
||||
"forForwarding": "für Weitermeldung",
|
||||
"showPlayerDetails": "Spielerdetails anzeigen",
|
||||
|
||||
@@ -1043,7 +1043,9 @@
|
||||
"setDiff": "Satzdifferenz",
|
||||
"createMatches": "Spiele erstellen",
|
||||
"startKORound": "K.o.-Runde starten",
|
||||
"startFinalRound": "Endrunde starten",
|
||||
"deleteKORound": "K.o.-Runde",
|
||||
"deleteFinalRound": "Endrunde löschen",
|
||||
"email": "E-Mail",
|
||||
"forForwarding": "für Weitermeldung",
|
||||
"showPlayerDetails": "Spielerdetails anzeigen",
|
||||
|
||||
@@ -1027,7 +1027,9 @@
|
||||
"setDiff": "Sentence Difference",
|
||||
"createMatches": "Set matches",
|
||||
"startKORound": "Start knockout round",
|
||||
"startFinalRound": "Start final round",
|
||||
"deleteKORound": "Knockout",
|
||||
"deleteFinalRound": "Delete final round",
|
||||
"email": "Email address",
|
||||
"forForwarding": "for forwarding",
|
||||
"showPlayerDetails": "View Player Details",
|
||||
|
||||
@@ -1028,7 +1028,9 @@
|
||||
"setDiff": "Sentence Difference",
|
||||
"createMatches": "Set matches",
|
||||
"startKORound": "Start knockout round",
|
||||
"startFinalRound": "Start final round",
|
||||
"deleteKORound": "Knockout",
|
||||
"deleteFinalRound": "Delete final round",
|
||||
"email": "Email address",
|
||||
"forForwarding": "for forwarding",
|
||||
"showPlayerDetails": "View Player Details",
|
||||
|
||||
@@ -1028,7 +1028,9 @@
|
||||
"setDiff": "Sentence Difference",
|
||||
"createMatches": "Set matches",
|
||||
"startKORound": "Start knockout round",
|
||||
"startFinalRound": "Start final round",
|
||||
"deleteKORound": "Knockout",
|
||||
"deleteFinalRound": "Delete final round",
|
||||
"email": "Email address",
|
||||
"forForwarding": "for forwarding",
|
||||
"showPlayerDetails": "View Player Details",
|
||||
|
||||
@@ -1027,7 +1027,9 @@
|
||||
"setDiff": "Diferencia de oración",
|
||||
"createMatches": "Establecer partidos",
|
||||
"startKORound": "Comienza la ronda eliminatoria",
|
||||
"startFinalRound": "Iniciar ronda final",
|
||||
"deleteKORound": "Knockear",
|
||||
"deleteFinalRound": "Eliminar ronda final",
|
||||
"email": "Dirección de correo electrónico",
|
||||
"forForwarding": "para reenviar",
|
||||
"showPlayerDetails": "Ver detalles del jugador",
|
||||
|
||||
@@ -1028,7 +1028,9 @@
|
||||
"setDiff": "Pagkakaiba ng Pangungusap",
|
||||
"createMatches": "Magtakda ng mga tugma",
|
||||
"startKORound": "Simulan ang knockout round",
|
||||
"startFinalRound": "Simulan ang final round",
|
||||
"deleteKORound": "Knockout",
|
||||
"deleteFinalRound": "Burahin ang final round",
|
||||
"email": "Email address",
|
||||
"forForwarding": "para sa pagpapasa",
|
||||
"showPlayerDetails": "Tingnan ang Mga Detalye ng Manlalaro",
|
||||
|
||||
@@ -1026,7 +1026,9 @@
|
||||
"setDiff": "Différence de phrase",
|
||||
"createMatches": "Définir des correspondances",
|
||||
"startKORound": "Commencer les huitièmes de finale",
|
||||
"startFinalRound": "Démarrer la phase finale",
|
||||
"deleteKORound": "Assommer",
|
||||
"deleteFinalRound": "Supprimer la phase finale",
|
||||
"email": "Adresse email",
|
||||
"forForwarding": "pour l'expédition",
|
||||
"showPlayerDetails": "Afficher les détails du joueur",
|
||||
|
||||
@@ -1028,7 +1028,9 @@
|
||||
"setDiff": "Differenza di frase",
|
||||
"createMatches": "Imposta le partite",
|
||||
"startKORound": "Inizia il turno a eliminazione diretta",
|
||||
"startFinalRound": "Avvia fase finale",
|
||||
"deleteKORound": "Tramortire",
|
||||
"deleteFinalRound": "Elimina fase finale",
|
||||
"email": "Indirizzo e-mail",
|
||||
"forForwarding": "per l'inoltro",
|
||||
"showPlayerDetails": "Visualizza i dettagli del giocatore",
|
||||
|
||||
@@ -1028,7 +1028,9 @@
|
||||
"setDiff": "文の違い",
|
||||
"createMatches": "マッチを設定する",
|
||||
"startKORound": "ノックアウトラウンドを開始する",
|
||||
"startFinalRound": "決勝ラウンドを開始",
|
||||
"deleteKORound": "ノックアウト",
|
||||
"deleteFinalRound": "決勝ラウンドを削除",
|
||||
"email": "電子メールアドレス",
|
||||
"forForwarding": "転送用",
|
||||
"showPlayerDetails": "プレーヤーの詳細を表示する",
|
||||
|
||||
@@ -1024,7 +1024,9 @@
|
||||
"setDiff": "Różnica zdań",
|
||||
"createMatches": "Ustaw mecze",
|
||||
"startKORound": "Rozpocznij rundę pucharową",
|
||||
"startFinalRound": "Rozpocznij rundę finałową",
|
||||
"deleteKORound": "Nokaut",
|
||||
"deleteFinalRound": "Usuń rundę finałową",
|
||||
"email": "Adres e-mail",
|
||||
"forForwarding": "do przesyłania",
|
||||
"showPlayerDetails": "Wyświetl szczegóły gracza",
|
||||
|
||||
@@ -1027,7 +1027,9 @@
|
||||
"setDiff": "ความแตกต่างของประโยค",
|
||||
"createMatches": "ตั้งค่าการแข่งขัน",
|
||||
"startKORound": "เริ่มรอบน็อคเอาท์",
|
||||
"startFinalRound": "เริ่มรอบสุดท้าย",
|
||||
"deleteKORound": "น็อคเอาท์",
|
||||
"deleteFinalRound": "ลบรอบสุดท้าย",
|
||||
"email": "ที่อยู่อีเมล",
|
||||
"forForwarding": "สำหรับการส่งต่อ",
|
||||
"showPlayerDetails": "ดูรายละเอียดผู้เล่น",
|
||||
|
||||
@@ -1028,7 +1028,9 @@
|
||||
"setDiff": "Pagkakaiba ng Pangungusap",
|
||||
"createMatches": "Magtakda ng mga tugma",
|
||||
"startKORound": "Simulan ang knockout round",
|
||||
"startFinalRound": "Simulan ang final round",
|
||||
"deleteKORound": "Knockout",
|
||||
"deleteFinalRound": "Burahin ang final round",
|
||||
"email": "Email address",
|
||||
"forForwarding": "para sa pagpapasa",
|
||||
"showPlayerDetails": "Tingnan ang Mga Detalye ng Manlalaro",
|
||||
|
||||
@@ -1028,7 +1028,9 @@
|
||||
"setDiff": "句子差异",
|
||||
"createMatches": "设置匹配项",
|
||||
"startKORound": "开始淘汰赛",
|
||||
"startFinalRound": "开始决赛轮",
|
||||
"deleteKORound": "昏死",
|
||||
"deleteFinalRound": "删除决赛轮",
|
||||
"email": "电子邮件",
|
||||
"forForwarding": "用于转发",
|
||||
"showPlayerDetails": "查看玩家详细信息",
|
||||
|
||||
@@ -369,7 +369,7 @@ export default {
|
||||
...mapGetters(['isAuthenticated', 'currentClub']),
|
||||
|
||||
knockoutMatches() {
|
||||
const koMatches = this.matches.filter(m => m.round !== 'group');
|
||||
const koMatches = this.matches.filter(m => m.round !== 'group' && !this.isEmptyThirdPlacePlaceholder(m));
|
||||
// Sortiere nach Klasse, dann nach Runde
|
||||
return koMatches.sort((a, b) => {
|
||||
// Zuerst nach Klasse (null zuletzt)
|
||||
@@ -406,6 +406,16 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
isEmptyThirdPlacePlaceholder(match) {
|
||||
return match
|
||||
&& String(match.round || '').includes('Spiel um Platz 3')
|
||||
&& !match.player1Id
|
||||
&& !match.player2Id
|
||||
&& !match.player1
|
||||
&& !match.player2
|
||||
&& !match.result;
|
||||
},
|
||||
|
||||
// Computed property für aktive Gruppentabellen-Zellen
|
||||
activeGroupCells() {
|
||||
if (!this.activeMatchId) return [];
|
||||
@@ -866,6 +876,9 @@ export default {
|
||||
// kein Gruppenmodus → immer starten
|
||||
return true;
|
||||
}
|
||||
if ((this.groups || []).some(g => g.stageId !== null && g.stageId !== undefined)) {
|
||||
return false;
|
||||
}
|
||||
// Gruppenmodus → prüfe Anzahl der Gruppen
|
||||
const totalGroups = this.getTotalNumberOfGroups;
|
||||
if (totalGroups <= 1) {
|
||||
@@ -892,8 +905,8 @@ export default {
|
||||
},
|
||||
|
||||
canResetKnockout() {
|
||||
// Zeige den Löschen‑Button, sobald KO‑Matches existieren
|
||||
return this.knockoutMatches.length > 0;
|
||||
return this.knockoutMatches.length > 0
|
||||
|| (this.groups || []).some(g => g.stageId !== null && g.stageId !== undefined);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -1304,7 +1317,12 @@ export default {
|
||||
`/tournament/matches/${this.currentClub}/${this.selectedDate}`
|
||||
);
|
||||
const grpMap = this.groups.reduce((m, g) => {
|
||||
m[g.groupId] = g.groupNumber;
|
||||
m[g.groupId] = {
|
||||
groupNumber: g.groupNumber,
|
||||
groupLabel: g.groupLabel || null,
|
||||
stageId: g.stageId || null,
|
||||
stageName: g.stageName || null
|
||||
};
|
||||
return m;
|
||||
}, {});
|
||||
|
||||
@@ -1315,12 +1333,16 @@ export default {
|
||||
// Stelle sicher, dass groupRound vorhanden ist (kann als group_round vom Backend kommen)
|
||||
const groupRound = m.groupRound || m.group_round || 0;
|
||||
|
||||
const groupNumber = grpMap[matchGroupId] || 0;
|
||||
const groupInfo = grpMap[matchGroupId] || {};
|
||||
const groupNumber = groupInfo.groupNumber || 0;
|
||||
|
||||
return {
|
||||
...m,
|
||||
groupId: matchGroupId,
|
||||
groupNumber: groupNumber,
|
||||
groupLabel: groupInfo.groupLabel || null,
|
||||
stageId: m.stageId || groupInfo.stageId || null,
|
||||
stageName: groupInfo.stageName || null,
|
||||
groupRound: groupRound, // Stelle sicher, dass groupRound gesetzt ist
|
||||
resultInput: '',
|
||||
isActive: m.isActive || false
|
||||
@@ -1333,7 +1355,8 @@ export default {
|
||||
// Setze Kollaps-Status: ausgeklappt wenn keine Spiele, eingeklappt wenn Spiele vorhanden
|
||||
this.showParticipants = this.matches.length === 0;
|
||||
|
||||
this.showKnockout = this.matches.some(m => m.round !== 'group');
|
||||
this.showKnockout = this.matches.some(m => m.round !== 'group')
|
||||
|| (this.groups || []).some(g => g.stageId !== null && g.stageId !== undefined);
|
||||
},
|
||||
|
||||
async loadMatches() {
|
||||
@@ -1342,7 +1365,12 @@ export default {
|
||||
`/tournament/matches/${this.currentClub}/${this.selectedDate}`
|
||||
);
|
||||
const grpMap = this.groups.reduce((m, g) => {
|
||||
m[g.groupId] = g.groupNumber;
|
||||
m[g.groupId] = {
|
||||
groupNumber: g.groupNumber,
|
||||
groupLabel: g.groupLabel || null,
|
||||
stageId: g.stageId || null,
|
||||
stageName: g.stageName || null
|
||||
};
|
||||
return m;
|
||||
}, {});
|
||||
|
||||
@@ -1353,12 +1381,16 @@ export default {
|
||||
// Stelle sicher, dass groupRound vorhanden ist (kann als group_round vom Backend kommen)
|
||||
const groupRound = m.groupRound || m.group_round || 0;
|
||||
|
||||
const groupNumber = grpMap[matchGroupId] || 0;
|
||||
const groupInfo = grpMap[matchGroupId] || {};
|
||||
const groupNumber = groupInfo.groupNumber || 0;
|
||||
|
||||
return {
|
||||
...m,
|
||||
groupId: matchGroupId,
|
||||
groupNumber: groupNumber,
|
||||
groupLabel: groupInfo.groupLabel || null,
|
||||
stageId: m.stageId || groupInfo.stageId || null,
|
||||
stageName: groupInfo.stageName || null,
|
||||
groupRound: groupRound,
|
||||
resultInput: '',
|
||||
isActive: m.isActive || false
|
||||
@@ -1368,7 +1400,8 @@ export default {
|
||||
// Setze Kollaps-Status: ausgeklappt wenn keine Spiele, eingeklappt wenn Spiele vorhanden
|
||||
this.showParticipants = this.matches.length === 0;
|
||||
|
||||
this.showKnockout = this.matches.some(m => m.round !== 'group');
|
||||
this.showKnockout = this.matches.some(m => m.round !== 'group')
|
||||
|| (this.groups || []).some(g => g.stageId !== null && g.stageId !== undefined);
|
||||
},
|
||||
|
||||
async handleTournamentChanged(data) {
|
||||
@@ -1896,44 +1929,24 @@ export default {
|
||||
}))
|
||||
.filter(s => Number.isFinite(s.stageIndex));
|
||||
|
||||
// Ermittle die Reihenfolge der Stages
|
||||
// Ermittle die Reihenfolge der Stages und führe genau den nächsten
|
||||
// konfigurierten Übergang aus. Die Zielrunde kann Gruppen oder K.o. sein.
|
||||
const ordered = normalizedStages.sort((a, b) => a.stageIndex - b.stageIndex);
|
||||
const groupStage = ordered.find(s => (s.type || s.targetType || s.target) === 'groups');
|
||||
const knockoutStage = ordered.find(s => (s.type || s.targetType || s.target) === 'knockout');
|
||||
const fromStage = ordered.find(s => (s.type || s.targetType || s.target) === 'groups');
|
||||
const toStage = fromStage
|
||||
? ordered.find(s => s.stageIndex > fromStage.stageIndex)
|
||||
: null;
|
||||
|
||||
if (groupStage && knockoutStage) {
|
||||
// Falls es Zwischenstufen vom Typ 'groups' gibt, iteriere bis zur KO‑Stufe
|
||||
let fromIdx = groupStage.stageIndex;
|
||||
let fromId = groupStage.stageId;
|
||||
for (const stage of ordered) {
|
||||
if (stage.stageIndex <= fromIdx) continue;
|
||||
// Advance Schrittweise zur nächsten Stage; prefer IDs if backend expects them
|
||||
const payload = {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
};
|
||||
if (Number.isFinite(fromId) && Number.isFinite(stage.stageId)) {
|
||||
payload.fromStageId = fromId;
|
||||
payload.toStageId = stage.stageId;
|
||||
} else {
|
||||
payload.fromStageIndex = fromIdx;
|
||||
payload.toStageIndex = stage.stageIndex;
|
||||
}
|
||||
await apiClient.post('/tournament/stages/advance', payload);
|
||||
|
||||
// Update trackers
|
||||
fromIdx = stage.stageIndex;
|
||||
fromId = stage.stageId;
|
||||
|
||||
// Wenn KO erreicht, beende
|
||||
if ((stage.type || stage.targetType || stage.target) === 'knockout') {
|
||||
await this.loadTournamentData();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nach jedem Schritt neu laden, damit Folgeschritt korrekte Daten hat
|
||||
await this.loadTournamentData();
|
||||
}
|
||||
if (fromStage && toStage) {
|
||||
const payload = {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
};
|
||||
payload.fromStageIndex = fromStage.stageIndex;
|
||||
payload.toStageIndex = toStage.stageIndex;
|
||||
await apiClient.post('/tournament/stages/advance', payload);
|
||||
await this.loadTournamentData();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -621,7 +621,15 @@ internal fun TournamentEditorMatchesTab(
|
||||
var confirmDelete by remember { mutableStateOf<Pair<Int, Int>?>(null) }
|
||||
var editingSet by remember { mutableStateOf<Pair<Int, Int>?>(null) }
|
||||
var editInput by remember { mutableStateOf("") }
|
||||
val hasKO = matches.any { (it.round ?: "").lowercase() != "group" && (it.round ?: "").isNotBlank() }
|
||||
fun isEmptyThirdPlacePlaceholder(match: TournamentMatchDto): Boolean {
|
||||
return match.round.contains("Spiel um Platz 3") &&
|
||||
match.player1Id == null &&
|
||||
match.player2Id == null &&
|
||||
match.player1 == null &&
|
||||
match.player2 == null &&
|
||||
match.result == null
|
||||
}
|
||||
val hasKO = matches.any { it.round.lowercase() != "group" && it.round.isNotBlank() && !isEmptyThirdPlacePlaceholder(it) }
|
||||
var showDistributedDialog by remember { mutableStateOf(false) }
|
||||
var distributedMatches by remember { mutableStateOf<List<TournamentMatchDto>>(emptyList()) }
|
||||
var distributedMessage by remember { mutableStateOf<String?>(null) }
|
||||
@@ -681,7 +689,7 @@ internal fun TournamentEditorMatchesTab(
|
||||
}) { Text(tr("tournaments.distributeTables", "Freie Tische verteilen")) }
|
||||
}
|
||||
// Table-like view: show header + flattened rows so Android resembles web UI
|
||||
val displayList = matches.sortedWith(compareBy<TournamentMatchDto> {
|
||||
val displayList = matches.filterNot { isEmptyThirdPlacePlaceholder(it) }.sortedWith(compareBy<TournamentMatchDto> {
|
||||
it.isFinished == true
|
||||
}.thenBy {
|
||||
// primary: if groupRound available use it, else try parse round as int, else large
|
||||
|
||||
Reference in New Issue
Block a user