Update training group management and enhance UI components
This commit introduces the `TrainingGroup` model and related functionality, allowing for the management of training groups within the application. The `ClubService` is updated to automatically create preset groups upon club creation. The frontend is enhanced with new views and components, including `TrainingGroupsView` and `TrainingGroupsTab`, to facilitate the display and management of training groups. Additionally, the `MembersView` is updated to allow adding and removing members from training groups, improving the overall user experience and interactivity in managing club members and their associated training groups.
This commit is contained in:
@@ -5,6 +5,7 @@ import Member from '../models/Member.js';
|
||||
import { Op, fn, where, col } from 'sequelize';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import permissionService from './permissionService.js';
|
||||
import trainingGroupService from './trainingGroupService.js';
|
||||
|
||||
class ClubService {
|
||||
async getAllClubs() {
|
||||
@@ -18,7 +19,10 @@ class ClubService {
|
||||
}
|
||||
|
||||
async createClub(clubName) {
|
||||
return await Club.create({ name: clubName });
|
||||
const club = await Club.create({ name: clubName });
|
||||
// Erstelle automatisch die Vorgaben-Gruppen
|
||||
await trainingGroupService.createPresetGroups(club.id);
|
||||
return club;
|
||||
}
|
||||
|
||||
async addUserToClub(userId, clubId, isOwner = false) {
|
||||
|
||||
324
backend/services/trainingGroupService.js
Normal file
324
backend/services/trainingGroupService.js
Normal file
@@ -0,0 +1,324 @@
|
||||
import { Op } from 'sequelize';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import TrainingGroup from '../models/TrainingGroup.js';
|
||||
import MemberTrainingGroup from '../models/MemberTrainingGroup.js';
|
||||
import Member from '../models/Member.js';
|
||||
import ClubDisabledPresetGroup from '../models/ClubDisabledPresetGroup.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
class TrainingGroupService {
|
||||
// Vorgaben-Gruppen beim Vereins-Erstellen anlegen (idempotent - erstellt nur fehlende)
|
||||
async createPresetGroups(clubId) {
|
||||
const presetGroups = [
|
||||
{ name: 'Anfänger', presetType: 'anfaenger', sortOrder: 1 },
|
||||
{ name: 'Fortgeschrittene', presetType: 'fortgeschrittene', sortOrder: 2 },
|
||||
{ name: 'Erwachsene', presetType: 'erwachsene', sortOrder: 3 },
|
||||
{ name: 'Nachwuchs', presetType: 'nachwuchs', sortOrder: 4 },
|
||||
{ name: 'Leistungsgruppe', presetType: 'leistungsgruppe', sortOrder: 5 },
|
||||
];
|
||||
|
||||
// Hole alle deaktivierten Preset-Gruppen für diesen Verein
|
||||
const disabledPresetGroups = await ClubDisabledPresetGroup.findAll({
|
||||
where: { clubId },
|
||||
});
|
||||
const disabledPresetTypes = new Set(disabledPresetGroups.map(d => d.presetType));
|
||||
|
||||
const createdGroups = [];
|
||||
for (const preset of presetGroups) {
|
||||
// Überspringe deaktivierte Preset-Gruppen
|
||||
if (disabledPresetTypes.has(preset.presetType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prüfe, ob diese Preset-Gruppe bereits existiert
|
||||
const existing = await TrainingGroup.findOne({
|
||||
where: {
|
||||
clubId,
|
||||
isPreset: true,
|
||||
presetType: preset.presetType
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
const group = await TrainingGroup.create({
|
||||
clubId,
|
||||
name: preset.name,
|
||||
isPreset: true,
|
||||
presetType: preset.presetType,
|
||||
sortOrder: preset.sortOrder,
|
||||
});
|
||||
createdGroups.push(group);
|
||||
} else {
|
||||
createdGroups.push(existing);
|
||||
}
|
||||
}
|
||||
return createdGroups;
|
||||
}
|
||||
|
||||
// Stelle sicher, dass alle Preset-Gruppen für einen Verein existieren
|
||||
async ensurePresetGroups(userToken, clubId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
return await this.createPresetGroups(clubId);
|
||||
}
|
||||
|
||||
// Aktiviere eine deaktivierte Preset-Gruppe wieder
|
||||
async enablePresetGroup(userToken, clubId, presetType) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
// Entferne die Deaktivierung
|
||||
await ClubDisabledPresetGroup.destroy({
|
||||
where: { clubId, presetType },
|
||||
});
|
||||
|
||||
// Erstelle die Gruppe, falls sie nicht existiert
|
||||
const presetGroups = [
|
||||
{ name: 'Anfänger', presetType: 'anfaenger', sortOrder: 1 },
|
||||
{ name: 'Fortgeschrittene', presetType: 'fortgeschrittene', sortOrder: 2 },
|
||||
{ name: 'Erwachsene', presetType: 'erwachsene', sortOrder: 3 },
|
||||
{ name: 'Nachwuchs', presetType: 'nachwuchs', sortOrder: 4 },
|
||||
{ name: 'Leistungsgruppe', presetType: 'leistungsgruppe', sortOrder: 5 },
|
||||
];
|
||||
|
||||
const preset = presetGroups.find(p => p.presetType === presetType);
|
||||
if (!preset) {
|
||||
throw new HttpError('Ungültiger Preset-Typ', 400);
|
||||
}
|
||||
|
||||
const existing = await TrainingGroup.findOne({
|
||||
where: {
|
||||
clubId,
|
||||
isPreset: true,
|
||||
presetType: presetType
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
return await TrainingGroup.create({
|
||||
clubId,
|
||||
name: preset.name,
|
||||
isPreset: true,
|
||||
presetType: preset.presetType,
|
||||
sortOrder: preset.sortOrder,
|
||||
});
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
async getTrainingGroups(userToken, clubId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
// Stelle sicher, dass alle Preset-Gruppen existieren
|
||||
await this.ensurePresetGroups(userToken, clubId);
|
||||
|
||||
const groups = await TrainingGroup.findAll({
|
||||
where: { clubId },
|
||||
order: [
|
||||
['isPreset', 'DESC'], // Preset-Gruppen zuerst
|
||||
['sortOrder', 'ASC'],
|
||||
['name', 'ASC'],
|
||||
],
|
||||
include: [
|
||||
{
|
||||
model: Member,
|
||||
as: 'members',
|
||||
through: { attributes: [] }, // Keine Junction-Table-Attribute
|
||||
attributes: ['id', 'firstName', 'lastName'],
|
||||
required: false, // LEFT JOIN, damit auch Gruppen ohne Mitglieder zurückgegeben werden
|
||||
},
|
||||
],
|
||||
});
|
||||
// Stelle sicher, dass es ein Array ist und dass jedes Gruppen-Objekt ein members-Array hat
|
||||
return groups.map(group => {
|
||||
const groupData = group.toJSON ? group.toJSON() : group;
|
||||
return {
|
||||
...groupData,
|
||||
members: Array.isArray(groupData.members) ? groupData.members : []
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async createTrainingGroup(userToken, clubId, name, sortOrder = 0) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
// Prüfe, ob bereits eine Gruppe mit diesem Namen existiert
|
||||
const existing = await TrainingGroup.findOne({
|
||||
where: { clubId, name },
|
||||
});
|
||||
if (existing) {
|
||||
throw new HttpError('Eine Gruppe mit diesem Namen existiert bereits', 409);
|
||||
}
|
||||
|
||||
const group = await TrainingGroup.create({
|
||||
clubId,
|
||||
name,
|
||||
isPreset: false,
|
||||
presetType: null,
|
||||
sortOrder,
|
||||
});
|
||||
return group;
|
||||
}
|
||||
|
||||
async updateTrainingGroup(userToken, clubId, groupId, name, sortOrder) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const group = await TrainingGroup.findOne({
|
||||
where: { id: groupId, clubId },
|
||||
});
|
||||
if (!group) {
|
||||
throw new HttpError('Gruppe nicht gefunden', 404);
|
||||
}
|
||||
|
||||
// Preset-Gruppen können nicht umbenannt werden
|
||||
if (group.isPreset && name !== group.name) {
|
||||
throw new HttpError('Vorgaben-Gruppen können nicht umbenannt werden', 400);
|
||||
}
|
||||
|
||||
// Prüfe, ob bereits eine andere Gruppe mit diesem Namen existiert
|
||||
if (name !== group.name) {
|
||||
const existing = await TrainingGroup.findOne({
|
||||
where: { clubId, name, id: { [Op.ne]: groupId } },
|
||||
});
|
||||
if (existing) {
|
||||
throw new HttpError('Eine Gruppe mit diesem Namen existiert bereits', 409);
|
||||
}
|
||||
}
|
||||
|
||||
group.name = name;
|
||||
if (sortOrder !== undefined) {
|
||||
group.sortOrder = sortOrder;
|
||||
}
|
||||
await group.save();
|
||||
return group;
|
||||
}
|
||||
|
||||
async deleteTrainingGroup(userToken, clubId, groupId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const group = await TrainingGroup.findOne({
|
||||
where: { id: groupId, clubId },
|
||||
});
|
||||
if (!group) {
|
||||
throw new HttpError('Gruppe nicht gefunden', 404);
|
||||
}
|
||||
|
||||
// Wenn es eine Preset-Gruppe ist, markiere sie als deaktiviert statt sie zu löschen
|
||||
if (group.isPreset && group.presetType) {
|
||||
// Prüfe, ob bereits als deaktiviert markiert
|
||||
const existing = await ClubDisabledPresetGroup.findOne({
|
||||
where: { clubId, presetType: group.presetType },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await ClubDisabledPresetGroup.create({
|
||||
clubId,
|
||||
presetType: group.presetType,
|
||||
});
|
||||
}
|
||||
|
||||
// Lösche die Gruppe
|
||||
await group.destroy();
|
||||
return { success: true, message: 'Preset-Gruppe wurde deaktiviert und wird nicht automatisch wieder erstellt' };
|
||||
}
|
||||
|
||||
await group.destroy();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async addMemberToGroup(userToken, clubId, groupId, memberId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
// Prüfe, ob Gruppe existiert und zum Verein gehört
|
||||
const group = await TrainingGroup.findOne({
|
||||
where: { id: groupId, clubId },
|
||||
});
|
||||
if (!group) {
|
||||
throw new HttpError('Gruppe nicht gefunden', 404);
|
||||
}
|
||||
|
||||
// Prüfe, ob Mitglied existiert und zum Verein gehört
|
||||
const member = await Member.findOne({
|
||||
where: { id: memberId, clubId },
|
||||
});
|
||||
if (!member) {
|
||||
throw new HttpError('Mitglied nicht gefunden', 404);
|
||||
}
|
||||
|
||||
// Prüfe, ob Zuordnung bereits existiert
|
||||
const existing = await MemberTrainingGroup.findOne({
|
||||
where: { memberId, trainingGroupId: groupId },
|
||||
});
|
||||
if (existing) {
|
||||
throw new HttpError('Mitglied ist bereits in dieser Gruppe', 409);
|
||||
}
|
||||
|
||||
const memberGroup = await MemberTrainingGroup.create({
|
||||
memberId,
|
||||
trainingGroupId: groupId,
|
||||
});
|
||||
return memberGroup;
|
||||
}
|
||||
|
||||
async removeMemberFromGroup(userToken, clubId, groupId, memberId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
// Prüfe, ob Gruppe existiert und zum Verein gehört
|
||||
const group = await TrainingGroup.findOne({
|
||||
where: { id: groupId, clubId },
|
||||
});
|
||||
if (!group) {
|
||||
throw new HttpError('Gruppe nicht gefunden', 404);
|
||||
}
|
||||
|
||||
// Prüfe, ob Mitglied existiert und zum Verein gehört
|
||||
const member = await Member.findOne({
|
||||
where: { id: memberId, clubId },
|
||||
});
|
||||
if (!member) {
|
||||
throw new HttpError('Mitglied nicht gefunden', 404);
|
||||
}
|
||||
|
||||
const memberGroup = await MemberTrainingGroup.findOne({
|
||||
where: { memberId, trainingGroupId: groupId },
|
||||
});
|
||||
if (!memberGroup) {
|
||||
throw new HttpError('Zuordnung nicht gefunden', 404);
|
||||
}
|
||||
|
||||
await memberGroup.destroy();
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async getMemberGroups(userToken, clubId, memberId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const member = await Member.findOne({
|
||||
where: { id: memberId, clubId },
|
||||
});
|
||||
if (!member) {
|
||||
throw new HttpError('Mitglied nicht gefunden', 404);
|
||||
}
|
||||
|
||||
const groups = await TrainingGroup.findAll({
|
||||
where: { clubId },
|
||||
include: [
|
||||
{
|
||||
model: Member,
|
||||
as: 'members',
|
||||
where: { id: memberId },
|
||||
through: { attributes: [] },
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
order: [
|
||||
['isPreset', 'DESC'],
|
||||
['sortOrder', 'ASC'],
|
||||
['name', 'ASC'],
|
||||
],
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
|
||||
export default new TrainingGroupService();
|
||||
|
||||
Reference in New Issue
Block a user