diff --git a/backend/controllers/trainingTimeController.js b/backend/controllers/trainingTimeController.js
new file mode 100644
index 0000000..92d379c
--- /dev/null
+++ b/backend/controllers/trainingTimeController.js
@@ -0,0 +1,80 @@
+import trainingTimeService from '../services/trainingTimeService.js';
+import { getSafeErrorMessage } from '../utils/errorUtils.js';
+
+export const getTrainingTimes = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId } = req.params;
+ const groups = await trainingTimeService.getTrainingTimes(userToken, clubId);
+ res.status(200).json(groups);
+ } catch (error) {
+ console.error('[getTrainingTimes] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingszeiten');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
+export const createTrainingTime = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId } = req.params;
+ const { trainingGroupId, weekday, startTime, endTime } = req.body;
+
+ if (!trainingGroupId || weekday === undefined || !startTime || !endTime) {
+ return res.status(400).json({ error: 'Alle Felder müssen ausgefüllt sein' });
+ }
+
+ const trainingTime = await trainingTimeService.createTrainingTime(
+ userToken,
+ clubId,
+ trainingGroupId,
+ weekday,
+ startTime,
+ endTime
+ );
+
+ res.status(201).json(trainingTime);
+ } catch (error) {
+ console.error('[createTrainingTime] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Trainingszeit');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
+export const updateTrainingTime = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId, timeId } = req.params;
+ const { weekday, startTime, endTime } = req.body;
+
+ const trainingTime = await trainingTimeService.updateTrainingTime(
+ userToken,
+ clubId,
+ timeId,
+ weekday,
+ startTime,
+ endTime
+ );
+
+ res.status(200).json(trainingTime);
+ } catch (error) {
+ console.error('[updateTrainingTime] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Trainingszeit');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
+export const deleteTrainingTime = async (req, res) => {
+ try {
+ const { authcode: userToken } = req.headers;
+ const { clubId, timeId } = req.params;
+
+ const result = await trainingTimeService.deleteTrainingTime(userToken, clubId, timeId);
+ res.status(200).json(result);
+ } catch (error) {
+ console.error('[deleteTrainingTime] - Error:', error);
+ const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Trainingszeit');
+ res.status(error.statusCode || 500).json({ error: msg });
+ }
+};
+
diff --git a/backend/migrations/create_training_times_table.sql b/backend/migrations/create_training_times_table.sql
new file mode 100644
index 0000000..836f911
--- /dev/null
+++ b/backend/migrations/create_training_times_table.sql
@@ -0,0 +1,19 @@
+-- Migration: Create training_times table
+-- Date: 2025-01-16
+-- For MariaDB/MySQL
+-- Stores training times for training groups
+
+CREATE TABLE IF NOT EXISTS `training_times` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `training_group_id` INT(11) NOT NULL,
+ `weekday` TINYINT(1) NOT NULL COMMENT '0 = Sunday, 1 = Monday, ..., 6 = Saturday',
+ `start_time` TIME NOT NULL,
+ `end_time` TIME NOT NULL,
+ `sort_order` INT(11) NOT NULL DEFAULT 0 COMMENT 'Order for displaying multiple times on the same weekday',
+ `created_at` DATETIME NOT NULL,
+ `updated_at` DATETIME NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `training_group_id` (`training_group_id`),
+ CONSTRAINT `training_times_ibfk_1` 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/TrainingTime.js b/backend/models/TrainingTime.js
new file mode 100644
index 0000000..27c67d4
--- /dev/null
+++ b/backend/models/TrainingTime.js
@@ -0,0 +1,47 @@
+import { DataTypes } from 'sequelize';
+import sequelize from '../database.js';
+import TrainingGroup from './TrainingGroup.js';
+
+const TrainingTime = sequelize.define('TrainingTime', {
+ id: {
+ type: DataTypes.INTEGER,
+ primaryKey: true,
+ autoIncrement: true,
+ allowNull: false,
+ },
+ trainingGroupId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ references: {
+ model: TrainingGroup,
+ key: 'id',
+ },
+ onDelete: 'CASCADE',
+ },
+ weekday: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ comment: '0 = Sunday, 1 = Monday, ..., 6 = Saturday'
+ },
+ startTime: {
+ type: DataTypes.TIME,
+ allowNull: false,
+ },
+ endTime: {
+ type: DataTypes.TIME,
+ allowNull: false,
+ },
+ sortOrder: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ defaultValue: 0,
+ comment: 'Order for displaying multiple times on the same weekday'
+ }
+}, {
+ tableName: 'training_times',
+ underscored: true,
+ timestamps: true,
+});
+
+export default TrainingTime;
+
diff --git a/backend/models/index.js b/backend/models/index.js
index a210585..ad9253f 100644
--- a/backend/models/index.js
+++ b/backend/models/index.js
@@ -47,6 +47,7 @@ import MemberImage from './MemberImage.js';
import TrainingGroup from './TrainingGroup.js';
import MemberTrainingGroup from './MemberTrainingGroup.js';
import ClubDisabledPresetGroup from './ClubDisabledPresetGroup.js';
+import TrainingTime from './TrainingTime.js';
// Official tournaments relations
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
@@ -316,6 +317,10 @@ TrainingGroup.belongsToMany(Member, {
Club.hasMany(ClubDisabledPresetGroup, { foreignKey: 'clubId', as: 'disabledPresetGroups' });
ClubDisabledPresetGroup.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
+// Training Times
+TrainingGroup.hasMany(TrainingTime, { foreignKey: 'trainingGroupId', as: 'trainingTimes' });
+TrainingTime.belongsTo(TrainingGroup, { foreignKey: 'trainingGroupId', as: 'trainingGroup' });
+
export {
User,
Log,
@@ -365,4 +370,5 @@ export {
TrainingGroup,
MemberTrainingGroup,
ClubDisabledPresetGroup,
+ TrainingTime,
};
diff --git a/backend/routes/trainingTimeRoutes.js b/backend/routes/trainingTimeRoutes.js
new file mode 100644
index 0000000..82e58c3
--- /dev/null
+++ b/backend/routes/trainingTimeRoutes.js
@@ -0,0 +1,19 @@
+import express from 'express';
+import { authenticate } from '../middleware/authMiddleware.js';
+import {
+ getTrainingTimes,
+ createTrainingTime,
+ updateTrainingTime,
+ deleteTrainingTime
+} from '../controllers/trainingTimeController.js';
+
+const router = express.Router();
+router.use(authenticate);
+
+router.get('/:clubId', getTrainingTimes);
+router.post('/:clubId', createTrainingTime);
+router.put('/:clubId/:timeId', updateTrainingTime);
+router.delete('/:clubId/:timeId', deleteTrainingTime);
+
+export default router;
+
diff --git a/backend/scripts/cleanupAllIndexes.js b/backend/scripts/cleanupAllIndexes.js
index 36c7569..e3b39e9 100644
--- a/backend/scripts/cleanupAllIndexes.js
+++ b/backend/scripts/cleanupAllIndexes.js
@@ -1,15 +1,22 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+import { development } from '../config.js';
-dotenv.config();
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+// Load .env from backend directory
+dotenv.config({ path: join(__dirname, '..', '.env') });
const report = [];
const dbConfig = {
- host: process.env.DB_HOST || 'localhost',
- user: process.env.DB_USER || 'root',
- password: process.env.DB_PASSWORD || '',
- database: process.env.DB_NAME || 'trainingdiary',
+ host: process.env.DB_HOST || development.host || 'localhost',
+ user: process.env.DB_USER || development.username || 'root',
+ password: process.env.DB_PASSWORD || development.password || '',
+ database: process.env.DB_NAME || development.database || 'trainingdiary',
};
async function getTables(connection) {
diff --git a/backend/server.js b/backend/server.js
index 0917a82..9b5102e 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -47,6 +47,7 @@ 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 trainingTimeRoutes from './routes/trainingTimeRoutes.js';
import schedulerService from './services/schedulerService.js';
import { requestLoggingMiddleware } from './middleware/requestLoggingMiddleware.js';
@@ -134,6 +135,7 @@ 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('/api/training-times', trainingTimeRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));
diff --git a/backend/services/trainingTimeService.js b/backend/services/trainingTimeService.js
new file mode 100644
index 0000000..11159a8
--- /dev/null
+++ b/backend/services/trainingTimeService.js
@@ -0,0 +1,142 @@
+import { checkAccess } from '../utils/userUtils.js';
+import TrainingTime from '../models/TrainingTime.js';
+import TrainingGroup from '../models/TrainingGroup.js';
+import HttpError from '../exceptions/HttpError.js';
+
+class TrainingTimeService {
+ async getTrainingTimes(userToken, clubId) {
+ await checkAccess(userToken, clubId);
+
+ const groups = await TrainingGroup.findAll({
+ where: { clubId },
+ include: [{
+ model: TrainingTime,
+ as: 'trainingTimes',
+ required: false,
+ separate: true,
+ order: [['weekday', 'ASC'], ['sortOrder', 'ASC'], ['startTime', 'ASC']]
+ }],
+ order: [['isPreset', 'DESC'], ['sortOrder', 'ASC'], ['name', 'ASC']]
+ });
+
+ return groups.map(group => {
+ const groupData = group.toJSON ? group.toJSON() : group;
+ return {
+ ...groupData,
+ trainingTimes: Array.isArray(groupData.trainingTimes)
+ ? groupData.trainingTimes.sort((a, b) => {
+ if (a.weekday !== b.weekday) return a.weekday - b.weekday;
+ if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
+ return a.startTime.localeCompare(b.startTime);
+ })
+ : []
+ };
+ });
+ }
+
+ async createTrainingTime(userToken, clubId, trainingGroupId, weekday, startTime, endTime) {
+ await checkAccess(userToken, clubId);
+
+ // Validate that the training group belongs to the club
+ const group = await TrainingGroup.findOne({
+ where: { id: trainingGroupId, clubId }
+ });
+
+ if (!group) {
+ throw new HttpError('Trainingsgruppe nicht gefunden', 404);
+ }
+
+ // Validate weekday (0-6)
+ if (weekday < 0 || weekday > 6) {
+ throw new HttpError('Ungültiger Wochentag (0-6)', 400);
+ }
+
+ // Validate times
+ if (!startTime || !endTime) {
+ throw new HttpError('Start- und Endzeit müssen angegeben werden', 400);
+ }
+
+ if (startTime >= endTime) {
+ throw new HttpError('Startzeit muss vor Endzeit liegen', 400);
+ }
+
+ // Get max sortOrder for this weekday and group
+ const maxSortOrder = await TrainingTime.max('sortOrder', {
+ where: {
+ trainingGroupId,
+ weekday
+ }
+ }) || 0;
+
+ const trainingTime = await TrainingTime.create({
+ trainingGroupId,
+ weekday,
+ startTime,
+ endTime,
+ sortOrder: maxSortOrder + 1
+ });
+
+ return trainingTime;
+ }
+
+ async updateTrainingTime(userToken, clubId, timeId, weekday, startTime, endTime) {
+ await checkAccess(userToken, clubId);
+
+ const trainingTime = await TrainingTime.findByPk(timeId, {
+ include: [{
+ model: TrainingGroup,
+ as: 'trainingGroup'
+ }]
+ });
+
+ if (!trainingTime || !trainingTime.trainingGroup) {
+ throw new HttpError('Trainingszeit nicht gefunden', 404);
+ }
+
+ if (trainingTime.trainingGroup.clubId !== parseInt(clubId)) {
+ throw new HttpError('Keine Berechtigung', 403);
+ }
+
+ // Validate weekday (0-6)
+ if (weekday !== undefined && (weekday < 0 || weekday > 6)) {
+ throw new HttpError('Ungültiger Wochentag (0-6)', 400);
+ }
+
+ // Validate times
+ if (startTime && endTime && startTime >= endTime) {
+ throw new HttpError('Startzeit muss vor Endzeit liegen', 400);
+ }
+
+ if (weekday !== undefined) trainingTime.weekday = weekday;
+ if (startTime !== undefined) trainingTime.startTime = startTime;
+ if (endTime !== undefined) trainingTime.endTime = endTime;
+
+ await trainingTime.save();
+ return trainingTime;
+ }
+
+ async deleteTrainingTime(userToken, clubId, timeId) {
+ await checkAccess(userToken, clubId);
+
+ const trainingTime = await TrainingTime.findByPk(timeId, {
+ include: [{
+ model: TrainingGroup,
+ as: 'trainingGroup'
+ }]
+ });
+
+ if (!trainingTime || !trainingTime.trainingGroup) {
+ throw new HttpError('Trainingszeit nicht gefunden', 404);
+ }
+
+ if (trainingTime.trainingGroup.clubId !== parseInt(clubId)) {
+ throw new HttpError('Keine Berechtigung', 403);
+ }
+
+ await trainingTime.destroy();
+ return { success: true };
+ }
+}
+
+export default new TrainingTimeService();
+
diff --git a/frontend/src/components/TrainingTimesTab.vue b/frontend/src/components/TrainingTimesTab.vue
new file mode 100644
index 0000000..0baa722
--- /dev/null
+++ b/frontend/src/components/TrainingTimesTab.vue
@@ -0,0 +1,483 @@
+
+ {{ group.name }}
+
+ Trainingszeit bearbeiten
+