diff --git a/backend/controllers/diaryDateActivityController.js b/backend/controllers/diaryDateActivityController.js index cc014df..d7ea4ff 100644 --- a/backend/controllers/diaryDateActivityController.js +++ b/backend/controllers/diaryDateActivityController.js @@ -4,13 +4,14 @@ export const createDiaryDateActivity = async (req, res) => { try { const { authcode: userToken } = req.headers; const { clubId } = req.params; - const { diaryDateId, activity, duration, durationText, orderId } = req.body; + const { diaryDateId, activity, duration, durationText, orderId, isTimeblock } = req.body; const activityItem = await diaryDateActivityService.createActivity(userToken, clubId, { diaryDateId, activity, duration, durationText, orderId, + isTimeblock, }); res.status(201).json(activityItem); } catch (error) { @@ -23,13 +24,14 @@ export const updateDiaryDateActivity = async (req, res) => { try { const { authcode: userToken } = req.headers; const { clubId, id } = req.params; - const { predefinedActivityId, customActivityName, duration, durationText, orderId } = req.body; + const { predefinedActivityId, customActivityName, duration, durationText, orderId, groupId } = req.body; // Add groupId const updatedActivity = await diaryDateActivityService.updateActivity(userToken, clubId, id, { predefinedActivityId, customActivityName, duration, durationText, orderId, + groupId, // Pass groupId to the service }); res.status(200).json(updatedActivity); } catch (error) { @@ -68,6 +70,19 @@ export const getDiaryDateActivities = async (req, res) => { const activities = await diaryDateActivityService.getActivities(userToken, clubId, diaryDateId); res.status(200).json(activities); } catch (error) { + console.log(error); res.status(500).json({ error: 'Error getting activities' }); } +} + +export const addGroupActivity = async(req, res) => { + try { + const { authcode: userToken } = req.headers; + const { clubId, diaryDateId, groupId, activity } = req.body; + const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity); + res.status(201).json(activityItem); + } catch (error) { + console.log(error); + res.status(500).json({ error: 'Error adding group activity' }); + } } \ No newline at end of file diff --git a/backend/controllers/groupController.js b/backend/controllers/groupController.js new file mode 100644 index 0000000..b2d0aba --- /dev/null +++ b/backend/controllers/groupController.js @@ -0,0 +1,29 @@ +import HttpError from '../exceptions/HttpError.js'; +import groupService from '../services/groupService.js'; + +const addGroup = async(req, res) => { + try { + const { authcode: userToken } = req.headers; + const { clubid: clubId, dateid: dateId, name, lead } = req.body; + const result = await groupService.addGroup(userToken, clubId, dateId, name, lead); + res.status(201).json(result); + } catch (error) { + console.error('[addGroup] - Error:', error); + console.log(req.params, req.headers, req.body) + res.status(error.statusCode || 500).json({ error: error.message }); + } +} + +const getGroups = async(req, res) => { + try { + const { clubId, dateId } = req.params; + const { authcode: userToken } = req.headers; + const result = await groupService.getGroups(userToken, clubId, dateId); + res.status(200).json(result); + } catch (error) { + console.error('[getGroups] - Error:', error); + res.status(error.statusCode || 500).json({ error: error.message }); + } +} + +export { addGroup, getGroups }; \ No newline at end of file diff --git a/backend/models/DiaryDateActivity.js b/backend/models/DiaryDateActivity.js index 2390262..7fa4f8d 100644 --- a/backend/models/DiaryDateActivity.js +++ b/backend/models/DiaryDateActivity.js @@ -11,13 +11,18 @@ const DiaryDateActivity = sequelize.define('DiaryDateActivity', { }, diaryDateId: { type: DataTypes.INTEGER, - allowNull: false, + allowNull: true, references: { model: DiaryDate, key: 'id', }, onDelete: 'CASCADE', }, + isTimeblock: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + }, predefinedActivityId: { type: DataTypes.INTEGER, allowNull: true, @@ -27,11 +32,7 @@ const DiaryDateActivity = sequelize.define('DiaryDateActivity', { }, onDelete: 'SET NULL', }, - customActivityName: { - type: DataTypes.STRING, - allowNull: true, - }, - duration: { + duration: { type: DataTypes.INTEGER, allowNull: true, }, diff --git a/backend/models/Group.js b/backend/models/Group.js index f7257a5..0c87dcc 100644 --- a/backend/models/Group.js +++ b/backend/models/Group.js @@ -3,7 +3,13 @@ import sequelize from '../database.js'; import DiaryDate from './DiaryDates.js'; const Group = sequelize.define('Group', { - diaryDate: { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + diaryDateId: { type: DataTypes.INTEGER, allowNull: false, references: { diff --git a/backend/models/GroupActivity.js b/backend/models/GroupActivity.js new file mode 100644 index 0000000..a112203 --- /dev/null +++ b/backend/models/GroupActivity.js @@ -0,0 +1,40 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; +import DiaryDateActivity from './DiaryDateActivity.js'; +import Group from './Group.js'; + +const GroupActivity = sequelize.define('GroupActivity', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + diaryDateActivity: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: DiaryDateActivity, + key: 'id' + }, + onDelete: 'CASCADE' + }, + groupId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: Group, + key: 'id', + }, + onDelete: 'CASCADE' + }, + customActivity: { + type: DataTypes.INTEGER, + allowNull: true, + }, +}, { + tableName: 'group_activity', + underscored: true, + timestamps: true, +}); + +export default GroupActivity; \ No newline at end of file diff --git a/backend/models/index.js b/backend/models/index.js index ae180ea..a0fda39 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -19,6 +19,8 @@ import League from './League.js'; import Team from './Team.js'; import Season from './Season.js'; import Location from './Location.js'; +import Group from './Group.js'; +import GroupActivity from './GroupActivity.js'; User.hasMany(Log, { foreignKey: 'userId' }); Log.belongsTo(User, { foreignKey: 'userId' }); @@ -94,6 +96,17 @@ User.hasMany(UserClub, { foreignKey: 'userId' }); UserClub.belongsTo(Club, { foreignKey: 'clubId', as: 'club' }); Club.hasMany(UserClub, { foreignKey: 'clubId' }); +Group.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDateGroup' }); +DiaryDate.hasMany(Group, { foreignKey: 'diaryDateId', as: 'groupsDiaryDate' }); + +GroupActivity.belongsTo(DiaryDateActivity, { foreignKey: 'id', as: 'activityGroupActivity' }); +DiaryDateActivity.hasMany(GroupActivity, { foreignKey: 'diaryDateActivity', as: 'groupActivities' }); + +Group.hasOne(GroupActivity, { foreignKey: 'groupId', as: 'groupGroupActivity' }); +GroupActivity.belongsTo(Group, { foreignKey: 'groupId', as: 'groupsGroupActivity' }); + +GroupActivity.belongsTo(PredefinedActivity, { foreignKey: 'customActivity', as: 'groupPredefinedActivity' }); +PredefinedActivity.hasMany(GroupActivity, { foreignKey: 'predefinedActivityId', as: 'groupPredefinedActivities' }); export { User, @@ -116,4 +129,6 @@ export { Match, League, Team, + Group, + GroupActivity, }; diff --git a/backend/routes/diaryDateActivityRoutes.js b/backend/routes/diaryDateActivityRoutes.js index 6843aee..b781292 100644 --- a/backend/routes/diaryDateActivityRoutes.js +++ b/backend/routes/diaryDateActivityRoutes.js @@ -5,11 +5,13 @@ import { deleteDiaryDateActivity, updateDiaryDateActivityOrder, getDiaryDateActivities, + addGroupActivity, } from '../controllers/diaryDateActivityController.js'; import { authenticate } from '../middleware/authMiddleware.js'; const router = express.Router(); +router.post('/group', authenticate, addGroupActivity); router.post('/:clubId/', authenticate, createDiaryDateActivity); router.put('/:clubId/:id/order', authenticate, updateDiaryDateActivityOrder); router.put('/:clubId/:id', authenticate, updateDiaryDateActivity); diff --git a/backend/routes/groupRoutes.js b/backend/routes/groupRoutes.js new file mode 100644 index 0000000..11eee8c --- /dev/null +++ b/backend/routes/groupRoutes.js @@ -0,0 +1,10 @@ +import express from 'express'; +import { authenticate } from '../middleware/authMiddleware.js'; +import { addGroup, getGroups } from '../controllers/groupController.js'; + +const router = express.Router(); + +router.post('/', authenticate, addGroup); +router.get('/:clubId/:dateId', authenticate, getGroups); + +export default router; diff --git a/backend/server.js b/backend/server.js index 27733ce..d7f6dc9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,7 +5,8 @@ import sequelize from './database.js'; import { User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote, DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag, - PredefinedActivity, DiaryDateActivity, Match, League, Team // Import der neuen Modelle + PredefinedActivity, DiaryDateActivity, Match, League, Team, Group, + GroupActivity } from './models/index.js'; import authRoutes from './routes/authRoutes.js'; import clubRoutes from './routes/clubRoutes.js'; @@ -22,6 +23,7 @@ import diaryDateActivityRoutes from './routes/diaryDateActivityRoutes.js'; import matchRoutes from './routes/matchRoutes.js'; import Season from './models/Season.js'; import Location from './models/Location.js'; +import groupRoutes from './routes/groupRoutes.js'; const app = express(); const port = process.env.PORT || 3000; @@ -44,6 +46,7 @@ app.use('/api/diarymember', diaryMemberRoutes); app.use('/api/predefined-activities', predefinedActivityRoutes); app.use('/api/diary-date-activities', diaryDateActivityRoutes); app.use('/api/matches', matchRoutes); +app.use('/api/group', groupRoutes); app.use(express.static(path.join(__dirname, '../frontend/dist'))); @@ -76,7 +79,9 @@ app.get('*', (req, res) => { await League.sync({ alter: true }); await Team.sync({ alter: true }); await Location.sync({ alter: true }); - await Match.sync({ alter: true }); // Match zuletzt, um sicherzustellen, dass alle Referenzen existieren + await Match.sync({ alter: true }); + await Group.sync({ alter: true }); + await GroupActivity.sync({ alter: true }); app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); diff --git a/backend/services/diaryDateActivityService.js b/backend/services/diaryDateActivityService.js index fe70c29..d34a7d4 100644 --- a/backend/services/diaryDateActivityService.js +++ b/backend/services/diaryDateActivityService.js @@ -1,4 +1,6 @@ import DiaryDateActivity from '../models/DiaryDateActivity.js'; +import GroupActivity from '../models/GroupActivity.js'; +import Group from '../models/Group.js'; import PredefinedActivity from '../models/PredefinedActivity.js'; import { checkAccess } from '../utils/userUtils.js'; import { Op } from 'sequelize'; @@ -9,6 +11,7 @@ class DiaryDateActivityService { console.log('[DiaryDateActivityService::createActivity] - check user access'); await checkAccess(userToken, clubId); console.log('[DiaryDateActivityService::createActivity] - add: ', data); + const { activity, ...restData } = data; let predefinedActivity = await PredefinedActivity.findOne({ where: { name: data.activity } }); if (!predefinedActivity) { predefinedActivity = await PredefinedActivity.create({ @@ -17,21 +20,20 @@ class DiaryDateActivityService { duration: data.duration }); } + restData.predefinedActivityId = predefinedActivity.id; const maxOrderId = await DiaryDateActivity.max('orderId', { where: { diaryDateId: data.diaryDateId } }); const newOrderId = maxOrderId !== null ? maxOrderId + 1 : 1; - const { activity, ...restData } = data; - restData.predefinedActivityId = predefinedActivity.id; restData.orderId = newOrderId; console.log('[DiaryDateActivityService::createActivity] - create diary date activity'); return await DiaryDateActivity.create(restData); } async updateActivity(userToken, clubId, id, data) { - console.log('[DiaryDateActivityService::upateActivity] - check user access'); + console.log('[DiaryDateActivityService::updateActivity] - check user access'); await checkAccess(userToken, clubId); - console.log('[DiaryDateActivityService::updateActivity] - load activit', id); + console.log('[DiaryDateActivityService::updateActivity] - load activity', id); const activity = await DiaryDateActivity.findByPk(id); if (!activity) { console.log('[DiaryDateActivityService::updateActivity] - activity not found'); @@ -53,11 +55,9 @@ class DiaryDateActivityService { async updateActivityOrder(userToken, clubId, id, newOrderId) { console.log(`[DiaryDateActivityService::updateActivityOrder] - Start update for activity id: ${id}`); console.log(`[DiaryDateActivityService::updateActivityOrder] - User token: ${userToken}, Club id: ${clubId}, New order id: ${newOrderId}`); - console.log('[DiaryDateActivityService::updateActivityOrder] - Checking user access'); await checkAccess(userToken, clubId); console.log('[DiaryDateActivityService::updateActivityOrder] - User access confirmed'); - console.log(`[DiaryDateActivityService::updateActivityOrder] - Finding activity with id: ${id}`); const activity = await DiaryDateActivity.findByPk(id); if (!activity) { @@ -65,10 +65,8 @@ class DiaryDateActivityService { throw new Error('Activity not found'); } console.log('[DiaryDateActivityService::updateActivityOrder] - Activity found:', activity); - const currentOrderId = activity.orderId; console.log(`[DiaryDateActivityService::updateActivityOrder] - Current order id: ${currentOrderId}`); - if (newOrderId < currentOrderId) { console.log(`[DiaryDateActivityService::updateActivityOrder] - Shifting items down. Moving activities with orderId between ${newOrderId} and ${currentOrderId - 1}`); await DiaryDateActivity.increment( @@ -96,14 +94,11 @@ class DiaryDateActivityService { } else { console.log('[DiaryDateActivityService::updateActivityOrder] - New order id is the same as the current order id. No shift required.'); } - console.log(`[DiaryDateActivityService::updateActivityOrder] - Setting new order id for activity id: ${id}`); activity.orderId = newOrderId; - console.log('[DiaryDateActivityService::updateActivityOrder] - Saving activity with new order id'); const savedActivity = await activity.save(); console.log('[DiaryDateActivityService::updateActivityOrder] - Activity saved:', savedActivity); - console.log(`[DiaryDateActivityService::updateActivityOrder] - Finished update for activity id: ${id}`); return savedActivity; } @@ -118,13 +113,66 @@ class DiaryDateActivityService { include: [ { model: PredefinedActivity, - as: 'predefinedActivity', + as: 'predefinedActivity', + }, + { + model: GroupActivity, + as: 'groupActivities', + include: [ + { + model: Group, + as: 'groupsGroupActivity' + }, + { + model: PredefinedActivity, + as: 'groupPredefinedActivity', + }, + ], } ] }); console.log(`[DiaryDateActivityService::getActivities] - found ${activities.length} activities`); return activities; } + + async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity) { + console.log('[DiaryDateActivityService::addGroupActivity] Check user access'); + await checkAccess(userToken, clubId); + console.log('[DiaryDateActivityService::addGroupActivity] Check diary date'); + const diaryDateActivity = await DiaryDateActivity.findOne({ + where: { + diaryDateId, + isTimeblock: true, + }, + order: [['order_id', 'ASC']], + limit: 1 + }); + if (!diaryDateActivity) { + console.error('[DiaryDateActivityService::addGroupActivity] Activity not found'); + throw new Error('Activity not found'); + } + console.log('[DiaryDateActivityService::addGroupActivity] Check group'); + const group = await Group.findByPk(groupId); + if (!group || group.diaryDateId !== diaryDateActivity.diaryDateId) { + console.error('[DiaryDateActivityService::addGroupActivity] Group and date don\'t fit'); + throw new Error('Group isn\'t related to date'); + } + console.log('[DiaryDateActivityService::addGroupActivity] Get predefined activity'); + const [predefinedActivity, created] = await PredefinedActivity.findOrCreate({ + where: { + name: activity + } + }); + console.log('[DiaryDateActivityService::addGroupActivity] Add group activity'); + console.log(predefinedActivity); + const activityData = { + diaryDateActivity: diaryDateActivity.id, + groupId: groupId, + customActivity: predefinedActivity.id + } + console.log(activityData); + return await GroupActivity.create(activityData); + } } export default new DiaryDateActivityService(); diff --git a/backend/services/groupService.js b/backend/services/groupService.js new file mode 100644 index 0000000..d4ec0b8 --- /dev/null +++ b/backend/services/groupService.js @@ -0,0 +1,43 @@ +import { checkAccess } from '../utils/userUtils.js'; +import DiaryDate from '../models/DiaryDates.js'; +import HttpError from '../exceptions/HttpError.js'; +import Group from '../models/Group.js'; + +class GroupService { + + async checkDiaryDateToClub(clubId, dateId) { + const diaryDate = await DiaryDate.findOne({ + where: { + id: dateId, + clubId: clubId + } + }); + if (!diaryDate) { + throw new HttpError('Datum nicht gefunden oder passt nicht zum Verein', 404); + } + } + + async addGroup(userToken, clubId, dateId, name, lead) { + await checkAccess(userToken, clubId); + await this.checkDiaryDateToClub(clubId, dateId); + const group = await Group.create({ + diaryDateId: dateId, + name, + lead + }); + return group; + } + + async getGroups(userToken, clubId, dateId) { + await checkAccess(userToken, clubId); + await this.checkDiaryDateToClub(clubId, dateId); + const groups = await Group.findAll({ + where: { + diaryDateId: dateId + } + }); + return groups; + } +} + +export default new GroupService(); \ No newline at end of file diff --git a/backend/services/matchService.js b/backend/services/matchService.js index 8bb0778..3bb3deb 100644 --- a/backend/services/matchService.js +++ b/backend/services/matchService.js @@ -120,7 +120,7 @@ class MatchService { const leagues = await League.findAll({ include: [{ model: Match, - as: 'leagueMatches', // Verwendung des Alias 'leagueMatches' + as: 'leagueMatches', where: { seasonId: season.id, clubId: clubId diff --git a/frontend/src/components/PDFGenerator.js b/frontend/src/components/PDFGenerator.js index a7d9d98..f2b8a2a 100644 --- a/frontend/src/components/PDFGenerator.js +++ b/frontend/src/components/PDFGenerator.js @@ -6,12 +6,17 @@ class PDFGenerator { this.pdf = new jsPDF('p', 'mm', 'a4'); this.margin = margin; this.columnGap = columnGap; - this.pageHeight = 295 - margin * 2; + this.pageHeight = 297 - margin * 2; this.columnWidth = (210 - margin * 2 - columnGap) / 2; this.position = margin; this.yPos = this.position; this.xPos = margin; this.isLeftColumn = true; + this.COLUMN_START_TIME = margin; + this.COLUMN_ACTIVITY = margin + 30; + this.COLUMN_GROUP = margin + 100; + this.COLUMN_DURATION = margin + 150; + this.LINE_HEIGHT = 7; } async addSchedule(element) { @@ -38,52 +43,7 @@ class PDFGenerator { this.isLeftColumn = true; } - addHeader(title) { - this.pdf.setFontSize(12); - this.pdf.setFont('helvetica', 'bold'); - this.pdf.text(title, this.margin, this.position); - this.pdf.setLineWidth(0.5); - this.pdf.line(this.margin, this.position + 2, 210 - this.margin, this.position + 2); - this.yPos += 10; - this.position = this.yPos; - } - - addAddress(clubName, addressLines) { - this.pdf.setFontSize(10); - this.pdf.setFont('helvetica', 'bold'); - this.pdf.text(clubName, this.xPos, this.yPos); - this.yPos += 5; - - this.pdf.setFont('helvetica', 'normal'); - addressLines.forEach(line => { - this.pdf.text(line, this.xPos, this.yPos); - this.yPos += 5; - }); - this.yPos += 10; - this.checkColumnOverflow(); - } - - checkColumnOverflow() { - if (this.isLeftColumn) { - if (this.yPos > this.pageHeight) { - this.xPos += this.columnWidth + this.columnGap; - this.yPos = this.position; - this.isLeftColumn = false; - } - } else { - if (this.yPos > this.pageHeight) { - this.pdf.addPage(); - this.xPos = this.margin; - this.yPos = this.position; - this.isLeftColumn = true; - } - } - } - - addTrainingPlan(clubName, trainingDate, trainingStart, trainingEnd, trainingPlan) { - const formattedDate = new Date(trainingDate).toLocaleDateString('de-DE'); - const formattedStartTime = trainingStart.slice(0, 5); - const formattedEndTime = trainingEnd.slice(0, 5); + addHeader(clubName, formattedDate, formattedStartTime, formattedEndTime) { this.pdf.setFontSize(14); this.pdf.setFont('helvetica', 'bold'); this.pdf.text(`${clubName} - Trainingsplan`, this.margin, this.yPos); @@ -94,42 +54,88 @@ class PDFGenerator { this.yPos += 7; this.pdf.text(`Uhrzeit: ${formattedStartTime} - ${formattedEndTime}`, this.margin, this.yPos); this.yPos += 10; + } + + addTableHeaders() { this.pdf.setFont('helvetica', 'bold'); - this.pdf.text('Uhrzeit', this.margin, this.yPos); - this.pdf.text('Aktivität', this.margin + 60, this.yPos); - this.pdf.text('Länge / Gesamtzeit (Min)', this.margin + 150, this.yPos); - this.yPos += 10; + this.pdf.text('Startzeit', this.COLUMN_START_TIME, this.yPos); + this.pdf.text('Aktivität / Zeitblock', this.COLUMN_ACTIVITY, this.yPos); + this.pdf.text('Gruppe', this.COLUMN_GROUP, this.yPos); + this.pdf.text('Dauer (Min)', this.COLUMN_DURATION, this.yPos); + this.yPos += this.LINE_HEIGHT; this.pdf.setFont('helvetica', 'normal'); - trainingPlan.forEach((item, index) => { - const time = this.calculatePlanItemTime(index, trainingStart, trainingPlan); - this.pdf.text(time.slice(0, 5), this.margin, this.yPos); - this.pdf.text(item.predefinedActivity.name, this.margin + 60, this.yPos); - this.pdf.text(item.duration.toString(), this.margin + 150, this.yPos); - this.yPos += 7; + } - if (this.yPos > this.pageHeight) { - this.addNewPage(); + addTrainingPlan(clubName, trainingDate, trainingStart, trainingEnd, trainingPlan) { + const formattedDate = new Date(trainingDate).toLocaleDateString('de-DE'); + const formattedStartTime = trainingStart ? trainingStart.slice(0, 5) : ''; + const formattedEndTime = trainingEnd ? trainingEnd.slice(0, 5) : ''; + this.addHeader(clubName, formattedDate, formattedStartTime, formattedEndTime); + this.addTableHeaders(); + for (const item of trainingPlan) { + if (item.isTimeblock) { + this.addTimeBlock(item); + } else { + this.addActivity(item); } - }); + this.checkPageOverflow(); + } } - calculatePlanItemTime(index, startTime, trainingPlan) { - let time = startTime; - for (let i = 0; i < index; i++) { - time = this.addDurationToTime(time, trainingPlan[i].duration); + addTimeBlock(item) { + this.pdf.setFont('helvetica'); + this.pdf.text(item.startTime, this.COLUMN_START_TIME, this.yPos); + this.pdf.text('Zeitblock', this.COLUMN_ACTIVITY, this.yPos); + this.yPos += this.LINE_HEIGHT; + this.pdf.setFont('helvetica', 'normal'); + if (item.groupActivities && item.groupActivities.length > 0) { + this.addGroupActivities(item.groupActivities); } - return time; } - addDurationToTime(startTime, duration) { - let [hours, minutes] = startTime.split(':').map(Number); - minutes += Number(duration); - if (minutes >= 60) { - hours += Math.floor(minutes / 60); - minutes = minutes % 60; + addActivity(item) { + const startTime = item.startTime; + const activityName = item.predefinedActivity + ? item.predefinedActivity.name + : item.activity; + const groupName = item.groupActivity ? item.groupActivity.name : ''; + const duration = item.duration ? item.duration.toString() : ''; + let text = ''; + if (item.durationText) { + text = item.durationText + ' / '; + } + text += duration; + this.pdf.text(startTime, this.COLUMN_START_TIME, this.yPos); + this.pdf.text(activityName, this.COLUMN_ACTIVITY, this.yPos); + this.pdf.text(groupName, this.COLUMN_GROUP, this.yPos); + this.pdf.text(text, this.COLUMN_DURATION, this.yPos); + this.yPos += this.LINE_HEIGHT; + if (item.groupActivities && item.groupActivities.length > 0) { + this.addGroupActivities(item.groupActivities); + } + } + + addGroupActivities(groupActivities) { + for (const groupItem of groupActivities) { + const activityName = groupItem.groupPredefinedActivity + ? groupItem.groupPredefinedActivity.name + : ''; + const groupName = groupItem.groupsGroupActivity + ? groupItem.groupsGroupActivity.name + : ''; + this.pdf.text('', this.COLUMN_START_TIME, this.yPos); // Leere Startzeit + this.pdf.text(' - ' + activityName, this.COLUMN_ACTIVITY + 5, this.yPos); // Einrückung + this.pdf.text(groupName, this.COLUMN_GROUP, this.yPos); + this.pdf.text('', this.COLUMN_DURATION, this.yPos); // Leere Dauer + this.yPos += this.LINE_HEIGHT; + this.checkPageOverflow(); + } + } + + checkPageOverflow() { + if (this.yPos > this.pageHeight) { + this.addNewPage(); } - hours = hours % 24; - return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; } save(filename) { diff --git a/frontend/src/main.js b/frontend/src/main.js index ff1e984..931f116 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -5,7 +5,9 @@ import store from './store'; import '@/assets/css/main.scss'; import './assets/css/vue-multiselect.css'; -createApp(App) +const app = createApp(App); +app.config.devtools = true; +app .use(router) .use(store) .mount('#app'); diff --git a/frontend/src/store.js b/frontend/src/store.js index 232ed57..0b5ee0c 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -73,11 +73,6 @@ const store = createStore({ clubs: state => state.clubs, currentClubName: state => { const club = state.clubs.find(club => club.id === parseInt(state.currentClub)); - if (club) { - console.log('Found Club:', club.name); - } else { - console.log('Club not found'); - } return club ? club.name : ''; } }, diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index 69dbd3c..0a3baee 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -46,50 +46,105 @@
+

Gruppenverwaltung

+
+

Vorhandene Gruppen

+
    +
  • + {{ group.name }} (Leiter: {{ group.lead }}) +
  • +
+
+
+

Neue Gruppe erstellen

+
+
+ + +
+
+ + +
+
+ + +
+
+

Trainingsplan

- - - - - - - - - - - - - - - - - - - - - - - - - -
UhrzeitAktivitätLänge / Gesamtzeit (Min)
{{ calculatePlanItemTime(index) }}{{ planItem.predefinedActivity.name }}{{ planItem.durationText }} / {{ planItem.duration }}-
{{ calculateNextTime }} - - - - - - + -
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
StartzeitAktivität / ZeitblockGruppeDauer (Min)
{{ calculateNextTime }} + + + + + + Zeitblock + + + + + + + + X +
+ +
-

Teilnehmer

    @@ -116,39 +171,6 @@
- - -
- -
@@ -188,11 +210,21 @@ export default { activity: '', duration: '', durationText: '', + groupId: '', + isTimeBlock: false, }, predefinedActivities: [], showDropdown: false, showImage: false, imageUrl: '', + groups: [], + currentTimeBlockId: null, + newGroupName: '', + newGroupLead: '', + addtype: 'activity', + addNewItem: false, + addNewGroupActivity: false, + addNewTimeblock: false, }; }, watch: { @@ -206,11 +238,12 @@ export default { computed: { ...mapGetters(['isAuthenticated', 'currentClub', 'currentClubName']), calculateNextTime() { - let lastTime = this.trainingStart; - for (let item of this.trainingPlan) { - lastTime = this.addDurationToTime(lastTime, item.duration); + this.calculateAllItemTimes(); + if (this.trainingPlan.length === 0) { + return this.trainingStart; } - return lastTime; + const lastItem = this.trainingPlan[this.trainingPlan.length - 1]; + return lastItem.endTime; }, filteredPredefinedActivities() { const input = this.newPlanItem.activity.toLowerCase(); @@ -224,8 +257,8 @@ export default { if (this.isAuthenticated && this.currentClub) { const response = await apiClient.get(`/diary/${this.currentClub}`); this.dates = response.data.map(entry => ({ id: entry.id, date: entry.date })); - this.loadTags(); - this.loadPredefinedActivities(); + await this.loadTags(); + await this.loadPredefinedActivities(); } }, setCurrentDate() { @@ -245,7 +278,6 @@ export default { name: tag.name })); this.previousActivityTags = [...this.selectedActivityTags]; - await this.loadMembers(); await this.loadParticipants(dateId); await this.loadActivities(dateId); @@ -254,6 +286,7 @@ export default { .then(response => response.data); this.initializeSortable(); + await this.loadGroups(); } else { this.newDate = ''; this.trainingStart = ''; @@ -275,7 +308,6 @@ export default { trainingStart: this.trainingStart || null, trainingEnd: this.trainingEnd || null, }); - console.log(response); this.dates.push({ id: response.data.id, date: response.data.date }); this.date = { id: response.data.id, date: response.data.date }; this.showForm = false; @@ -324,6 +356,14 @@ export default { console.error('Fehler beim Laden der vordefinierten Aktivitäten:', error); } }, + async loadGroups() { + try { + const response = await apiClient.get(`/group/${this.currentClub}/${this.date.id}`); + this.groups = response.data; + } catch (error) { + console.log(error); + } + }, isParticipant(memberId) { return this.participants.includes(memberId); }, @@ -359,7 +399,7 @@ export default { }, async openNotesModal(member) { this.selectedMember = member; - await this.loadMemberImage(member); + await this.loadMemberImage(member); this.loadMemberNotesAndTags(this.date.id, member.id); this.showNotesModal = true; }, @@ -467,7 +507,6 @@ export default { const selectedTags = this.selectedActivityTags; if (!selectedTags || !Array.isArray(selectedTags)) { - console.log(typeof selectedTags, JSON.stringify(selectedTags)); throw new TypeError('Expected selectedTags to be an array'); } @@ -549,36 +588,95 @@ export default { }, async addPlanItem() { try { - await apiClient.post(`/diary-date-activities/${this.currentClub}`, { - diaryDateId: this.date.id, - activity: this.newPlanItem.activity, - duration: this.newPlanItem.duration, - durationText: this.newPlanItem.durationText, - orderId: this.trainingPlan.length - }); - this.newPlanItem = { activity: '', duration: '', durationText: '' }; + if (this.addNewItem || this.addNewTimeblock) { + await apiClient.post(`/diary-date-activities/${this.currentClub}`, { + diaryDateId: this.date.id, + activity: this.addNewTimeblock ? '' : this.newPlanItem.activity, + isTimeblock: this.addNewTimeblock, + duration: this.newPlanItem.duration, + durationText: this.newPlanItem.durationText, + orderId: this.trainingPlan.length + }); + } else if (this.addNewGroupActivity) {//clubId, diaryDateActivityId, groupId, activity + await apiClient.post(`/diary-date-activities/group`, { + clubId: this.currentClub, + diaryDateId: this.date.id, + activity: this.newPlanItem.activity, + groupId: this.newPlanItem.groupId, + }); + } + this.addNewTimeblock = false; + this.addNewItem = false; + this.newPlanItem = { activity: '', duration: '', durationText: '', groupId: '' }; this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data); } catch (error) { console.error('Fehler beim Hinzufügen des Planungsitems:', error); alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.'); } }, + async updatePlanItemGroup(planItemId, groupId) { + try { + await apiClient.put(`/diary-date-activities/${this.currentClub}/${planItemId}/group`, { + groupId: groupId + }); + } catch (error) { + console.error('Fehler beim Aktualisieren der Planungs-Item-Gruppe:', error); + } + }, async removePlanItem(planItemId) { try { - await apiClient.delete(`/diary-date-activities/${this.currentClub}`, { - params: { planItemId } - }); - this.planItems = this.planItems.filter(item => item.id !== planItemId); + await apiClient.delete(`/diary-date-activities/${this.currentClub}/${planItemId}`); + this.trainingPlan = this.trainingPlan.filter(item => item.id !== planItemId); } catch (error) { console.error('Fehler beim Entfernen des Planungsitems:', error); } }, - calculatePlanItemTime(index) { - let time = this.trainingStart; - for (let i = 0; i < index; i++) { - time = this.addDurationToTime(time, this.trainingPlan[i].duration); + + isTimeGreater(time1, time2) { + const [hours1, minutes1] = time1.split(':').map(Number); + const [hours2, minutes2] = time2.split(':').map(Number); + + if (hours1 > hours2) return true; + if (hours1 < hours2) return false; + return minutes1 > minutes2; + }, + calculateAllItemTimes() { + let currentTime = this.trainingStart; + let index = 0; + const trainingPlanLength = this.trainingPlan.length; + + while (index < trainingPlanLength) { + const currentItem = this.trainingPlan[index]; + + if (!currentItem.groupId) { + currentItem.startTime = currentTime; + currentItem.endTime = this.addDurationToTime(currentItem.startTime, currentItem.duration); + currentTime = currentItem.endTime; + index += 1; + } else { + const groupActivities = []; + while (index < trainingPlanLength && this.trainingPlan[index].groupId) { + groupActivities.push(this.trainingPlan[index]); + index += 1; + } + for (const item of groupActivities) { + item.startTime = currentTime; + item.endTime = this.addDurationToTime(item.startTime, item.duration); + } + let latestEndTime = currentTime; + for (const item of groupActivities) { + if (this.isTimeGreater(item.endTime, latestEndTime)) { + latestEndTime = item.endTime; + } + } + currentTime = latestEndTime; + } } - return time; + }, + + calculatePlanItemTime(index) { + this.calculateAllItemTimes(); + return this.trainingPlan[index].startTime; }, addDurationToTime(startTime, duration) { let [hours, minutes] = startTime.split(':').map(Number); @@ -670,20 +768,57 @@ export default { this.imageUrl = URL.createObjectURL(response.data); } catch (error) { console.error("Failed to load member image:", error); - this.imageUrl = null; + this.imageUrl = null; } }, recalculateTimes() { - let currentTime = this.trainingStart; // Die Startzeit des Trainings + let currentTime = this.trainingStart; this.trainingPlan.forEach((item, index) => { - item.startTime = currentTime; - currentTime = this.addDurationToTime(currentTime, item.duration); + item.startTime = currentTime; + currentTime = this.addDurationToTime(currentTime, item.duration); }); }, + async addGroup() { + try { + const form = { + clubid: this.currentClub, + dateid: this.date.id, + name: this.newGroupName, + lead: this.newGroupLead, + } + await apiClient.post('/group', form); + await this.loadGroups(); + } catch (error) { + + } + }, + parentIsTimeblock() { + return this.trainingPlan && this.trainingPlan.length > 0 ? (this.trainingPlan[this.trainingPlan.length - 1].isTimeblock) : false; + }, + openNewPlanItem() { + this.addNewItem = true; + this.addNewGroupActivity = false; + this.addNewTimeblock = false; + }, + cancelAddItem() { + this.addNewItem = false; + this.addNewGroupActivity = false; + this.addNewTimeblock = false; + }, + addTimeblock() { + this.addNewTimeblock = true; + this.addNewGroupActivity = false; + this.addNewItem = false; + }, + async addGroupActivity() { + this.addNewGroupActivity = true; + this.addNewItem = false; + this.addNewTimeblock = false; + }, }, async mounted() { await this.init(); - } + }, }; @@ -703,6 +838,7 @@ button[type="button"] { h3 { display: block; } + .columns { display: flex; justify-content: space-between; @@ -843,6 +979,22 @@ input[type="number"] { line-height: 1.2em; font-weight: bold; margin-left: 5px; + border-radius: 2px; +} + +.cancel { + border: 1px solid black; + cursor: pointer; + display: inline-block; + width: 1.2em; + height: 1.2em; + text-align: center; + line-height: 1.2em; + font-weight: bold; + margin-left: 5px; + color: #ff0000; + font-weight: bold; + border-radius: 2px; } .add-plan-item:hover { @@ -871,8 +1023,8 @@ input[type="number"] { flex: 1; } -.img-icon { - cursor: pointer; +img { + max-width: 100%; } .memberImage { @@ -886,8 +1038,14 @@ input[type="number"] { overflow: hidden; padding: 3px; } -.memberImage > img { + +.memberImage>img { max-width: 100%; max-height: 100%; } + +.groups { + display: flex; + flex-direction: row; +} diff --git a/frontend/src/views/ScheduleView.vue b/frontend/src/views/ScheduleView.vue index 041e320..7649010 100644 --- a/frontend/src/views/ScheduleView.vue +++ b/frontend/src/views/ScheduleView.vue @@ -142,7 +142,6 @@ export default { }, getCurrentClubName() { const club = this.clubs.find(club => club.id === this.currentClub); - console.log(club, this.currentClub); return club ? club.name : ''; }, async generatePDF() { @@ -164,7 +163,6 @@ export default { }, getUniqueLocations() { const uniqueLocations = new Map(); - this.matches.forEach(match => { const location = match.location; const clubName = match.homeTeam.name; @@ -173,9 +171,7 @@ export default { location.address, `${location.zip} ${location.city}` ]; - - const addressKey = addressLines.join('; '); // Unique key für die Map - + const addressKey = addressLines.join('; '); if (!uniqueLocations.has(addressKey)) { uniqueLocations.set(clubName, addressLines); }