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:
@@ -434,6 +434,20 @@ export const updateParticipantSeeded = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setParticipantGaveUp = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId } = req.params;
|
||||
const { gaveUp } = req.body;
|
||||
try {
|
||||
await tournamentService.setParticipantGaveUp(token, clubId, tournamentId, participantId, gaveUp);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Aufgabe-Status aktualisiert' });
|
||||
} catch (err) {
|
||||
console.error('[setParticipantGaveUp] Error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteMatchResult = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, matchId, set } = req.body;
|
||||
@@ -555,6 +569,20 @@ export const updateExternalParticipantSeeded = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const setExternalParticipantGaveUp = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId } = req.params;
|
||||
const { gaveUp } = req.body;
|
||||
try {
|
||||
await tournamentService.setExternalParticipantGaveUp(token, clubId, tournamentId, participantId, gaveUp);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Aufgabe-Status aktualisiert' });
|
||||
} catch (error) {
|
||||
console.error('[setExternalParticipantGaveUp] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Tournament Classes
|
||||
export const getTournamentClasses = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Add gave_up (Aufgabe) to tournament participants
|
||||
-- Wenn ein Spieler aufgibt: alle seine Spiele zählen für den Gegner (11:0), beide aufgegeben = 0:0, kein Sieger
|
||||
|
||||
ALTER TABLE `tournament_member`
|
||||
ADD COLUMN `gave_up` TINYINT(1) NOT NULL DEFAULT 0 AFTER `out_of_competition`;
|
||||
|
||||
ALTER TABLE `external_tournament_participant`
|
||||
ADD COLUMN `gave_up` TINYINT(1) NOT NULL DEFAULT 0 AFTER `out_of_competition`;
|
||||
@@ -88,6 +88,12 @@ const ExternalTournamentParticipant = sequelize.define('ExternalTournamentPartic
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
gaveUp: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
field: 'gave_up'
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
|
||||
@@ -30,6 +30,12 @@ const TournamentMember = sequelize.define('TournamentMember', {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
gaveUp: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
field: 'gave_up'
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
resetMatches,
|
||||
removeParticipant,
|
||||
updateParticipantSeeded,
|
||||
setParticipantGaveUp,
|
||||
deleteMatchResult,
|
||||
reopenMatch,
|
||||
deleteKnockoutMatches,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
getExternalParticipants,
|
||||
removeExternalParticipant,
|
||||
updateExternalParticipantSeeded,
|
||||
setExternalParticipantGaveUp,
|
||||
getTournamentClasses,
|
||||
addTournamentClass,
|
||||
updateTournamentClass,
|
||||
@@ -54,6 +56,7 @@ router.post('/participant', authenticate, addParticipant);
|
||||
router.post('/participants', authenticate, getParticipants);
|
||||
router.delete('/participant', authenticate, removeParticipant);
|
||||
router.put('/participant/:clubId/:tournamentId/:participantId/seeded', authenticate, updateParticipantSeeded);
|
||||
router.put('/participant/:clubId/:tournamentId/:participantId/gave-up', authenticate, setParticipantGaveUp);
|
||||
router.post('/modus', authenticate, setModus);
|
||||
router.post('/groups/reset', authenticate, resetGroups);
|
||||
router.post('/matches/reset', authenticate, resetMatches);
|
||||
@@ -82,6 +85,7 @@ router.post('/external-participant', authenticate, addExternalParticipant);
|
||||
router.post('/external-participants', authenticate, getExternalParticipants);
|
||||
router.delete('/external-participant', authenticate, removeExternalParticipant);
|
||||
router.put('/external-participant/:clubId/:tournamentId/:participantId/seeded', authenticate, updateExternalParticipantSeeded);
|
||||
router.put('/external-participant/:clubId/:tournamentId/:participantId/gave-up', authenticate, setExternalParticipantGaveUp);
|
||||
|
||||
// Tournament Classes
|
||||
router.get('/classes/:clubId/:tournamentId', authenticate, getTournamentClasses);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
<tr v-for="(pl, idx) in groupRankings[group.groupId]" :key="pl.id">
|
||||
<td><strong>G{{ String.fromCharCode(96 + group.groupNumber) }}{{ idx + 1 }}</strong></td>
|
||||
<td>{{ pl.position }}.</td>
|
||||
<td><span v-if="pl.seeded" class="seeded-star">★</span>{{ pl.name }}</td>
|
||||
<td><span v-if="pl.seeded" class="seeded-star">★</span>{{ pl.name }}<span v-if="pl.gaveUp" class="gave-up-badge" :title="$t('tournaments.gaveUpHint')">{{ $t('tournaments.gaveUp') }}</span></td>
|
||||
<td>{{ (pl.matchesWon || 0) * 2 }}:{{ (pl.matchesLost || 0) * 2 }}</td>
|
||||
<td>{{ pl.setsWon }}:{{ pl.setsLost }}</td>
|
||||
<td>
|
||||
@@ -513,5 +513,13 @@ export default {
|
||||
.merge-pools-actions {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.gave-up-badge {
|
||||
margin-left: 0.35rem;
|
||||
padding: 0.1rem 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="participant-seeded-cell">{{ $t('tournaments.seeded') }}</th>
|
||||
<th class="participant-gave-up-cell">{{ $t('tournaments.gaveUp') }}</th>
|
||||
<th class="participant-name">{{ $t('tournaments.name') }}</th>
|
||||
<th v-if="allowsExternal" class="participant-gender-cell">{{ $t('members.gender') }}</th>
|
||||
<th v-if="allowsExternal" class="participant-club-cell">{{ $t('tournaments.club') }}</th>
|
||||
@@ -110,6 +111,11 @@
|
||||
<input type="checkbox" :checked="participant.seeded" @change="$emit('update-participant-seeded', participant, $event)" />
|
||||
</label>
|
||||
</td>
|
||||
<td class="participant-gave-up-cell">
|
||||
<label class="gave-up-checkbox-label" :title="$t('tournaments.gaveUpHint')">
|
||||
<input type="checkbox" :checked="participant.gaveUp" @change="$emit('update-participant-gave-up', participant, $event)" />
|
||||
</label>
|
||||
</td>
|
||||
<td class="participant-name">
|
||||
<template v-if="participant.member">
|
||||
{{ participant.member.firstName || $t('tournaments.unknown') }}
|
||||
@@ -168,6 +174,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="participant-seeded-cell">{{ $t('tournaments.seeded') }}</th>
|
||||
<th class="participant-gave-up-cell">{{ $t('tournaments.gaveUp') }}</th>
|
||||
<th class="participant-name">{{ $t('tournaments.name') }}</th>
|
||||
<th v-if="allowsExternal" class="participant-gender-cell">{{ $t('members.gender') }}</th>
|
||||
<th v-if="allowsExternal" class="participant-club-cell">{{ $t('tournaments.club') }}</th>
|
||||
@@ -186,6 +193,11 @@
|
||||
<input type="checkbox" :checked="participant.seeded" @change="$emit('update-participant-seeded', participant, $event)" />
|
||||
</label>
|
||||
</td>
|
||||
<td class="participant-gave-up-cell">
|
||||
<label class="gave-up-checkbox-label" :title="$t('tournaments.gaveUpHint')">
|
||||
<input type="checkbox" :checked="participant.gaveUp" @change="$emit('update-participant-gave-up', participant, $event)" />
|
||||
</label>
|
||||
</td>
|
||||
<td class="participant-name">
|
||||
<template v-if="participant.member">
|
||||
{{ participant.member.firstName || $t('tournaments.unknown') }}
|
||||
@@ -422,6 +434,7 @@ export default {
|
||||
'update:newExternalParticipant',
|
||||
'add-external-participant',
|
||||
'update-participant-seeded',
|
||||
'update-participant-gave-up',
|
||||
'update-participant-group',
|
||||
'update-participant-class',
|
||||
'remove-participant',
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
<template v-if="m.result === 'BYE'">
|
||||
BYE
|
||||
</template>
|
||||
<template v-else-if="matchHasGaveUp(m)">
|
||||
<span class="result-text gave-up-result">{{ formatResult(m) }}</span>
|
||||
<span v-if="m.player1?.gaveUp && m.player2?.gaveUp" class="gave-up-badge-small">({{ $t('tournaments.gaveUp') }})</span>
|
||||
</template>
|
||||
<template v-else-if="!m.isFinished">
|
||||
<template v-for="r in m.tournamentResults" :key="r.set">
|
||||
<template v-if="isEditing(m, r.set)">
|
||||
@@ -86,10 +90,15 @@
|
||||
{{ getSetsString(m) }}
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="!m.isFinished" @click="$emit('finish-match', m)">Abschließen</button>
|
||||
<button v-else @click="$emit('reopen-match', m)" class="btn-correct">Korrigieren</button>
|
||||
<button v-if="!m.isFinished && !m.isActive" @click.stop="$emit('set-match-active', m, true)" class="btn-live" title="Als laufend markieren">▶️</button>
|
||||
<button v-if="!m.isFinished && m.isActive" @click.stop="$emit('set-match-active', m, false)" class="btn-live active" title="Laufend-Markierung entfernen">⏸️</button>
|
||||
<template v-if="matchHasGaveUp(m)">
|
||||
<span class="no-edit-hint">{{ $t('tournaments.gaveUp') }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button v-if="!m.isFinished" @click="$emit('finish-match', m)">Abschließen</button>
|
||||
<button v-else @click="$emit('reopen-match', m)" class="btn-correct">Korrigieren</button>
|
||||
<button v-if="!m.isFinished && !m.isActive" @click.stop="$emit('set-match-active', m, true)" class="btn-live" title="Als laufend markieren">▶️</button>
|
||||
<button v-if="!m.isFinished && m.isActive" @click.stop="$emit('set-match-active', m, false)" class="btn-live active" title="Laufend-Markierung entfernen">⏸️</button>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -502,6 +511,9 @@ export default {
|
||||
isLastResult(match, result) {
|
||||
const arr = match.tournamentResults || [];
|
||||
return arr.length > 0 && arr[arr.length - 1].set === result.set;
|
||||
},
|
||||
matchHasGaveUp(match) {
|
||||
return match.gaveUpMatch || match.player1?.gaveUp || match.player2?.gaveUp;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -555,6 +567,19 @@ export default {
|
||||
color: #856404 !important;
|
||||
}
|
||||
|
||||
.gave-up-result {
|
||||
color: #626262;
|
||||
}
|
||||
.gave-up-badge-small {
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
color: #721c24;
|
||||
}
|
||||
.no-edit-hint {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.active-match:hover {
|
||||
background-color: #ffe69c !important;
|
||||
}
|
||||
|
||||
@@ -595,6 +595,8 @@
|
||||
"noPlacementsYet": "Noch keine Platzierungen vorhanden.",
|
||||
"participants": "Teilnehmer",
|
||||
"seeded": "Gesetzt",
|
||||
"gaveUp": "Aufgegeben",
|
||||
"gaveUpHint": "Spieler hat aufgegeben – alle Spiele zählen für den Gegner (11:0) bzw. 0:0 bei beiden aufgegeben.",
|
||||
"club": "Verein",
|
||||
"class": "Klasse",
|
||||
"group": "Gruppe",
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
@update:newExternalParticipant="newExternalParticipant = $event"
|
||||
@add-external-participant="addExternalParticipant()"
|
||||
@update-participant-seeded="updateParticipantSeeded"
|
||||
@update-participant-gave-up="updateParticipantGaveUp"
|
||||
@update-participant-group="updateParticipantGroup"
|
||||
@update-participant-class="updateParticipantClass"
|
||||
@remove-participant="removeParticipant"
|
||||
@@ -485,6 +486,7 @@ export default {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
seeded: p.seeded || false,
|
||||
gaveUp: p.gaveUp || false,
|
||||
position: p.position || 0,
|
||||
points: p.points || 0,
|
||||
setsWon: p.setsWon || 0,
|
||||
@@ -1233,6 +1235,7 @@ export default {
|
||||
this.participants = (Array.isArray(pRes.data) ? pRes.data : []).map(p => ({
|
||||
...p,
|
||||
seeded: p.seeded || false,
|
||||
gaveUp: p.gaveUp || false,
|
||||
groupNumber: p.groupId ? (groupIdToNumberMap[p.groupId] || null) : null
|
||||
}));
|
||||
|
||||
@@ -1275,6 +1278,7 @@ export default {
|
||||
this.externalParticipants = (Array.isArray(allExternalParticipants) ? allExternalParticipants : []).map(p => ({
|
||||
...p,
|
||||
seeded: p.seeded || false,
|
||||
gaveUp: p.gaveUp || false,
|
||||
isExternal: true,
|
||||
groupNumber: p.groupId ? (groupIdToNumberMap[p.groupId] || null) : null
|
||||
}));
|
||||
@@ -2297,6 +2301,21 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async updateParticipantGaveUp(participant, event) {
|
||||
const gaveUp = event.target.checked;
|
||||
participant.gaveUp = gaveUp;
|
||||
try {
|
||||
if (this.allowsExternal && participant.isExternal) {
|
||||
await apiClient.put(`/tournament/external-participant/${this.currentClub}/${this.selectedDate}/${participant.id}/gave-up`, { gaveUp });
|
||||
} else {
|
||||
await apiClient.put(`/tournament/participant/${this.currentClub}/${this.selectedDate}/${participant.id}/gave-up`, { gaveUp });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Aufgabe-Status:', error);
|
||||
await this.loadTournamentData();
|
||||
}
|
||||
},
|
||||
|
||||
async updateParticipantGroup(participant, event) {
|
||||
const value = event.target.value;
|
||||
const groupNumber = value === '' || value === 'null' ? null : parseInt(value);
|
||||
@@ -3467,7 +3486,9 @@ button {
|
||||
|
||||
/* Synchronisiere Spaltenbreiten zwischen Header und Body */
|
||||
.participants-table-header .participant-seeded-cell,
|
||||
.participants-table-body .participant-seeded-cell {
|
||||
.participants-table-body .participant-seeded-cell,
|
||||
.participants-table-header .participant-gave-up-cell,
|
||||
.participants-table-body .participant-gave-up-cell {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
@@ -3500,10 +3521,17 @@ button {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.participant-seeded-cell {
|
||||
.participant-seeded-cell,
|
||||
.participant-gave-up-cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.gave-up-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.seeded-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user