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 - - ⚙️ - Vereinseinstellungen -