Add participant assignment to groups functionality
Implement a new endpoint to assign participants to specific groups within tournaments. This includes the addition of the `assignParticipantToGroup` function in the tournament controller, which handles the assignment logic and emits relevant events. Update the tournament routes to include this new functionality. Enhance the tournament service to manage group assignments and ensure proper statistics are calculated for participants. Additionally, update the frontend to support adding participants, including external ones, and reflect changes in the UI for group assignments.
This commit is contained in:
@@ -253,6 +253,28 @@ export const manualAssignGroups = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const assignParticipantToGroup = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId, groupNumber, isExternal } = req.body;
|
||||
|
||||
try {
|
||||
const groups = await tournamentService.assignParticipantToGroup(
|
||||
token,
|
||||
clubId,
|
||||
tournamentId,
|
||||
participantId,
|
||||
groupNumber,
|
||||
isExternal || false
|
||||
);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(groups);
|
||||
} catch (error) {
|
||||
console.error('Error in assignParticipantToGroup:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const resetGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
deleteTournamentClass,
|
||||
updateParticipantClass,
|
||||
createGroupsPerClass,
|
||||
assignParticipantToGroup,
|
||||
} from '../controllers/tournamentController.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
|
||||
@@ -55,12 +56,13 @@ router.post("/match/reopen", authenticate, reopenMatch);
|
||||
router.post('/match/finish', authenticate, finishMatch);
|
||||
router.put('/match/:clubId/:tournamentId/:matchId/active', authenticate, setMatchActive);
|
||||
router.get('/matches/:clubId/:tournamentId', authenticate, getTournamentMatches);
|
||||
router.put('/:clubId/:tournamentId', authenticate, updateTournament);
|
||||
router.get('/:clubId/:tournamentId', authenticate, getTournament);
|
||||
router.get('/:clubId', authenticate, getTournaments);
|
||||
router.post('/knockout', authenticate, startKnockout);
|
||||
router.delete("/matches/knockout", authenticate, deleteKnockoutMatches);
|
||||
router.post('/groups/manual', authenticate, manualAssignGroups);
|
||||
router.put('/participant/group', authenticate, assignParticipantToGroup); // Muss VOR /:clubId/:tournamentId stehen!
|
||||
router.put('/:clubId/:tournamentId', authenticate, updateTournament);
|
||||
router.get('/:clubId/:tournamentId', authenticate, getTournament);
|
||||
router.get('/:clubId', authenticate, getTournaments);
|
||||
router.post('/', authenticate, addTournament);
|
||||
|
||||
// Externe Teilnehmer
|
||||
|
||||
@@ -497,6 +497,12 @@ class TournamentService {
|
||||
]
|
||||
});
|
||||
|
||||
// Lade alle Gruppen-Matches mit Results für Rankings
|
||||
const groupMatches = await TournamentMatch.findAll({
|
||||
where: { tournamentId, round: 'group', isFinished: true },
|
||||
include: [{ model: TournamentResult, as: 'tournamentResults' }]
|
||||
});
|
||||
|
||||
// Gruppiere nach Klassen und nummeriere Gruppen pro Klasse
|
||||
const groupsByClass = {};
|
||||
groups.forEach(g => {
|
||||
@@ -510,25 +516,156 @@ class TournamentService {
|
||||
const result = [];
|
||||
for (const [classKey, classGroups] of Object.entries(groupsByClass)) {
|
||||
classGroups.forEach((g, idx) => {
|
||||
const internalParticipants = (g.tournamentGroupMembers || []).map(m => ({
|
||||
id: m.id,
|
||||
name: `${m.member.firstName} ${m.member.lastName}`,
|
||||
seeded: m.seeded || false,
|
||||
isExternal: false
|
||||
}));
|
||||
// Berechne Rankings für diese Gruppe
|
||||
const stats = {};
|
||||
|
||||
const externalParticipants = (g.externalGroupMembers || []).map(m => ({
|
||||
id: m.id,
|
||||
name: `${m.firstName} ${m.lastName}`,
|
||||
seeded: m.seeded || false,
|
||||
isExternal: true
|
||||
}));
|
||||
// Interne Teilnehmer
|
||||
for (const tm of g.tournamentGroupMembers || []) {
|
||||
stats[tm.id] = {
|
||||
id: tm.id,
|
||||
name: `${tm.member.firstName} ${tm.member.lastName}`,
|
||||
seeded: tm.seeded || false,
|
||||
isExternal: false,
|
||||
points: 0,
|
||||
setsWon: 0,
|
||||
setsLost: 0,
|
||||
pointsWon: 0,
|
||||
pointsLost: 0,
|
||||
pointRatio: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Externe Teilnehmer
|
||||
for (const ext of g.externalGroupMembers || []) {
|
||||
stats[ext.id] = {
|
||||
id: ext.id,
|
||||
name: `${ext.firstName} ${ext.lastName}`,
|
||||
seeded: ext.seeded || false,
|
||||
isExternal: true,
|
||||
points: 0,
|
||||
setsWon: 0,
|
||||
setsLost: 0,
|
||||
pointsWon: 0,
|
||||
pointsLost: 0,
|
||||
pointRatio: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Berechne Statistiken aus Matches
|
||||
for (const m of groupMatches.filter(m => m.groupId === g.id)) {
|
||||
if (!stats[m.player1Id] || !stats[m.player2Id]) continue;
|
||||
const [s1, s2] = m.result.split(':').map(n => parseInt(n, 10));
|
||||
|
||||
if (s1 > s2) {
|
||||
stats[m.player1Id].points += 1; // Sieger bekommt +1
|
||||
stats[m.player2Id].points -= 1; // Verlierer bekommt -1
|
||||
} else if (s2 > s1) {
|
||||
stats[m.player2Id].points += 1; // Sieger bekommt +1
|
||||
stats[m.player1Id].points -= 1; // Verlierer bekommt -1
|
||||
}
|
||||
|
||||
stats[m.player1Id].setsWon += s1;
|
||||
stats[m.player1Id].setsLost += s2;
|
||||
stats[m.player2Id].setsWon += s2;
|
||||
stats[m.player2Id].setsLost += s1;
|
||||
|
||||
// Berechne gespielte Punkte aus tournamentResults
|
||||
if (m.tournamentResults && m.tournamentResults.length > 0) {
|
||||
let p1Points = 0, p2Points = 0;
|
||||
for (const r of m.tournamentResults) {
|
||||
p1Points += r.pointsPlayer1 || 0;
|
||||
p2Points += r.pointsPlayer2 || 0;
|
||||
}
|
||||
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
|
||||
Object.values(stats).forEach(s => {
|
||||
const totalPoints = s.pointsWon + s.pointsLost;
|
||||
s.pointRatio = totalPoints > 0 ? s.pointsWon / totalPoints : 0;
|
||||
s.setDiff = s.setsWon - s.setsLost;
|
||||
s.pointsDiff = s.pointsWon - s.pointsLost; // Absolute Differenz der gespielten Punkte
|
||||
});
|
||||
|
||||
// Sortiere nach: Punkte -> Satzverhältnis -> mehr gewonnene Sätze -> absolute Punktdifferenz -> mehr erzielte Spielpunkte -> direkter Vergleich
|
||||
const ranked = Object.values(stats).sort((a, b) => {
|
||||
// 1. Beste Punkte
|
||||
if (b.points !== a.points) return b.points - a.points;
|
||||
// 2. Besseres Satzverhältnis
|
||||
if (b.setDiff !== a.setDiff) return b.setDiff - a.setDiff;
|
||||
// 3. Bei Satzgleichheit: Wer mehr Sätze gewonnen hat
|
||||
if (b.setsWon !== a.setsWon) return b.setsWon - a.setsWon;
|
||||
// 4. Bessere absolute Differenz der gespielten Punkte (höhere Differenz zuerst)
|
||||
if (b.pointsDiff !== a.pointsDiff) return b.pointsDiff - a.pointsDiff;
|
||||
// 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 =>
|
||||
m.groupId === g.id &&
|
||||
((m.player1Id === a.id && m.player2Id === b.id) ||
|
||||
(m.player1Id === b.id && m.player2Id === a.id))
|
||||
);
|
||||
if (directMatch) {
|
||||
const [s1, s2] = directMatch.result.split(':').map(n => parseInt(n, 10));
|
||||
const aWon = (directMatch.player1Id === a.id && s1 > s2) ||
|
||||
(directMatch.player2Id === a.id && s2 > s1);
|
||||
if (aWon) return -1; // a hat gewonnen -> a kommt weiter oben
|
||||
return 1; // b hat gewonnen -> b kommt weiter oben
|
||||
}
|
||||
// Fallback: Alphabetisch nach Name
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
|
||||
// Weise Positionen zu, wobei Spieler mit identischen Werten den gleichen Platz bekommen
|
||||
let currentPosition = 1;
|
||||
const participantsWithPosition = ranked.map((p, i) => {
|
||||
if (i > 0) {
|
||||
const prev = ranked[i - 1];
|
||||
// Prüfe, ob alle Sortierkriterien identisch sind
|
||||
const pointsEqual = prev.points === p.points;
|
||||
const setDiffEqual = prev.setDiff === p.setDiff;
|
||||
const setsWonEqual = prev.setsWon === p.setsWon;
|
||||
const pointsDiffEqual = prev.pointsDiff === p.pointsDiff;
|
||||
const pointsWonEqual = prev.pointsWon === p.pointsWon;
|
||||
|
||||
if (pointsEqual && setDiffEqual && setsWonEqual && pointsDiffEqual && pointsWonEqual) {
|
||||
// Prüfe direkten Vergleich
|
||||
const directMatch = groupMatches.find(m =>
|
||||
m.groupId === g.id &&
|
||||
((m.player1Id === prev.id && m.player2Id === p.id) ||
|
||||
(m.player1Id === p.id && m.player2Id === prev.id))
|
||||
);
|
||||
if (!directMatch || directMatch.result.split(':').map(n => +n)[0] === directMatch.result.split(':').map(n => +n)[1]) {
|
||||
// Gleicher Platz wie Vorgänger (unentschieden oder kein direktes Match)
|
||||
return {
|
||||
...p,
|
||||
position: currentPosition,
|
||||
setDiff: p.setDiff,
|
||||
pointsDiff: p.pointsDiff
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// Neuer Platz
|
||||
currentPosition = i + 1;
|
||||
return {
|
||||
...p,
|
||||
position: currentPosition,
|
||||
setDiff: p.setDiff,
|
||||
pointsDiff: p.pointsDiff
|
||||
};
|
||||
});
|
||||
|
||||
result.push({
|
||||
groupId: g.id,
|
||||
classId: g.classId,
|
||||
groupNumber: idx + 1, // Nummer innerhalb der Klasse
|
||||
participants: [...internalParticipants, ...externalParticipants]
|
||||
participants: participantsWithPosition
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -752,7 +889,8 @@ class TournamentService {
|
||||
]
|
||||
});
|
||||
const groupMatches = await TournamentMatch.findAll({
|
||||
where: { tournamentId, round: "group", isFinished: true }
|
||||
where: { tournamentId, round: "group", isFinished: true },
|
||||
include: [{ model: TournamentResult, as: "tournamentResults" }]
|
||||
});
|
||||
|
||||
const qualifiers = [];
|
||||
@@ -760,11 +898,11 @@ class TournamentService {
|
||||
const stats = {};
|
||||
// Interne Teilnehmer
|
||||
for (const tm of g.tournamentGroupMembers || []) {
|
||||
stats[tm.id] = { member: tm, points: 0, setsWon: 0, setsLost: 0, isExternal: false };
|
||||
stats[tm.id] = { member: tm, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: false };
|
||||
}
|
||||
// Externe Teilnehmer
|
||||
for (const ext of g.externalGroupMembers || []) {
|
||||
stats[ext.id] = { member: ext, points: 0, setsWon: 0, setsLost: 0, isExternal: true };
|
||||
stats[ext.id] = { member: ext, points: 0, setsWon: 0, setsLost: 0, pointsWon: 0, pointsLost: 0, isExternal: true };
|
||||
}
|
||||
for (const m of groupMatches.filter(m => m.groupId === g.id)) {
|
||||
if (!stats[m.player1Id] || !stats[m.player2Id]) continue;
|
||||
@@ -780,13 +918,55 @@ class TournamentService {
|
||||
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) {
|
||||
p1Points += r.pointsPlayer1 || 0;
|
||||
p2Points += r.pointsPlayer2 || 0;
|
||||
}
|
||||
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
|
||||
Object.values(stats).forEach(s => {
|
||||
const totalPoints = s.pointsWon + s.pointsLost;
|
||||
s.pointRatio = totalPoints > 0 ? s.pointsWon / totalPoints : 0;
|
||||
s.pointsDiff = s.pointsWon - s.pointsLost; // Absolute Differenz der gespielten Punkte
|
||||
});
|
||||
|
||||
const ranked = Object.values(stats).sort((a, b) => {
|
||||
const diffA = a.setsWon - a.setsLost;
|
||||
const diffB = b.setsWon - b.setsLost;
|
||||
// 1. Beste Punkte
|
||||
if (b.points !== a.points) return b.points - a.points;
|
||||
// 2. Besseres Satzverhältnis
|
||||
if (diffB !== diffA) return diffB - diffA;
|
||||
// 3. Bei Satzgleichheit: Wer mehr Sätze gewonnen hat
|
||||
if (b.setsWon !== a.setsWon) return b.setsWon - a.setsWon;
|
||||
// 4. Bessere absolute Differenz der gespielten Punkte (höhere Differenz zuerst)
|
||||
if (b.pointsDiff !== a.pointsDiff) return b.pointsDiff - a.pointsDiff;
|
||||
// 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 =>
|
||||
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) ||
|
||||
(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
|
||||
}
|
||||
// Fallback: Nach ID
|
||||
return a.member.id - b.member.id;
|
||||
});
|
||||
// Füge classId zur Gruppe hinzu
|
||||
@@ -903,8 +1083,25 @@ class TournamentService {
|
||||
throw new Error('Keine Teilnehmer zum Verteilen');
|
||||
}
|
||||
|
||||
// 2) Bestimme, wie viele Gruppen wir anlegen
|
||||
let groupCount;
|
||||
// 2) Hole bestehende Gruppen und erstelle nur neue, wenn nötig
|
||||
const existingGroups = await TournamentGroup.findAll({
|
||||
where: { tournamentId },
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
|
||||
// Berechne die maximale groupNumber aus den assignments
|
||||
const maxGroupNumber = assignments.reduce((max, a) => {
|
||||
if (a.groupNumber != null && a.groupNumber > max) {
|
||||
return a.groupNumber;
|
||||
}
|
||||
return max;
|
||||
}, 0);
|
||||
|
||||
// Erstelle nur neue Gruppen, wenn die maximale groupNumber größer ist als die Anzahl der bestehenden Gruppen
|
||||
const existingGroupCount = existingGroups.length;
|
||||
let groupCount = Math.max(existingGroupCount, maxGroupNumber);
|
||||
|
||||
// Wenn numberOfGroups oder maxGroupSize gesetzt sind, verwende diese
|
||||
if (numberOfGroups != null) {
|
||||
groupCount = Number(numberOfGroups);
|
||||
if (isNaN(groupCount) || groupCount < 1) {
|
||||
@@ -915,21 +1112,17 @@ class TournamentService {
|
||||
if (isNaN(sz) || sz < 1) {
|
||||
throw new Error('Ungültige maximale Gruppengröße');
|
||||
}
|
||||
groupCount = Math.ceil(totalMembers / sz);
|
||||
} else {
|
||||
// Fallback auf im Turnier gespeicherte Anzahl
|
||||
groupCount = tournament.numberOfGroups;
|
||||
if (!groupCount || groupCount < 1) {
|
||||
throw new Error('Anzahl Gruppen nicht definiert');
|
||||
}
|
||||
groupCount = Math.max(groupCount, Math.ceil(totalMembers / sz));
|
||||
}
|
||||
|
||||
console.log(`[manualAssignGroups] Existing groups: ${existingGroupCount}, maxGroupNumber: ${maxGroupNumber}, target groupCount: ${groupCount}`);
|
||||
|
||||
// 3) Alte Gruppen löschen und neue anlegen
|
||||
await TournamentGroup.destroy({ where: { tournamentId } });
|
||||
const createdGroups = [];
|
||||
for (let i = 0; i < groupCount; i++) {
|
||||
// 3) Erstelle nur fehlende Gruppen
|
||||
const createdGroups = [...existingGroups];
|
||||
for (let i = existingGroupCount; i < groupCount; i++) {
|
||||
const grp = await TournamentGroup.create({ tournamentId });
|
||||
createdGroups.push(grp);
|
||||
console.log(`[manualAssignGroups] Created new group ${i + 1} with id ${grp.id}`);
|
||||
}
|
||||
|
||||
// 4) Mapping von UI‑Nummer (1…groupCount) auf reale DB‑ID
|
||||
@@ -937,77 +1130,142 @@ class TournamentService {
|
||||
createdGroups.forEach((grp, idx) => {
|
||||
groupMap[idx + 1] = grp.id;
|
||||
});
|
||||
|
||||
console.log(`[manualAssignGroups] Group mapping:`, groupMap);
|
||||
|
||||
// 5) Teilnehmer updaten (sowohl interne als auch externe)
|
||||
await Promise.all(
|
||||
assignments.map(async ({ participantId, groupNumber }) => {
|
||||
const dbGroupId = groupMap[groupNumber];
|
||||
if (!dbGroupId) {
|
||||
assignments.map(async ({ participantId, groupNumber, isExternal }) => {
|
||||
console.log(`[manualAssignGroups] Processing assignment: participantId=${participantId}, groupNumber=${groupNumber}, isExternal=${isExternal}`);
|
||||
console.log(`[manualAssignGroups] Group map:`, JSON.stringify(groupMap));
|
||||
|
||||
// Wenn groupNumber null ist, entferne den Teilnehmer aus der Gruppe (groupId = null)
|
||||
const dbGroupId = groupNumber != null ? groupMap[groupNumber] : null;
|
||||
console.log(`[manualAssignGroups] Calculated dbGroupId: ${dbGroupId} (from groupNumber ${groupNumber})`);
|
||||
|
||||
if (groupNumber != null && !dbGroupId) {
|
||||
throw new Error(`Ungültige Gruppen‑Nummer: ${groupNumber}`);
|
||||
}
|
||||
|
||||
// Prüfe zuerst, ob es ein interner Teilnehmer ist
|
||||
const internalMember = await TournamentMember.findOne({
|
||||
console.log(`[manualAssignGroups] Updating participant ${participantId}, isExternal: ${isExternal}, groupNumber: ${groupNumber}, dbGroupId: ${dbGroupId}`);
|
||||
|
||||
// Prüfe zuerst, ob es ein interner Teilnehmer ist (wenn isExternal nicht explizit true ist)
|
||||
if (isExternal !== true) {
|
||||
const internalMember = await TournamentMember.findOne({
|
||||
where: { id: participantId, tournamentId }
|
||||
});
|
||||
|
||||
if (internalMember) {
|
||||
// Interner Teilnehmer
|
||||
console.log(`[manualAssignGroups] Updating internal member id=${participantId} (memberId=${internalMember.memberId}) to groupId ${dbGroupId}`);
|
||||
await TournamentMember.update(
|
||||
{ groupId: dbGroupId },
|
||||
{ where: { id: participantId, tournamentId } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Versuche externen Teilnehmer
|
||||
const externalMember = await ExternalTournamentParticipant.findOne({
|
||||
where: { id: participantId, tournamentId }
|
||||
});
|
||||
|
||||
if (internalMember) {
|
||||
// Interner Teilnehmer
|
||||
return TournamentMember.update(
|
||||
if (externalMember) {
|
||||
// Externer Teilnehmer
|
||||
console.log(`[manualAssignGroups] Updating external member id=${participantId} to groupId ${dbGroupId}`);
|
||||
await ExternalTournamentParticipant.update(
|
||||
{ groupId: dbGroupId },
|
||||
{ where: { id: participantId, tournamentId } }
|
||||
);
|
||||
} else {
|
||||
// Versuche externen Teilnehmer
|
||||
const externalMember = await ExternalTournamentParticipant.findOne({
|
||||
where: { id: participantId, tournamentId }
|
||||
});
|
||||
|
||||
if (externalMember) {
|
||||
// Externer Teilnehmer
|
||||
return ExternalTournamentParticipant.update(
|
||||
{ groupId: dbGroupId },
|
||||
{ where: { id: participantId, tournamentId } }
|
||||
);
|
||||
} else {
|
||||
throw new Error(`Teilnehmer mit ID ${participantId} nicht gefunden`);
|
||||
}
|
||||
throw new Error(`Teilnehmer mit ID ${participantId} nicht gefunden`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 6) Ergebnis zurückliefern wie getGroupsWithParticipants
|
||||
const groups = await TournamentGroup.findAll({
|
||||
// 6) Ergebnis zurückliefern wie getGroupsWithParticipants (mit vollständigen Rankings)
|
||||
// Verwende die gleiche Methode wie getGroupsWithParticipants, um konsistente Daten zu liefern
|
||||
return await this.getGroupsWithParticipants(userToken, clubId, tournamentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordne einen einzelnen Teilnehmer einer Gruppe zu
|
||||
* @param {string} userToken - User authentication token
|
||||
* @param {number} clubId - Club ID
|
||||
* @param {number} tournamentId - Tournament ID
|
||||
* @param {number} participantId - Participant ID
|
||||
* @param {number|null} groupNumber - Group number (1-based) or null to remove from group
|
||||
* @param {boolean} isExternal - Whether the participant is external
|
||||
* @returns {Promise<Object>} Updated groups with participants
|
||||
*/
|
||||
async assignParticipantToGroup(userToken, clubId, tournamentId, participantId, groupNumber, isExternal = false) {
|
||||
console.log(`[assignParticipantToGroup] Called with: participantId=${participantId}, groupNumber=${groupNumber}, isExternal=${isExternal}, tournamentId=${tournamentId}, clubId=${clubId}`);
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
|
||||
// Hole bestehende Gruppen
|
||||
const existingGroups = await TournamentGroup.findAll({
|
||||
where: { tournamentId },
|
||||
include: [{
|
||||
model: TournamentMember,
|
||||
as: 'tournamentGroupMembers',
|
||||
include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }]
|
||||
}, {
|
||||
model: ExternalTournamentParticipant,
|
||||
as: 'externalGroupMembers'
|
||||
}],
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
console.log(`[assignParticipantToGroup] Found ${existingGroups.length} existing groups`);
|
||||
|
||||
return groups.map(g => {
|
||||
const internalParticipants = (g.tournamentGroupMembers || []).map(m => ({
|
||||
id: m.id,
|
||||
name: `${m.member.firstName} ${m.member.lastName}`,
|
||||
isExternal: false
|
||||
}));
|
||||
// Berechne dbGroupId
|
||||
let dbGroupId = null;
|
||||
if (groupNumber != null) {
|
||||
// Stelle sicher, dass genug Gruppen existieren
|
||||
if (groupNumber > existingGroups.length) {
|
||||
console.log(`[assignParticipantToGroup] Creating ${groupNumber - existingGroups.length} new groups`);
|
||||
// Erstelle fehlende Gruppen
|
||||
for (let i = existingGroups.length; i < groupNumber; i++) {
|
||||
const grp = await TournamentGroup.create({ tournamentId });
|
||||
existingGroups.push(grp);
|
||||
}
|
||||
}
|
||||
// Mapping von groupNumber (1-based) zu groupId
|
||||
dbGroupId = existingGroups[groupNumber - 1].id;
|
||||
console.log(`[assignParticipantToGroup] Mapped groupNumber ${groupNumber} to dbGroupId ${dbGroupId}`);
|
||||
}
|
||||
|
||||
// Aktualisiere den Teilnehmer
|
||||
if (isExternal) {
|
||||
const externalMember = await ExternalTournamentParticipant.findOne({
|
||||
where: { id: participantId, tournamentId }
|
||||
});
|
||||
|
||||
const externalParticipants = (g.externalGroupMembers || []).map(m => ({
|
||||
id: m.id,
|
||||
name: `${m.firstName} ${m.lastName}`,
|
||||
isExternal: true
|
||||
}));
|
||||
if (!externalMember) {
|
||||
throw new Error(`Externer Teilnehmer mit ID ${participantId} nicht gefunden`);
|
||||
}
|
||||
|
||||
return {
|
||||
groupId: g.id,
|
||||
participants: [...internalParticipants, ...externalParticipants]
|
||||
};
|
||||
});
|
||||
console.log(`[assignParticipantToGroup] Updating external member ${participantId} to groupId ${dbGroupId}`);
|
||||
await ExternalTournamentParticipant.update(
|
||||
{ groupId: dbGroupId },
|
||||
{ where: { id: participantId, tournamentId } }
|
||||
);
|
||||
} else {
|
||||
const internalMember = await TournamentMember.findOne({
|
||||
where: { id: participantId, tournamentId }
|
||||
});
|
||||
|
||||
if (!internalMember) {
|
||||
throw new Error(`Interner Teilnehmer mit ID ${participantId} nicht gefunden`);
|
||||
}
|
||||
|
||||
console.log(`[assignParticipantToGroup] Updating internal member ${participantId} (memberId=${internalMember.memberId}) to groupId ${dbGroupId}`);
|
||||
await TournamentMember.update(
|
||||
{ groupId: dbGroupId },
|
||||
{ where: { id: participantId, tournamentId } }
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[assignParticipantToGroup] Successfully updated participant, loading groups with participants...`);
|
||||
// Lade aktualisierte Gruppen mit Teilnehmern zurück
|
||||
return await this.getGroupsWithParticipants(userToken, clubId, tournamentId);
|
||||
}
|
||||
|
||||
// services/tournamentService.js
|
||||
|
||||
@@ -610,6 +610,7 @@
|
||||
"points": "Punkte",
|
||||
"sets": "Satz",
|
||||
"diff": "Diff",
|
||||
"pointsRatio": "Spielpunkte",
|
||||
"livePosition": "Live-Platz",
|
||||
"resetGroupMatches": "Gruppenspiele",
|
||||
"groupMatches": "Gruppenspiele",
|
||||
|
||||
@@ -89,6 +89,47 @@
|
||||
<span class="collapse-icon" :class="{ 'expanded': showParticipants }">▼</span>
|
||||
</div>
|
||||
<div v-show="showParticipants" class="participants-content">
|
||||
<div class="add-participant">
|
||||
<div v-if="allowsExternal" class="add-participant-section">
|
||||
<h5>{{ $t('tournaments.addClubMember') }}</h5>
|
||||
<div class="add-participant-row">
|
||||
<select v-model="selectedMember" class="member-select">
|
||||
<option :value="null">{{ $t('tournaments.selectParticipant') }}</option>
|
||||
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
|
||||
{{ member.firstName }}
|
||||
{{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="addParticipant" class="btn-add">{{ $t('tournaments.add') }}</button>
|
||||
<button v-if="hasTrainingToday && !allowsExternal" @click="loadParticipantsFromTraining" class="training-btn">
|
||||
📅 {{ $t('tournaments.loadFromTraining') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="allowsExternal" class="add-participant-section">
|
||||
<h5>{{ $t('tournaments.addExternalParticipant') }}</h5>
|
||||
<div class="add-participant-row">
|
||||
<input type="text" v-model="newExternalParticipant.firstName" :placeholder="$t('tournaments.firstName')" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.lastName" :placeholder="$t('tournaments.lastName')" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.club" :placeholder="$t('tournaments.club') + ' (' + $t('tournaments.optional') + ')'" class="external-input" />
|
||||
<input type="date" v-model="newExternalParticipant.birthDate" :placeholder="$t('tournaments.birthdate') + ' (' + $t('tournaments.optional') + ')'" class="external-input" />
|
||||
<button @click="addExternalParticipant" class="btn-add">{{ $t('tournaments.add') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!allowsExternal" class="add-participant-row">
|
||||
<select v-model="selectedMember" class="member-select">
|
||||
<option :value="null">{{ $t('tournaments.selectParticipant') }}</option>
|
||||
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
|
||||
{{ member.firstName }}
|
||||
{{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="addParticipant" class="btn-add">Hinzufügen</button>
|
||||
<button v-if="hasTrainingToday" @click="loadParticipantsFromTraining" class="training-btn">
|
||||
📅 Aus Trainingstag laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="participants-table-container">
|
||||
<table class="participants-table participants-table-header">
|
||||
<thead>
|
||||
@@ -153,47 +194,6 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="add-participant">
|
||||
<div v-if="allowsExternal" class="add-participant-section">
|
||||
<h5>{{ $t('tournaments.addClubMember') }}</h5>
|
||||
<div class="add-participant-row">
|
||||
<select v-model="selectedMember" class="member-select">
|
||||
<option :value="null">{{ $t('tournaments.selectParticipant') }}</option>
|
||||
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
|
||||
{{ member.firstName }}
|
||||
{{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="addParticipant" class="btn-add">{{ $t('tournaments.add') }}</button>
|
||||
<button v-if="hasTrainingToday && !allowsExternal" @click="loadParticipantsFromTraining" class="training-btn">
|
||||
📅 {{ $t('tournaments.loadFromTraining') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="allowsExternal" class="add-participant-section">
|
||||
<h5>{{ $t('tournaments.addExternalParticipant') }}</h5>
|
||||
<div class="add-participant-row">
|
||||
<input type="text" v-model="newExternalParticipant.firstName" :placeholder="$t('tournaments.firstName')" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.lastName" :placeholder="$t('tournaments.lastName')" class="external-input" />
|
||||
<input type="text" v-model="newExternalParticipant.club" :placeholder="$t('tournaments.club') + ' (' + $t('tournaments.optional') + ')'" class="external-input" />
|
||||
<input type="date" v-model="newExternalParticipant.birthDate" :placeholder="$t('tournaments.birthdate') + ' (' + $t('tournaments.optional') + ')'" class="external-input" />
|
||||
<button @click="addExternalParticipant" class="btn-add">{{ $t('tournaments.add') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!allowsExternal" class="add-participant-row">
|
||||
<select v-model="selectedMember" class="member-select">
|
||||
<option :value="null">{{ $t('tournaments.selectParticipant') }}</option>
|
||||
<option v-for="member in clubMembers" :key="member.id" :value="member.id">
|
||||
{{ member.firstName }}
|
||||
{{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
<button @click="addParticipant" class="btn-add">Hinzufügen</button>
|
||||
<button v-if="hasTrainingToday" @click="loadParticipantsFromTraining" class="training-btn">
|
||||
📅 Aus Trainingstag laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="isGroupTournament" class="group-controls">
|
||||
@@ -246,6 +246,7 @@
|
||||
<th>{{ $t('tournaments.points') }}</th>
|
||||
<th>{{ $t('tournaments.sets') }}</th>
|
||||
<th>{{ $t('tournaments.diff') }}</th>
|
||||
<th>{{ $t('tournaments.pointsRatio') }}</th>
|
||||
<th v-for="(opponent, idx) in groupRankings[group.groupId]" :key="`opp-${opponent.id}`">
|
||||
G{{ String.fromCharCode(96 + group.groupNumber) }}{{ idx + 1 }}
|
||||
</th>
|
||||
@@ -262,6 +263,9 @@
|
||||
<td>
|
||||
{{ pl.setDiff >= 0 ? '+' + pl.setDiff : pl.setDiff }}
|
||||
</td>
|
||||
<td>
|
||||
{{ pl.pointsWon }}:{{ pl.pointsLost }} ({{ (pl.pointsWon - pl.pointsLost) >= 0 ? '+' + (pl.pointsWon - pl.pointsLost) : (pl.pointsWon - pl.pointsLost) }})
|
||||
</td>
|
||||
<td v-for="(opponent, oppIdx) in groupRankings[group.groupId]"
|
||||
:key="`match-${pl.id}-${opponent.id}`"
|
||||
:class="['match-cell', { 'clickable': idx !== oppIdx, 'active-group-cell': activeGroupCells.includes(`match-${pl.id}-${opponent.id}`), 'diagonal-cell': idx === oppIdx }]"
|
||||
@@ -1044,63 +1048,23 @@ export default {
|
||||
},
|
||||
|
||||
groupRankings() {
|
||||
const byGroup = {};
|
||||
// Die Teilnehmer kommen bereits sortiert vom Backend mit allen benötigten Statistiken
|
||||
const rankings = {};
|
||||
this.groups.forEach(g => {
|
||||
byGroup[g.groupId] = g.participants.map(p => ({
|
||||
rankings[g.groupId] = g.participants.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
seeded: p.seeded || false,
|
||||
points: 0,
|
||||
setsWon: 0,
|
||||
setsLost: 0,
|
||||
setDiff: 0,
|
||||
position: p.position || 0,
|
||||
points: p.points || 0,
|
||||
setsWon: p.setsWon || 0,
|
||||
setsLost: p.setsLost || 0,
|
||||
setDiff: p.setDiff || 0,
|
||||
pointsWon: p.pointsWon || 0,
|
||||
pointsLost: p.pointsLost || 0,
|
||||
pointRatio: p.pointRatio || 0
|
||||
}));
|
||||
});
|
||||
this.matches.forEach(m => {
|
||||
if (!m.isFinished || m.round !== 'group') return;
|
||||
const [s1, s2] = m.result.split(':').map(n => +n);
|
||||
const arr = byGroup[m.groupId];
|
||||
if (!arr) return;
|
||||
const e1 = arr.find(x => x.id === m.player1.id);
|
||||
const e2 = arr.find(x => x.id === m.player2.id);
|
||||
if (!e1 || !e2) return;
|
||||
if (s1 > s2) {
|
||||
e1.points += 1; // Sieger bekommt +1
|
||||
e2.points -= 1; // Verlierer bekommt -1
|
||||
} else if (s2 > s1) {
|
||||
e2.points += 1; // Sieger bekommt +1
|
||||
e1.points -= 1; // Verlierer bekommt -1
|
||||
}
|
||||
e1.setsWon += s1; e1.setsLost += s2;
|
||||
e2.setsWon += s2; e2.setsLost += s1;
|
||||
});
|
||||
const rankings = {};
|
||||
Object.entries(byGroup).forEach(([gid, arr]) => {
|
||||
arr.forEach(p => p.setDiff = p.setsWon - p.setsLost);
|
||||
arr.sort((a, b) => {
|
||||
if (b.points !== a.points) return b.points - a.points;
|
||||
if (b.setDiff !== a.setDiff) return b.setDiff - a.setDiff;
|
||||
if (b.setsWon !== a.setsWon) return b.setsWon - a.setsWon;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
// Weise Positionen zu, wobei Spieler mit identischen Werten den gleichen Platz bekommen
|
||||
let currentPosition = 1;
|
||||
rankings[gid] = arr.map((p, i) => {
|
||||
// Wenn nicht der erste Spieler und die Werte identisch sind, verwende die gleiche Position
|
||||
if (i > 0) {
|
||||
const prev = arr[i - 1];
|
||||
if (prev.points === p.points &&
|
||||
prev.setDiff === p.setDiff &&
|
||||
prev.setsWon === p.setsWon) {
|
||||
// Gleicher Platz wie Vorgänger
|
||||
return { ...p, position: currentPosition };
|
||||
}
|
||||
}
|
||||
// Neuer Platz
|
||||
currentPosition = i + 1;
|
||||
return { ...p, position: currentPosition };
|
||||
});
|
||||
});
|
||||
return rankings;
|
||||
},
|
||||
|
||||
@@ -1335,7 +1299,14 @@ export default {
|
||||
selectedDate: {
|
||||
immediate: true,
|
||||
handler: async function (val) {
|
||||
if (val === 'new') return;
|
||||
if (val === 'new') {
|
||||
// Leere die Matches-Liste, wenn "neues Turnier" ausgewählt wird
|
||||
this.matches = [];
|
||||
this.groups = [];
|
||||
this.showKnockout = false;
|
||||
this.showParticipants = true;
|
||||
return;
|
||||
}
|
||||
await this.loadTournamentData();
|
||||
}
|
||||
}
|
||||
@@ -1507,10 +1478,28 @@ export default {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
});
|
||||
// Lade Gruppen zuerst, damit wir groupNumber initialisieren können
|
||||
const gRes = await apiClient.get('/tournament/groups', {
|
||||
params: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
}
|
||||
});
|
||||
// Setze Gruppen neu, um Vue-Reaktivität sicherzustellen
|
||||
this.groups = [...gRes.data];
|
||||
|
||||
// Erstelle Mapping von groupId zu groupNumber
|
||||
const groupIdToNumberMap = this.groups.reduce((m, g) => {
|
||||
m[g.groupId] = g.groupNumber;
|
||||
return m;
|
||||
}, {});
|
||||
|
||||
// Stelle sicher, dass seeded-Feld vorhanden ist (für alte Einträge)
|
||||
// Initialisiere auch groupNumber basierend auf groupId
|
||||
this.participants = pRes.data.map(p => ({
|
||||
...p,
|
||||
seeded: p.seeded || false
|
||||
seeded: p.seeded || false,
|
||||
groupNumber: p.groupId ? (groupIdToNumberMap[p.groupId] || null) : null
|
||||
}));
|
||||
|
||||
// Lade externe Teilnehmer (nur bei allowsExternal = true)
|
||||
@@ -1523,7 +1512,8 @@ export default {
|
||||
this.externalParticipants = extRes.data.map(p => ({
|
||||
...p,
|
||||
seeded: p.seeded || false,
|
||||
isExternal: true
|
||||
isExternal: true,
|
||||
groupNumber: p.groupId ? (groupIdToNumberMap[p.groupId] || null) : null
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der externen Teilnehmer:', error);
|
||||
@@ -1532,13 +1522,6 @@ export default {
|
||||
} else {
|
||||
this.externalParticipants = [];
|
||||
}
|
||||
const gRes = await apiClient.get('/tournament/groups', {
|
||||
params: {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate
|
||||
}
|
||||
});
|
||||
this.groups = gRes.data;
|
||||
const mRes = await apiClient.get(
|
||||
`/tournament/matches/${this.currentClub}/${this.selectedDate}`
|
||||
);
|
||||
@@ -2288,46 +2271,42 @@ export default {
|
||||
},
|
||||
|
||||
async updateParticipantGroup(participant, event) {
|
||||
const groupNumber = parseInt(event.target.value);
|
||||
const value = event.target.value;
|
||||
const groupNumber = value === '' || value === 'null' ? null : parseInt(value);
|
||||
|
||||
console.log('[updateParticipantGroup] Updating participant:', participant.id, 'to groupNumber:', groupNumber, 'isExternal:', participant.isExternal);
|
||||
|
||||
// Aktualisiere lokal
|
||||
participant.groupNumber = groupNumber;
|
||||
|
||||
// Bereite alle Teilnehmer-Zuordnungen vor (interne und externe, wenn allowsExternal = true)
|
||||
const assignments = this.allowsExternal
|
||||
? [
|
||||
...this.participants.map(p => ({
|
||||
participantId: p.id,
|
||||
groupNumber: p.groupNumber || null,
|
||||
isExternal: false
|
||||
})),
|
||||
...this.externalParticipants.map(p => ({
|
||||
participantId: p.id,
|
||||
groupNumber: p.groupNumber || null,
|
||||
isExternal: true
|
||||
}))
|
||||
]
|
||||
: this.participants.map(p => ({
|
||||
participantId: p.id,
|
||||
groupNumber: p.groupNumber || null
|
||||
}));
|
||||
|
||||
// Sende an Backend
|
||||
// Sende nur diesen einen Teilnehmer an Backend
|
||||
try {
|
||||
await apiClient.post('/tournament/groups/manual', {
|
||||
const response = await apiClient.put('/tournament/participant/group', {
|
||||
clubId: this.currentClub,
|
||||
tournamentId: this.selectedDate,
|
||||
assignments: assignments,
|
||||
numberOfGroups: this.numberOfGroups,
|
||||
maxGroupSize: this.maxGroupSize
|
||||
participantId: participant.id,
|
||||
groupNumber: groupNumber,
|
||||
isExternal: participant.isExternal || false
|
||||
});
|
||||
console.log('[updateParticipantGroup] Response:', response.data);
|
||||
|
||||
// Lade Daten neu, um die aktualisierten Gruppen zu erhalten
|
||||
await this.loadTournamentData();
|
||||
// Verwende die Response-Daten direkt, um die Gruppen zu aktualisieren
|
||||
if (response.data && Array.isArray(response.data)) {
|
||||
// Setze Gruppen neu, um Vue-Reaktivität sicherzustellen
|
||||
this.groups = [...response.data];
|
||||
console.log('[updateParticipantGroup] Updated groups:', this.groups);
|
||||
} else {
|
||||
// Fallback: Lade Daten neu
|
||||
await this.loadTournamentData();
|
||||
}
|
||||
// Force Vue update, um sicherzustellen, dass die Gruppenübersicht aktualisiert wird
|
||||
this.$forceUpdate();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Gruppe:', error);
|
||||
// Bei Fehler: Lade Daten neu
|
||||
// Bei Fehler: Lade Daten neu und setze groupNumber zurück
|
||||
participant.groupNumber = participant.groupId ? this.groups.find(g => g.groupId === participant.groupId)?.groupNumber || null : null;
|
||||
await this.loadTournamentData();
|
||||
this.$forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user