diff --git a/backend/controllers/trainingGroupController.js b/backend/controllers/trainingGroupController.js
new file mode 100644
index 0000000..3a53382
--- /dev/null
+++ b/backend/controllers/trainingGroupController.js
@@ -0,0 +1,128 @@
+import trainingGroupService from '../services/trainingGroupService.js';
+import { getSafeErrorMessage } from '../utils/errorUtils.js';
+
+export const getTrainingGroups = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId } = req.params;
+ const groups = await trainingGroupService.getTrainingGroups(userToken, clubId);
+ res.status(200).json(groups);
+ } catch (error) {
+ console.error('[getTrainingGroups] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingsgruppen');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
+export const createTrainingGroup = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId } = req.params;
+ const { name, sortOrder } = req.body;
+ const group = await trainingGroupService.createTrainingGroup(userToken, clubId, name, sortOrder);
+ res.status(201).json(group);
+ } catch (error) {
+ console.error('[createTrainingGroup] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Trainingsgruppe');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
+export const updateTrainingGroup = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId, groupId } = req.params;
+ const { name, sortOrder } = req.body;
+ const group = await trainingGroupService.updateTrainingGroup(userToken, clubId, groupId, name, sortOrder);
+ res.status(200).json(group);
+ } catch (error) {
+ console.error('[updateTrainingGroup] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Trainingsgruppe');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
+export const deleteTrainingGroup = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId, groupId } = req.params;
+ await trainingGroupService.deleteTrainingGroup(userToken, clubId, groupId);
+ res.status(200).json({ success: true });
+ } catch (error) {
+ console.error('[deleteTrainingGroup] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Trainingsgruppe');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
+export const addMemberToGroup = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId, groupId, memberId } = req.params;
+ const memberGroup = await trainingGroupService.addMemberToGroup(userToken, clubId, groupId, memberId);
+ res.status(201).json(memberGroup);
+ } catch (error) {
+ console.error('[addMemberToGroup] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Hinzufügen des Mitglieds zur Gruppe');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
+export const removeMemberFromGroup = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId, groupId, memberId } = req.params;
+ await trainingGroupService.removeMemberFromGroup(userToken, clubId, groupId, memberId);
+ res.status(200).json({ success: true });
+ } catch (error) {
+ console.error('[removeMemberFromGroup] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Entfernen des Mitglieds aus der Gruppe');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
+export const getMemberGroups = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId, memberId } = req.params;
+ const groups = await trainingGroupService.getMemberGroups(userToken, clubId, memberId);
+ res.status(200).json(groups);
+ } catch (error) {
+ console.error('[getMemberGroups] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Gruppen des Mitglieds');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
+export const ensurePresetGroups = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId } = req.params;
+ const groups = await trainingGroupService.ensurePresetGroups(userToken, clubId);
+ res.status(200).json({
+ message: 'Preset-Gruppen wurden erstellt/überprüft',
+ groups: groups.length
+ });
+ } catch (error) {
+ console.error('[ensurePresetGroups] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Preset-Gruppen');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
+export const enablePresetGroup = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId, presetType } = req.params;
+ const group = await trainingGroupService.enablePresetGroup(userToken, clubId, presetType);
+ res.status(200).json({
+ message: 'Preset-Gruppe wurde aktiviert',
+ group
+ });
+ } catch (error) {
+ console.error('[enablePresetGroup] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Aktivieren der Preset-Gruppe');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
diff --git a/backend/migrations/create_club_disabled_preset_groups.sql b/backend/migrations/create_club_disabled_preset_groups.sql
new file mode 100644
index 0000000..93c9563
--- /dev/null
+++ b/backend/migrations/create_club_disabled_preset_groups.sql
@@ -0,0 +1,17 @@
+-- Migration: Create club_disabled_preset_groups table
+-- Date: 2025-01-16
+-- For MariaDB/MySQL
+-- Stores which preset groups are disabled for each club
+
+CREATE TABLE IF NOT EXISTS `club_disabled_preset_groups` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `club_id` INT(11) NOT NULL,
+ `preset_type` ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe') NOT NULL,
+ `created_at` DATETIME NOT NULL,
+ `updated_at` DATETIME NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `unique_club_preset_type` (`club_id`, `preset_type`),
+ KEY `club_id` (`club_id`),
+ CONSTRAINT `club_disabled_preset_groups_ibfk_1` FOREIGN KEY (`club_id`) REFERENCES `clubs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
diff --git a/backend/migrations/create_training_group_tables.sql b/backend/migrations/create_training_group_tables.sql
new file mode 100644
index 0000000..ac6e147
--- /dev/null
+++ b/backend/migrations/create_training_group_tables.sql
@@ -0,0 +1,34 @@
+-- Migration: Create training_group and member_training_group tables
+-- Date: 2025-01-16
+-- For MariaDB/MySQL
+
+-- Create training_group table
+CREATE TABLE IF NOT EXISTS `training_group` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `club_id` INT(11) NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
+ `is_preset` TINYINT(1) NOT NULL DEFAULT 0,
+ `preset_type` ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe') NULL,
+ `sort_order` INT(11) NOT NULL DEFAULT 0,
+ `created_at` DATETIME NOT NULL,
+ `updated_at` DATETIME NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `club_id` (`club_id`),
+ CONSTRAINT `training_group_ibfk_1` FOREIGN KEY (`club_id`) REFERENCES `clubs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+-- Create member_training_group junction table
+CREATE TABLE IF NOT EXISTS `member_training_group` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `member_id` INT(11) NOT NULL,
+ `training_group_id` INT(11) NOT NULL,
+ `created_at` DATETIME NOT NULL,
+ `updated_at` DATETIME NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `unique_member_group` (`member_id`, `training_group_id`),
+ KEY `member_id` (`member_id`),
+ KEY `training_group_id` (`training_group_id`),
+ CONSTRAINT `member_training_group_ibfk_1` FOREIGN KEY (`member_id`) REFERENCES `member` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ CONSTRAINT `member_training_group_ibfk_2` FOREIGN KEY (`training_group_id`) REFERENCES `training_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
diff --git a/backend/models/ClubDisabledPresetGroup.js b/backend/models/ClubDisabledPresetGroup.js
new file mode 100644
index 0000000..f1ad227
--- /dev/null
+++ b/backend/models/ClubDisabledPresetGroup.js
@@ -0,0 +1,33 @@
+import { DataTypes } from 'sequelize';
+import sequelize from '../database.js';
+import Club from './Club.js';
+
+const ClubDisabledPresetGroup = sequelize.define('ClubDisabledPresetGroup', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false,
+ },
+ clubId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: Club,
+ key: 'id',
+ },
+ onDelete: 'CASCADE',
+ },
+ presetType: {
+ type: DataTypes.ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe'),
+ allowNull: false,
+ comment: 'Type of preset group that is disabled for this club'
+ }
+}, {
+ tableName: 'club_disabled_preset_groups',
+ underscored: true,
+ timestamps: true,
+});
+
+export default ClubDisabledPresetGroup;
+
diff --git a/backend/models/MemberTrainingGroup.js b/backend/models/MemberTrainingGroup.js
new file mode 100644
index 0000000..cbc28c0
--- /dev/null
+++ b/backend/models/MemberTrainingGroup.js
@@ -0,0 +1,38 @@
+import { DataTypes } from 'sequelize';
+import sequelize from '../database.js';
+import Member from './Member.js';
+import TrainingGroup from './TrainingGroup.js';
+
+const MemberTrainingGroup = sequelize.define('MemberTrainingGroup', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false,
+ },
+ memberId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: Member,
+ key: 'id',
+ },
+ onDelete: 'CASCADE',
+ },
+ trainingGroupId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: TrainingGroup,
+ key: 'id',
+ },
+ onDelete: 'CASCADE',
+ }
+}, {
+ tableName: 'member_training_group',
+ underscored: true,
+ timestamps: true,
+});
+
+export default MemberTrainingGroup;
+
diff --git a/backend/models/TrainingGroup.js b/backend/models/TrainingGroup.js
new file mode 100644
index 0000000..5df9b01
--- /dev/null
+++ b/backend/models/TrainingGroup.js
@@ -0,0 +1,49 @@
+import { DataTypes } from 'sequelize';
+import sequelize from '../database.js';
+import Club from './Club.js';
+
+const TrainingGroup = sequelize.define('TrainingGroup', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false,
+ },
+ clubId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: Club,
+ key: 'id',
+ },
+ onDelete: 'CASCADE',
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ },
+ isPreset: {
+ type: DataTypes.BOOLEAN,
+ allowNull: false,
+ defaultValue: false,
+ comment: 'True if this is a preset group (Anfänger, Fortgeschrittene, etc.)'
+ },
+ presetType: {
+ type: DataTypes.ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe'),
+ allowNull: true,
+ comment: 'Type of preset group'
+ },
+ sortOrder: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ defaultValue: 0,
+ comment: 'Order for displaying groups'
+ }
+}, {
+ tableName: 'training_group',
+ underscored: true,
+ timestamps: true,
+});
+
+export default TrainingGroup;
+
diff --git a/backend/models/index.js b/backend/models/index.js
index 34d58cf..a210585 100644
--- a/backend/models/index.js
+++ b/backend/models/index.js
@@ -44,6 +44,9 @@ import ApiLog from './ApiLog.js';
import MemberTransferConfig from './MemberTransferConfig.js';
import MemberContact from './MemberContact.js';
import MemberImage from './MemberImage.js';
+import TrainingGroup from './TrainingGroup.js';
+import MemberTrainingGroup from './MemberTrainingGroup.js';
+import ClubDisabledPresetGroup from './ClubDisabledPresetGroup.js';
// Official tournaments relations
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
@@ -292,6 +295,27 @@ MemberContact.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
Member.hasMany(MemberImage, { foreignKey: 'memberId', as: 'images' });
MemberImage.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
+// Training Groups
+Club.hasMany(TrainingGroup, { foreignKey: 'clubId', as: 'trainingGroups' });
+TrainingGroup.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
+
+Member.belongsToMany(TrainingGroup, {
+ through: MemberTrainingGroup,
+ foreignKey: 'memberId',
+ otherKey: 'trainingGroupId',
+ as: 'trainingGroups'
+});
+TrainingGroup.belongsToMany(Member, {
+ through: MemberTrainingGroup,
+ foreignKey: 'trainingGroupId',
+ otherKey: 'memberId',
+ as: 'members'
+});
+
+// Club Disabled Preset Groups
+Club.hasMany(ClubDisabledPresetGroup, { foreignKey: 'clubId', as: 'disabledPresetGroups' });
+ClubDisabledPresetGroup.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
+
export {
User,
Log,
@@ -338,4 +362,7 @@ export {
MemberTransferConfig,
MemberContact,
MemberImage,
+ TrainingGroup,
+ MemberTrainingGroup,
+ ClubDisabledPresetGroup,
};
diff --git a/backend/node_modules/.package-lock.json b/backend/node_modules/.package-lock.json
index 6c37409..7f36e1a 100644
--- a/backend/node_modules/.package-lock.json
+++ b/backend/node_modules/.package-lock.json
@@ -2670,7 +2670,9 @@
"license": "ISC"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"peer": true,
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 96ba2d6..5d4638a 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -2706,7 +2706,9 @@
"license": "ISC"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"peer": true,
diff --git a/backend/routes/trainingGroupRoutes.js b/backend/routes/trainingGroupRoutes.js
new file mode 100644
index 0000000..24c10ee
--- /dev/null
+++ b/backend/routes/trainingGroupRoutes.js
@@ -0,0 +1,33 @@
+import express from 'express';
+import { authenticate } from '../middleware/authMiddleware.js';
+import {
+ getTrainingGroups,
+ createTrainingGroup,
+ updateTrainingGroup,
+ deleteTrainingGroup,
+ addMemberToGroup,
+ removeMemberFromGroup,
+ getMemberGroups,
+ ensurePresetGroups,
+ enablePresetGroup,
+} from '../controllers/trainingGroupController.js';
+
+const router = express.Router();
+
+router.use(authenticate);
+
+// Spezifischere Routen zuerst (mit /member/ im Pfad)
+router.get('/:clubId/member/:memberId', getMemberGroups);
+router.post('/:clubId/:groupId/member/:memberId', addMemberToGroup);
+router.delete('/:clubId/:groupId/member/:memberId', removeMemberFromGroup);
+
+// Allgemeinere Routen danach
+router.post('/:clubId/ensure-preset-groups', ensurePresetGroups);
+router.post('/:clubId/enable-preset-group/:presetType', enablePresetGroup);
+router.get('/:clubId', getTrainingGroups);
+router.post('/:clubId', createTrainingGroup);
+router.put('/:clubId/:groupId', updateTrainingGroup);
+router.delete('/:clubId/:groupId', deleteTrainingGroup);
+
+export default router;
+
diff --git a/backend/server.js b/backend/server.js
index 5025dbb..0917a82 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -46,6 +46,7 @@ import memberActivityRoutes from './routes/memberActivityRoutes.js';
import permissionRoutes from './routes/permissionRoutes.js';
import apiLogRoutes from './routes/apiLogRoutes.js';
import memberTransferConfigRoutes from './routes/memberTransferConfigRoutes.js';
+import trainingGroupRoutes from './routes/trainingGroupRoutes.js';
import schedulerService from './services/schedulerService.js';
import { requestLoggingMiddleware } from './middleware/requestLoggingMiddleware.js';
@@ -132,6 +133,7 @@ app.use('/api/member-activities', memberActivityRoutes);
app.use('/api/permissions', permissionRoutes);
app.use('/api/logs', apiLogRoutes);
app.use('/api/member-transfer-config', memberTransferConfigRoutes);
+app.use('/api/training-groups', trainingGroupRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));
diff --git a/backend/services/clubService.js b/backend/services/clubService.js
index ddf31c0..68c3d85 100644
--- a/backend/services/clubService.js
+++ b/backend/services/clubService.js
@@ -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) {
diff --git a/backend/services/trainingGroupService.js b/backend/services/trainingGroupService.js
new file mode 100644
index 0000000..cce1d65
--- /dev/null
+++ b/backend/services/trainingGroupService.js
@@ -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();
+
diff --git a/backend/utils/errorUtils.js b/backend/utils/errorUtils.js
new file mode 100644
index 0000000..eb9f9ff
--- /dev/null
+++ b/backend/utils/errorUtils.js
@@ -0,0 +1,33 @@
+/**
+ * Gibt eine sichere Fehlermeldung zurück, die an den Client gesendet werden kann.
+ * Verhindert, dass sensible Informationen (wie Stack-Traces oder interne Details) nach außen gelangen.
+ *
+ * @param {Error} error - Der Fehler-Objekt
+ * @param {string} defaultMessage - Standard-Nachricht, die verwendet wird, wenn keine sichere Nachricht verfügbar ist
+ * @returns {string} - Sichere Fehlermeldung
+ */
+export function getSafeErrorMessage(error, defaultMessage = 'Ein Fehler ist aufgetreten') {
+ // Wenn kein Fehler vorhanden ist, gib die Standard-Nachricht zurück
+ if (!error) {
+ return defaultMessage;
+ }
+
+ // Wenn der Fehler eine message hat und es keine interne/technische Nachricht ist
+ if (error.message) {
+ const message = error.message;
+
+ // Prüfe, ob die Nachricht sicher ist (keine Stack-Traces, keine internen Pfade, etc.)
+ // Erlaube nur benutzerfreundliche Nachrichten
+ if (message &&
+ !message.includes('at ') && // Keine Stack-Traces
+ !message.includes('Error:') && // Keine technischen Fehler-Präfixe
+ !message.startsWith('/') && // Keine Dateipfade
+ message.length < 500) { // Keine sehr langen Nachrichten (könnten Stack-Traces sein)
+ return message;
+ }
+ }
+
+ // Fallback: Standard-Nachricht verwenden
+ return defaultMessage;
+}
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e7c061a..c11e438 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -23,12 +23,12 @@
"vuex": "^4.1.0"
},
"devDependencies": {
- "@vitejs/plugin-vue": "^4.6.2",
+ "@vitejs/plugin-vue": "^5.2.1",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^8.0.3",
"sass": "^1.77.8",
"sass-loader": "^14.2.1",
- "vite": "^7.2.2"
+ "vite": "^5.4.2"
}
},
"node_modules/@babel/code-frame": {
@@ -191,9 +191,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
@@ -204,13 +204,13 @@
"aix"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
@@ -221,13 +221,13 @@
"android"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
@@ -238,13 +238,13 @@
"android"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
@@ -255,13 +255,13 @@
"android"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
@@ -272,13 +272,13 @@
"darwin"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
@@ -289,13 +289,13 @@
"darwin"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
@@ -306,13 +306,13 @@
"freebsd"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
@@ -323,13 +323,13 @@
"freebsd"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
@@ -340,13 +340,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
@@ -357,13 +357,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
@@ -374,13 +374,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
@@ -391,13 +391,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
@@ -408,13 +408,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
@@ -425,13 +425,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
@@ -442,13 +442,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
@@ -459,13 +459,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
@@ -476,30 +476,13 @@
"linux"
],
"engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
@@ -510,30 +493,13 @@
"netbsd"
],
"engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
@@ -544,30 +510,13 @@
"openbsd"
],
"engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
@@ -578,13 +527,13 @@
"sunos"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
@@ -595,13 +544,13 @@
"win32"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
@@ -612,13 +561,13 @@
"win32"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
@@ -629,7 +578,7 @@
"win32"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"node_modules/@eslint/eslintrc": {
@@ -1335,16 +1284,16 @@
"optional": true
},
"node_modules/@vitejs/plugin-vue": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz",
- "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==",
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
"dev": true,
"license": "MIT",
"engines": {
- "node": "^14.18.0 || >=16.0.0"
+ "node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
- "vite": "^4.0.0 || ^5.0.0",
+ "vite": "^5.0.0 || ^6.0.0",
"vue": "^3.2.25"
}
},
@@ -1993,9 +1942,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -2003,35 +1952,32 @@
"esbuild": "bin/esbuild"
},
"engines": {
- "node": ">=18"
+ "node": ">=12"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.12",
- "@esbuild/android-arm": "0.25.12",
- "@esbuild/android-arm64": "0.25.12",
- "@esbuild/android-x64": "0.25.12",
- "@esbuild/darwin-arm64": "0.25.12",
- "@esbuild/darwin-x64": "0.25.12",
- "@esbuild/freebsd-arm64": "0.25.12",
- "@esbuild/freebsd-x64": "0.25.12",
- "@esbuild/linux-arm": "0.25.12",
- "@esbuild/linux-arm64": "0.25.12",
- "@esbuild/linux-ia32": "0.25.12",
- "@esbuild/linux-loong64": "0.25.12",
- "@esbuild/linux-mips64el": "0.25.12",
- "@esbuild/linux-ppc64": "0.25.12",
- "@esbuild/linux-riscv64": "0.25.12",
- "@esbuild/linux-s390x": "0.25.12",
- "@esbuild/linux-x64": "0.25.12",
- "@esbuild/netbsd-arm64": "0.25.12",
- "@esbuild/netbsd-x64": "0.25.12",
- "@esbuild/openbsd-arm64": "0.25.12",
- "@esbuild/openbsd-x64": "0.25.12",
- "@esbuild/openharmony-arm64": "0.25.12",
- "@esbuild/sunos-x64": "0.25.12",
- "@esbuild/win32-arm64": "0.25.12",
- "@esbuild/win32-ia32": "0.25.12",
- "@esbuild/win32-x64": "0.25.12"
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/escape-string-regexp": {
@@ -3651,54 +3597,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/tinyglobby": {
- "version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.3"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tinyglobby/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -3773,24 +3671,21 @@
"license": "MIT"
},
"node_modules/vite": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
- "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "esbuild": "^0.25.0",
- "fdir": "^6.5.0",
- "picomatch": "^4.0.3",
- "postcss": "^8.5.6",
- "rollup": "^4.43.0",
- "tinyglobby": "^0.2.15"
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
- "node": "^20.19.0 || >=22.12.0"
+ "node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -3799,25 +3694,19 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "jiti": ">=1.21.0",
- "less": "^4.0.0",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
"lightningcss": "^1.21.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
- "jiti": {
- "optional": true
- },
"less": {
"optional": true
},
@@ -3838,46 +3727,9 @@
},
"terser": {
"optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
}
}
},
- "node_modules/vite/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/vite/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/vue": {
"version": "3.5.17",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz",
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 9e7c2f5..a31c5b6 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -79,14 +79,14 @@
Trainings-Statistik
-