feat(tournament): implement multi-stage tournament support with intermediate and final stages
- Added backend controller for tournament stages with endpoints to get, upsert, and advance stages. - Created database migration for new tables: tournament_stage and tournament_stage_advancement. - Updated models for TournamentStage and TournamentStageAdvancement. - Enhanced frontend components to manage tournament stages, including configuration for intermediate and final rounds. - Implemented logic for saving and advancing tournament stages, including handling of pool rules and third place matches. - Added error handling and loading states in the frontend for better user experience.
This commit is contained in:
@@ -171,10 +171,36 @@ export const getMemberActivities = async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter: explizite Zuordnungen sollen nur dann zählen, wenn
|
||||
// - der Participant keine Gruppe hat UND die Aktivität KEINE Gruppenbindung hat, oder
|
||||
// - die Aktivität keine Gruppenbindung hat, oder
|
||||
// - es eine Gruppenbindung gibt, die zur Gruppe des Participants passt.
|
||||
const filteredMemberActivities = memberActivities.filter((ma) => {
|
||||
if (!ma?.participant || !ma?.activity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const participantGroupId = ma.participant.groupId;
|
||||
const groupActivitiesForActivity = ma.activity.groupActivities || [];
|
||||
|
||||
// Participant ohne Gruppe -> nur Aktivitäten ohne Gruppenbindung zählen
|
||||
if (participantGroupId === null || participantGroupId === undefined) {
|
||||
return !groupActivitiesForActivity.length;
|
||||
}
|
||||
|
||||
// Keine Gruppenbindung -> immer zählen
|
||||
if (!groupActivitiesForActivity.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Gruppenbindung vorhanden -> nur zählen, wenn die Gruppe passt
|
||||
return groupActivitiesForActivity.some((ga) => Number(ga.groupId) === Number(participantGroupId));
|
||||
});
|
||||
|
||||
// 3. Kombiniere beide Listen und entferne Duplikate
|
||||
// Ein Duplikat liegt vor, wenn dieselbe Aktivität für denselben Participant bereits explizit zugeordnet ist
|
||||
const explicitActivityKeys = new Set();
|
||||
memberActivities.forEach(ma => {
|
||||
filteredMemberActivities.forEach(ma => {
|
||||
if (ma.activity && ma.activity.id && ma.participant && ma.participant.id) {
|
||||
// Erstelle einen eindeutigen Schlüssel: activityId-participantId
|
||||
const key = `${ma.activity.id}-${ma.participant.id}`;
|
||||
@@ -192,7 +218,7 @@ export const getMemberActivities = async (req, res) => {
|
||||
});
|
||||
|
||||
// Kombiniere beide Listen
|
||||
const allActivities = [...memberActivities, ...uniqueGroupActivities];
|
||||
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities];
|
||||
|
||||
// Group activities by name and count occurrences
|
||||
// Verwende einen Set pro Aktivität, um eindeutige Datum-Aktivität-Kombinationen zu tracken
|
||||
@@ -323,6 +349,22 @@ export const getMemberLastParticipations = async (req, res) => {
|
||||
order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']],
|
||||
limit: parseInt(limit) * 10 // Get more to filter by group
|
||||
});
|
||||
|
||||
// Siehe getMemberActivities(): nur zählen, wenn Gruppenbindung passt (oder keine existiert)
|
||||
const filteredMemberActivities = memberActivities.filter((ma) => {
|
||||
if (!ma?.participant || !ma?.activity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const participantGroupId = ma.participant.groupId;
|
||||
const groupActivitiesForActivity = ma.activity.groupActivities || [];
|
||||
|
||||
if (!groupActivitiesForActivity.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return groupActivitiesForActivity.some((ga) => Number(ga.groupId) === Number(participantGroupId));
|
||||
});
|
||||
|
||||
// 2. Get all group activities for groups the member belongs to
|
||||
const groupActivities = [];
|
||||
@@ -399,7 +441,7 @@ export const getMemberLastParticipations = async (req, res) => {
|
||||
// 3. Kombiniere beide Listen und entferne Duplikate
|
||||
// Ein Duplikat liegt vor, wenn dieselbe Aktivität für denselben Participant bereits explizit zugeordnet ist
|
||||
const explicitActivityKeys = new Set();
|
||||
memberActivities.forEach(ma => {
|
||||
filteredMemberActivities.forEach(ma => {
|
||||
if (ma.activity && ma.activity.id && ma.participant && ma.participant.id) {
|
||||
// Erstelle einen eindeutigen Schlüssel: activityId-participantId
|
||||
const key = `${ma.activity.id}-${ma.participant.id}`;
|
||||
@@ -417,7 +459,7 @@ export const getMemberLastParticipations = async (req, res) => {
|
||||
});
|
||||
|
||||
// Kombiniere beide Listen
|
||||
const allActivities = [...memberActivities, ...uniqueGroupActivities];
|
||||
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities];
|
||||
|
||||
// Gruppiere nach Datum
|
||||
const participationsByDate = new Map();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import tournamentService from "../services/tournamentService.js";
|
||||
import { emitTournamentChanged } from '../services/socketService.js';
|
||||
import TournamentClass from '../models/TournamentClass.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
// 1. Alle Turniere eines Vereins
|
||||
export const getTournaments = async (req, res) => {
|
||||
@@ -12,6 +13,11 @@ export const getTournaments = async (req, res) => {
|
||||
res.status(200).json(tournaments);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof HttpError) {
|
||||
res.set('x-debug-tournament-clubid', String(clubId));
|
||||
res.set('x-debug-tournament-clubid-num', String(Number(clubId)));
|
||||
return res.status(error.statusCode || 500).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
70
backend/controllers/tournamentStagesController.js
Normal file
70
backend/controllers/tournamentStagesController.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import tournamentService from '../services/tournamentService.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
export const getStages = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.query;
|
||||
try {
|
||||
if (clubId == null || tournamentId == null) {
|
||||
return res.status(400).json({ error: 'clubId und tournamentId sind erforderlich.' });
|
||||
}
|
||||
const data = await tournamentService.getTournamentStages(token, Number(clubId), Number(tournamentId));
|
||||
res.status(200).json(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof HttpError) {
|
||||
// Debug-Hilfe: zeigt, welche IDs tatsächlich am Endpoint ankamen (ohne sensible Daten)
|
||||
res.set('x-debug-stages-clubid', String(clubId));
|
||||
res.set('x-debug-stages-tournamentid', String(tournamentId));
|
||||
res.set('x-debug-stages-clubid-num', String(Number(clubId)));
|
||||
return res.status(error.statusCode || 500).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const upsertStages = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, stages, advancement, advancements } = req.body;
|
||||
try {
|
||||
const data = await tournamentService.upsertTournamentStages(
|
||||
token,
|
||||
Number(clubId),
|
||||
Number(tournamentId),
|
||||
stages,
|
||||
advancement,
|
||||
advancements
|
||||
);
|
||||
res.status(200).json(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof HttpError) {
|
||||
res.set('x-debug-stages-clubid', String(clubId));
|
||||
res.set('x-debug-stages-tournamentid', String(tournamentId));
|
||||
res.set('x-debug-stages-clubid-num', String(Number(clubId)));
|
||||
return res.status(error.statusCode || 500).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const advanceStage = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, fromStageIndex, toStageIndex } = req.body;
|
||||
try {
|
||||
const data = await tournamentService.advanceTournamentStage(
|
||||
token,
|
||||
Number(clubId),
|
||||
Number(tournamentId),
|
||||
Number(fromStageIndex || 1),
|
||||
(toStageIndex == null ? null : Number(toStageIndex))
|
||||
);
|
||||
res.status(200).json(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof HttpError) {
|
||||
return res.status(error.statusCode || 500).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user