feat(tournament): add participant gave-up functionality and UI updates
- Implemented setParticipantGaveUp and setExternalParticipantGaveUp methods in tournamentController to handle participant resignation. - Updated ExternalTournamentParticipant and TournamentMember models to include a gaveUp field for tracking resignation status. - Enhanced tournamentRoutes to include new endpoints for updating gave-up status. - Modified TournamentGroupsTab and TournamentParticipantsTab components to display and manage gave-up status visually. - Added localization strings for "gave up" and related hints in German. - Updated TournamentResultsTab to reflect gave-up status in match results.
This commit is contained in:
@@ -1582,6 +1582,7 @@ class TournamentService {
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
const winningSets = tournament.winningSets || 3;
|
||||
let groups = await TournamentGroup.findAll({
|
||||
where: { tournamentId },
|
||||
include: [{
|
||||
@@ -1672,6 +1673,7 @@ class TournamentService {
|
||||
? `${pairing.external2.firstName} ${pairing.external2.lastName}`
|
||||
: 'Unbekannt';
|
||||
|
||||
const pairingGaveUp = !!(pairing.member1?.gaveUp || pairing.member2?.gaveUp || pairing.external1?.gaveUp || pairing.external2?.gaveUp);
|
||||
stats[`pairing_${pairing.id}`] = {
|
||||
id: `pairing_${pairing.id}`,
|
||||
pairingId: pairing.id,
|
||||
@@ -1679,6 +1681,7 @@ class TournamentService {
|
||||
seeded: pairing.seeded || false,
|
||||
isExternal: false,
|
||||
isPairing: true,
|
||||
gaveUp: pairingGaveUp,
|
||||
player1Id: pairing.member1Id || pairing.external1Id,
|
||||
player2Id: pairing.member2Id || pairing.external2Id,
|
||||
points: 0,
|
||||
@@ -1700,6 +1703,7 @@ class TournamentService {
|
||||
name: `${tm.member.firstName} ${tm.member.lastName}`,
|
||||
seeded: tm.seeded || false,
|
||||
isExternal: false,
|
||||
gaveUp: !!tm.gaveUp,
|
||||
points: 0,
|
||||
setsWon: 0,
|
||||
setsLost: 0,
|
||||
@@ -1710,7 +1714,7 @@ class TournamentService {
|
||||
matchesLost: 0
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Externe Teilnehmer
|
||||
for (const ext of g.externalGroupMembers || []) {
|
||||
stats[ext.id] = {
|
||||
@@ -1718,6 +1722,7 @@ class TournamentService {
|
||||
name: `${ext.firstName} ${ext.lastName}`,
|
||||
seeded: ext.seeded || false,
|
||||
isExternal: true,
|
||||
gaveUp: !!ext.gaveUp,
|
||||
points: 0,
|
||||
setsWon: 0,
|
||||
setsLost: 0,
|
||||
@@ -1744,6 +1749,46 @@ class TournamentService {
|
||||
);
|
||||
|
||||
if (!pairing1Key || !pairing2Key) continue;
|
||||
|
||||
const p1GaveUp = stats[pairing1Key].gaveUp;
|
||||
const p2GaveUp = stats[pairing2Key].gaveUp;
|
||||
if (p1GaveUp || p2GaveUp) {
|
||||
if (p1GaveUp && p2GaveUp) {
|
||||
stats[pairing1Key].setsWon += 0;
|
||||
stats[pairing1Key].setsLost += 0;
|
||||
stats[pairing2Key].setsWon += 0;
|
||||
stats[pairing2Key].setsLost += 0;
|
||||
} else if (p1GaveUp && !p2GaveUp) {
|
||||
stats[pairing2Key].points += 1;
|
||||
stats[pairing2Key].matchesWon += 1;
|
||||
stats[pairing1Key].points -= 1;
|
||||
stats[pairing1Key].matchesLost += 1;
|
||||
stats[pairing1Key].setsWon += 0;
|
||||
stats[pairing1Key].setsLost += winningSets;
|
||||
stats[pairing2Key].setsWon += winningSets;
|
||||
stats[pairing2Key].setsLost += 0;
|
||||
const pts = 11 * winningSets;
|
||||
stats[pairing1Key].pointsWon += 0;
|
||||
stats[pairing1Key].pointsLost += pts;
|
||||
stats[pairing2Key].pointsWon += pts;
|
||||
stats[pairing2Key].pointsLost += 0;
|
||||
} else {
|
||||
stats[pairing1Key].points += 1;
|
||||
stats[pairing1Key].matchesWon += 1;
|
||||
stats[pairing2Key].points -= 1;
|
||||
stats[pairing2Key].matchesLost += 1;
|
||||
stats[pairing1Key].setsWon += winningSets;
|
||||
stats[pairing1Key].setsLost += 0;
|
||||
stats[pairing2Key].setsWon += 0;
|
||||
stats[pairing2Key].setsLost += winningSets;
|
||||
const pts = 11 * winningSets;
|
||||
stats[pairing1Key].pointsWon += pts;
|
||||
stats[pairing1Key].pointsLost += 0;
|
||||
stats[pairing2Key].pointsWon += 0;
|
||||
stats[pairing2Key].pointsLost += pts;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Ergebnis kann null/undefiniert oder in anderem Format sein -> defensiv prüfen
|
||||
if (!m.result || typeof m.result !== 'string' || !m.result.includes(':')) continue;
|
||||
const [s1, s2] = m.result.split(':').map(n => parseInt(n, 10));
|
||||
@@ -1781,6 +1826,46 @@ class TournamentService {
|
||||
} else {
|
||||
// Bei Einzel: Normale Logik
|
||||
if (!stats[m.player1Id] || !stats[m.player2Id]) continue;
|
||||
|
||||
const p1GaveUp = stats[m.player1Id].gaveUp;
|
||||
const p2GaveUp = stats[m.player2Id].gaveUp;
|
||||
if (p1GaveUp || p2GaveUp) {
|
||||
if (p1GaveUp && p2GaveUp) {
|
||||
stats[m.player1Id].setsWon += 0;
|
||||
stats[m.player1Id].setsLost += 0;
|
||||
stats[m.player2Id].setsWon += 0;
|
||||
stats[m.player2Id].setsLost += 0;
|
||||
} else if (p1GaveUp && !p2GaveUp) {
|
||||
stats[m.player2Id].points += 1;
|
||||
stats[m.player2Id].matchesWon += 1;
|
||||
stats[m.player1Id].points -= 1;
|
||||
stats[m.player1Id].matchesLost += 1;
|
||||
stats[m.player1Id].setsWon += 0;
|
||||
stats[m.player1Id].setsLost += winningSets;
|
||||
stats[m.player2Id].setsWon += winningSets;
|
||||
stats[m.player2Id].setsLost += 0;
|
||||
const pts = 11 * winningSets;
|
||||
stats[m.player1Id].pointsWon += 0;
|
||||
stats[m.player1Id].pointsLost += pts;
|
||||
stats[m.player2Id].pointsWon += pts;
|
||||
stats[m.player2Id].pointsLost += 0;
|
||||
} else {
|
||||
stats[m.player1Id].points += 1;
|
||||
stats[m.player1Id].matchesWon += 1;
|
||||
stats[m.player2Id].points -= 1;
|
||||
stats[m.player2Id].matchesLost += 1;
|
||||
stats[m.player1Id].setsWon += winningSets;
|
||||
stats[m.player1Id].setsLost += 0;
|
||||
stats[m.player2Id].setsWon += 0;
|
||||
stats[m.player2Id].setsLost += winningSets;
|
||||
const pts = 11 * winningSets;
|
||||
stats[m.player1Id].pointsWon += pts;
|
||||
stats[m.player1Id].pointsLost += 0;
|
||||
stats[m.player2Id].pointsWon += 0;
|
||||
stats[m.player2Id].pointsLost += pts;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Ergebnis kann null/undefiniert oder in anderem Format sein -> defensiv prüfen
|
||||
if (!m.result || typeof m.result !== 'string' || !m.result.includes(':')) continue;
|
||||
const [s1, s2] = m.result.split(':').map(n => parseInt(n, 10));
|
||||
@@ -2077,7 +2162,8 @@ class TournamentService {
|
||||
lastName: ep.lastName,
|
||||
club: ep.club,
|
||||
gender: ep.gender,
|
||||
birthDate: ep.birthDate
|
||||
birthDate: ep.birthDate,
|
||||
gaveUp: !!ep.gaveUp
|
||||
};
|
||||
} else {
|
||||
plain.player1 = null;
|
||||
@@ -2095,18 +2181,49 @@ class TournamentService {
|
||||
lastName: ep.lastName,
|
||||
club: ep.club,
|
||||
gender: ep.gender,
|
||||
birthDate: ep.birthDate
|
||||
birthDate: ep.birthDate,
|
||||
gaveUp: !!ep.gaveUp
|
||||
};
|
||||
} else {
|
||||
plain.player2 = null;
|
||||
}
|
||||
|
||||
// Virtuelle Ergebnisse bei Aufgabe: ein Spieler aufgegeben → Gegner 11:0 je Satz; beide aufgegeben → 0:0, kein Sieger
|
||||
const winningSets = t.winningSets || 3;
|
||||
const p1GaveUp = plain.player1?.gaveUp;
|
||||
const p2GaveUp = plain.player2?.gaveUp;
|
||||
if (p1GaveUp || p2GaveUp) {
|
||||
plain.gaveUpMatch = true;
|
||||
if (p1GaveUp && p2GaveUp) {
|
||||
plain.result = '0:0';
|
||||
plain.isFinished = true;
|
||||
plain.tournamentResults = [{ set: 1, pointsPlayer1: 0, pointsPlayer2: 0 }];
|
||||
} else if (p1GaveUp && !p2GaveUp) {
|
||||
plain.result = `0:${winningSets}`;
|
||||
plain.isFinished = true;
|
||||
plain.tournamentResults = Array.from({ length: winningSets }, (_, i) => ({ set: i + 1, pointsPlayer1: 0, pointsPlayer2: 11 }));
|
||||
} else {
|
||||
plain.result = `${winningSets}:0`;
|
||||
plain.isFinished = true;
|
||||
plain.tournamentResults = Array.from({ length: winningSets }, (_, i) => ({ set: i + 1, pointsPlayer1: 11, pointsPlayer2: 0 }));
|
||||
}
|
||||
}
|
||||
|
||||
return plain;
|
||||
});
|
||||
|
||||
return finalMatches;
|
||||
}
|
||||
|
||||
async _matchHasGaveUpPlayer(tournamentId, player1Id, player2Id) {
|
||||
const ids = [player1Id, player2Id].filter(Boolean);
|
||||
if (ids.length === 0) return false;
|
||||
const members = await TournamentMember.findAll({ where: { tournamentId, id: { [Op.in]: ids } } });
|
||||
const externals = await ExternalTournamentParticipant.findAll({ where: { tournamentId, id: { [Op.in]: ids } } });
|
||||
if (members.some(m => m.gaveUp) || externals.some(e => e.gaveUp)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 12. Satz-Ergebnis hinzufügen/überschreiben
|
||||
async addMatchResult(userToken, clubId, tournamentId, matchId, set, result) {
|
||||
await checkAccess(userToken, clubId);
|
||||
@@ -2119,6 +2236,9 @@ class TournamentService {
|
||||
// Lade Match, um isFinished zu prüfen
|
||||
const match = await TournamentMatch.findByPk(matchId);
|
||||
if (!match || match.tournamentId != tournamentId) throw new Error('Match nicht gefunden');
|
||||
if (await this._matchHasGaveUpPlayer(tournamentId, match.player1Id, match.player2Id)) {
|
||||
throw new Error('Ergebnisse von Spielen mit aufgegebenen Spielern können nicht bearbeitet werden.');
|
||||
}
|
||||
|
||||
const existing = await TournamentResult.findOne({ where: { matchId, set } });
|
||||
if (existing) {
|
||||
@@ -2163,6 +2283,9 @@ class TournamentService {
|
||||
include: [{ model: TournamentResult, as: "tournamentResults" }]
|
||||
});
|
||||
if (!match) throw new Error("Match nicht gefunden");
|
||||
if (await this._matchHasGaveUpPlayer(tournamentId, match.player1Id, match.player2Id)) {
|
||||
throw new Error('Spiele mit aufgegebenen Spielern können nicht abgeschlossen werden.');
|
||||
}
|
||||
|
||||
let win = 0, lose = 0;
|
||||
for (const r of match.tournamentResults) {
|
||||
@@ -2334,9 +2457,10 @@ class TournamentService {
|
||||
]
|
||||
});
|
||||
const groupMatches = await TournamentMatch.findAll({
|
||||
where: { tournamentId, round: "group", isFinished: true },
|
||||
where: { tournamentId, round: "group" },
|
||||
include: [{ model: TournamentResult, as: "tournamentResults" }]
|
||||
});
|
||||
const winningSets = tournament.winningSets || 3;
|
||||
|
||||
// Lade alle Klassen, um zu prüfen, ob es sich um Doppel-Klassen handelt
|
||||
const tournamentClasses = await TournamentClass.findAll({ where: { tournamentId } });
|
||||
@@ -2352,44 +2476,57 @@ class TournamentService {
|
||||
const stats = {};
|
||||
// Interne Teilnehmer
|
||||
for (const tm of g.tournamentGroupMembers || []) {
|
||||
stats[tm.id] = { member: tm, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: false, matchesWon: 0, matchesLost: 0 };
|
||||
stats[tm.id] = { member: tm, gaveUp: !!tm.gaveUp, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: false, matchesWon: 0, matchesLost: 0 };
|
||||
}
|
||||
// Externe Teilnehmer
|
||||
for (const ext of g.externalGroupMembers || []) {
|
||||
stats[ext.id] = { member: ext, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: true, matchesWon: 0, matchesLost: 0 };
|
||||
stats[ext.id] = { member: ext, gaveUp: !!ext.gaveUp, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: true, matchesWon: 0, matchesLost: 0 };
|
||||
}
|
||||
for (const m of groupMatches.filter(m => m.groupId === g.id)) {
|
||||
if (!stats[m.player1Id] || !stats[m.player2Id]) continue;
|
||||
const [p1, p2] = m.result.split(":").map(n => parseInt(n, 10));
|
||||
if (p1 > p2) {
|
||||
stats[m.player1Id].points += 1; // Sieger bekommt +1
|
||||
stats[m.player1Id].matchesWon += 1;
|
||||
stats[m.player2Id].points -= 1; // Verlierer bekommt -1
|
||||
stats[m.player2Id].matchesLost += 1;
|
||||
const p1GaveUp = stats[m.player1Id].gaveUp;
|
||||
const p2GaveUp = stats[m.player2Id].gaveUp;
|
||||
let p1, p2, p1Points = 0, p2Points = 0;
|
||||
if (p1GaveUp || p2GaveUp) {
|
||||
if (p1GaveUp && p2GaveUp) {
|
||||
p1 = 0; p2 = 0;
|
||||
} else if (p1GaveUp && !p2GaveUp) {
|
||||
p1 = 0; p2 = winningSets;
|
||||
p2Points = 11 * winningSets;
|
||||
} else {
|
||||
p1 = winningSets; p2 = 0;
|
||||
p1Points = 11 * winningSets;
|
||||
}
|
||||
} else if (m.isFinished && m.result && typeof m.result === 'string' && m.result.includes(':')) {
|
||||
[p1, p2] = m.result.split(":").map(n => parseInt(n, 10));
|
||||
if (m.tournamentResults && m.tournamentResults.length > 0) {
|
||||
for (const r of m.tournamentResults) {
|
||||
p1Points += Math.abs(r.pointsPlayer1 || 0);
|
||||
p2Points += Math.abs(r.pointsPlayer2 || 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stats[m.player2Id].points += 1; // Sieger bekommt +1
|
||||
continue;
|
||||
}
|
||||
if (p1 > p2) {
|
||||
stats[m.player1Id].points += 1;
|
||||
stats[m.player1Id].matchesWon += 1;
|
||||
stats[m.player2Id].points -= 1;
|
||||
stats[m.player2Id].matchesLost += 1;
|
||||
} else if (p2 > p1) {
|
||||
stats[m.player2Id].points += 1;
|
||||
stats[m.player2Id].matchesWon += 1;
|
||||
stats[m.player1Id].points -= 1; // Verlierer bekommt -1
|
||||
stats[m.player1Id].points -= 1;
|
||||
stats[m.player1Id].matchesLost += 1;
|
||||
}
|
||||
stats[m.player1Id].setsWon += p1;
|
||||
stats[m.player1Id].setsLost += p2;
|
||||
stats[m.player2Id].setsWon += p2;
|
||||
stats[m.player2Id].setsLost += p1;
|
||||
|
||||
// Berechne gespielte Punkte aus tournamentResults
|
||||
if (m.tournamentResults && m.tournamentResults.length > 0) {
|
||||
let p1Points = 0, p2Points = 0;
|
||||
for (const r of m.tournamentResults) {
|
||||
// Verwende absolute Werte, falls negative Werte gespeichert wurden
|
||||
p1Points += Math.abs(r.pointsPlayer1 || 0);
|
||||
p2Points += Math.abs(r.pointsPlayer2 || 0);
|
||||
}
|
||||
stats[m.player1Id].pointsWon += p1Points;
|
||||
stats[m.player1Id].pointsLost += p2Points;
|
||||
stats[m.player2Id].pointsWon += p2Points;
|
||||
stats[m.player2Id].pointsLost += p1Points;
|
||||
}
|
||||
stats[m.player1Id].pointsWon += p1Points;
|
||||
stats[m.player1Id].pointsLost += p2Points;
|
||||
stats[m.player2Id].pointsWon += p2Points;
|
||||
stats[m.player2Id].pointsLost += p1Points;
|
||||
}
|
||||
|
||||
// Berechne Punktverhältnis und absolute Differenz für jeden Spieler
|
||||
@@ -2413,17 +2550,30 @@ class TournamentService {
|
||||
// 5. Bei Spielpunktgleichheit: Wer mehr Spielpunkte erzielt hat
|
||||
if (b.pointsWon !== a.pointsWon) return b.pointsWon - a.pointsWon;
|
||||
// 6. Direkter Vergleich (Sieger weiter oben)
|
||||
const directMatch = groupMatches.find(m =>
|
||||
const directMatch = groupMatches.find(m =>
|
||||
m.groupId === g.id &&
|
||||
((m.player1Id === a.member.id && m.player2Id === b.member.id) ||
|
||||
(m.player1Id === b.member.id && m.player2Id === a.member.id))
|
||||
);
|
||||
if (directMatch) {
|
||||
const [s1, s2] = directMatch.result.split(":").map(n => parseInt(n, 10));
|
||||
const aWon = (directMatch.player1Id === a.member.id && s1 > s2) ||
|
||||
const p1Gu = stats[directMatch.player1Id]?.gaveUp;
|
||||
const p2Gu = stats[directMatch.player2Id]?.gaveUp;
|
||||
let s1, s2;
|
||||
if (p1Gu && p2Gu) {
|
||||
s1 = 0; s2 = 0;
|
||||
} else if (p1Gu && !p2Gu) {
|
||||
s1 = 0; s2 = winningSets;
|
||||
} else if (!p1Gu && p2Gu) {
|
||||
s1 = winningSets; s2 = 0;
|
||||
} else if (directMatch.result && typeof directMatch.result === 'string' && directMatch.result.includes(':')) {
|
||||
[s1, s2] = directMatch.result.split(":").map(n => parseInt(n, 10));
|
||||
} else {
|
||||
return a.member.id - b.member.id;
|
||||
}
|
||||
const aWon = (directMatch.player1Id === a.member.id && s1 > s2) ||
|
||||
(directMatch.player2Id === a.member.id && s2 > s1);
|
||||
if (aWon) return -1; // a hat gewonnen -> a kommt weiter oben
|
||||
return 1; // b hat gewonnen -> b kommt weiter oben
|
||||
if (aWon) return -1;
|
||||
return 1;
|
||||
}
|
||||
// Fallback: Nach ID
|
||||
return a.member.id - b.member.id;
|
||||
@@ -2942,13 +3092,35 @@ class TournamentService {
|
||||
await participant.save();
|
||||
}
|
||||
|
||||
async setParticipantGaveUp(userToken, clubId, tournamentId, participantId, gaveUp) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const participant = await TournamentMember.findOne({
|
||||
where: { id: participantId, tournamentId }
|
||||
});
|
||||
if (!participant) throw new Error('Teilnehmer nicht gefunden');
|
||||
participant.gaveUp = !!gaveUp;
|
||||
await participant.save();
|
||||
}
|
||||
|
||||
async setExternalParticipantGaveUp(userToken, clubId, tournamentId, participantId, gaveUp) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const participant = await ExternalTournamentParticipant.findOne({
|
||||
where: { id: participantId, tournamentId }
|
||||
});
|
||||
if (!participant) throw new Error('Externer Teilnehmer nicht gefunden');
|
||||
participant.gaveUp = !!gaveUp;
|
||||
await participant.save();
|
||||
}
|
||||
|
||||
// services/tournamentService.js
|
||||
async deleteMatchResult(userToken, clubId, tournamentId, matchId, setToDelete) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
// Match existiert?
|
||||
const match = await TournamentMatch.findOne({ where: { id: matchId, tournamentId } });
|
||||
if (!match) throw new Error('Match nicht gefunden');
|
||||
if (await this._matchHasGaveUpPlayer(tournamentId, match.player1Id, match.player2Id)) {
|
||||
throw new Error('Ergebnisse von Spielen mit aufgegebenen Spielern können nicht bearbeitet werden.');
|
||||
}
|
||||
|
||||
// Satz löschen
|
||||
await TournamentResult.destroy({ where: { matchId, set: setToDelete } });
|
||||
@@ -2977,6 +3149,9 @@ class TournamentService {
|
||||
if (!match) {
|
||||
throw new Error("Match nicht gefunden");
|
||||
}
|
||||
if (await this._matchHasGaveUpPlayer(tournamentId, match.player1Id, match.player2Id)) {
|
||||
throw new Error('Spiele mit aufgegebenen Spielern können nicht wieder geöffnet werden.');
|
||||
}
|
||||
|
||||
// Nur den Abschluss‑Status zurücksetzen, nicht die Einzelsätze
|
||||
match.isFinished = false;
|
||||
|
||||
Reference in New Issue
Block a user