feat(tournament): implement class merging and pool management features
- Added mergeClassesIntoPool and resetPool functions in tournamentService to handle merging classes into a common pool and resetting pool assignments. - Introduced new API routes for merging and resetting pools in tournamentRoutes. - Enhanced TournamentGroupsTab component with UI for merging classes, including selection for source and target classes, strategy options, and out-of-competition settings. - Updated localization files to include new strings related to class merging functionality. - Modified TournamentTab to handle merge pool events and manage API interactions for merging classes.
This commit is contained in:
@@ -96,6 +96,117 @@ function nextPowerOfTwo(n) {
|
||||
|
||||
const THIRD_PLACE_ROUND = 'Spiel um Platz 3';
|
||||
class TournamentService {
|
||||
/**
|
||||
* Klassen in einem gemeinsamen Pool zusammenführen.
|
||||
* strategy: 'singleGroup' (alle A-Spieler in eine neue Gruppe der Zielklasse) |
|
||||
* 'distribute' (A-Spieler gleichmäßig auf Ziel-Gruppen verteilen)
|
||||
*/
|
||||
async mergeClassesIntoPool(userToken, clubId, tournamentId, sourceClassId, targetClassId, strategy = 'distribute', outOfCompetitionForSource = false) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const tournament = await Tournament.findByPk(tournamentId);
|
||||
if (!tournament || tournament.clubId != clubId) {
|
||||
throw new Error('Turnier nicht gefunden');
|
||||
}
|
||||
if (!sourceClassId || !targetClassId) {
|
||||
throw new Error('sourceClassId und targetClassId sind erforderlich');
|
||||
}
|
||||
if (String(sourceClassId) === String(targetClassId)) {
|
||||
throw new Error('Quelle und Zielklasse dürfen nicht identisch sein');
|
||||
}
|
||||
|
||||
// Erzeuge eine neue poolId (einfacher Ansatz)
|
||||
const maxExisting = await TournamentGroup.max('poolId', { where: { tournamentId } });
|
||||
const poolId = Number.isFinite(Number(maxExisting)) ? Number(maxExisting) + 1 : 1;
|
||||
|
||||
// Lade Ziel-Gruppen
|
||||
const targetGroups = await TournamentGroup.findAll({
|
||||
where: { tournamentId, classId: targetClassId },
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
if (!targetGroups || targetGroups.length === 0) {
|
||||
throw new Error('Zielklasse hat keine Gruppen. Bitte zuerst Gruppen anlegen.');
|
||||
}
|
||||
|
||||
// Markiere Zielgruppen mit poolId
|
||||
for (const g of targetGroups) {
|
||||
g.poolId = poolId;
|
||||
await g.save();
|
||||
}
|
||||
|
||||
// Lade Teilnehmer der Quellklasse (intern + extern)
|
||||
const sourceInternals = await TournamentMember.findAll({ where: { tournamentId, classId: sourceClassId } });
|
||||
const sourceExternals = await ExternalTournamentParticipant.findAll({ where: { tournamentId, classId: sourceClassId } });
|
||||
|
||||
// Optional: „außer Konkurrenz“ für Source markieren
|
||||
if (outOfCompetitionForSource) {
|
||||
await Promise.all([
|
||||
...sourceInternals.map(m => { m.outOfCompetition = true; return m.save(); }),
|
||||
...sourceExternals.map(e => { e.outOfCompetition = true; return e.save(); })
|
||||
]);
|
||||
}
|
||||
|
||||
if (strategy === 'singleGroup') {
|
||||
// Lege eine zusätzliche Gruppe in der Zielklasse an, nur für Source-Spieler
|
||||
// Bestimme groupNumber: max + 1
|
||||
const maxGroupNumber = await TournamentGroup.max('id', { where: { tournamentId, classId: targetClassId } });
|
||||
const newGroup = await TournamentGroup.create({
|
||||
stageId: null,
|
||||
tournamentId,
|
||||
classId: targetClassId,
|
||||
poolId
|
||||
});
|
||||
|
||||
// Weisen wir allen Source-Spielern diese neue Gruppe zu
|
||||
for (const m of sourceInternals) {
|
||||
m.groupId = newGroup.id;
|
||||
m.classId = targetClassId; // bleiben sie in Quelle? Für Pool reicht Zielgruppen-Zuordnung; Klasse lassen wir unverändert
|
||||
await m.save();
|
||||
}
|
||||
for (const e of sourceExternals) {
|
||||
e.groupId = newGroup.id;
|
||||
e.classId = targetClassId;
|
||||
await e.save();
|
||||
}
|
||||
} else {
|
||||
// distribute: Gleichmäßig über bestehende Zielgruppen verteilen
|
||||
const cyclic = [...targetGroups];
|
||||
let idx = 0;
|
||||
const assignNext = () => {
|
||||
const g = cyclic[idx % cyclic.length];
|
||||
idx++;
|
||||
return g;
|
||||
};
|
||||
for (const m of sourceInternals) {
|
||||
const g = assignNext();
|
||||
m.groupId = g.id;
|
||||
// Klasse beim Teilnehmer unverändert lassen; Gruppenspiele laufen trotzdem im Pool
|
||||
await m.save();
|
||||
}
|
||||
for (const e of sourceExternals) {
|
||||
const g = assignNext();
|
||||
e.groupId = g.id;
|
||||
await e.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: vorhandene Gruppenspiele neu erstellen, damit die Verteilung greift
|
||||
await this.createGroupMatches(userToken, clubId, tournamentId, null);
|
||||
return { poolId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Pool zurücksetzen: Entfernt poolId von Gruppen des Pools.
|
||||
* (Vereinfachter Reset – Teilnehmer bleiben in Gruppen; Matches kann der Nutzer löschen.)
|
||||
*/
|
||||
async resetPool(userToken, clubId, tournamentId, poolId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
if (!poolId) throw new Error('poolId ist erforderlich');
|
||||
const groups = await TournamentGroup.findAll({ where: { tournamentId, poolId } });
|
||||
for (const g of groups) {
|
||||
g.poolId = null;
|
||||
await g.save();
|
||||
}
|
||||
}
|
||||
// -------- Multi-Stage (Runden) V1 --------
|
||||
async getTournamentStages(userToken, clubId, tournamentId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
Reference in New Issue
Block a user