diff --git a/backend/controllers/diaryDateActivityController.js b/backend/controllers/diaryDateActivityController.js new file mode 100644 index 0000000..cc014df --- /dev/null +++ b/backend/controllers/diaryDateActivityController.js @@ -0,0 +1,73 @@ +import diaryDateActivityService from '../services/diaryDateActivityService.js'; + +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 activityItem = await diaryDateActivityService.createActivity(userToken, clubId, { + diaryDateId, + activity, + duration, + durationText, + orderId, + }); + res.status(201).json(activityItem); + } catch (error) { + console.log(error); + res.status(500).json({ error: 'Error creating activity' }); + } +}; + +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 updatedActivity = await diaryDateActivityService.updateActivity(userToken, clubId, id, { + predefinedActivityId, + customActivityName, + duration, + durationText, + orderId, + }); + res.status(200).json(updatedActivity); + } catch (error) { + res.status(500).json({ error: 'Error updating activity' }); + } +}; + +export const deleteDiaryDateActivity = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { clubId, id } = req.params; + await diaryDateActivityService.deleteActivity(userToken, clubId, id); + res.status(200).json({ message: 'Activity deleted' }); + } catch (error) { + res.status(500).json({ error: 'Error deleting activity' }); + } +}; + +export const updateDiaryDateActivityOrder = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { clubId, id } = req.params; + const { orderId } = req.body; + const updatedActivity = await diaryDateActivityService.updateActivityOrder(userToken, clubId, id, orderId); + res.status(200).json(updatedActivity); + } catch (error) { + console.log(error); + res.status(500).json({ error: 'Error updating activity order' }); + } +}; + +export const getDiaryDateActivities = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { clubId, diaryDateId } = req.params; + const activities = await diaryDateActivityService.getActivities(userToken, clubId, diaryDateId); + res.status(200).json(activities); + } catch (error) { + res.status(500).json({ error: 'Error getting activities' }); + } +} \ No newline at end of file diff --git a/backend/controllers/diaryMemberController.js b/backend/controllers/diaryMemberController.js index 786967d..337bba0 100644 --- a/backend/controllers/diaryMemberController.js +++ b/backend/controllers/diaryMemberController.js @@ -58,8 +58,8 @@ const addMemberTag = async (req, res) => { const removeMemberNote = async (req, res) => { try { const { authcode: userToken } = req.headers; - const { memberId, diaryDateId, content } = req.body; - await DiaryMemberService.removeNoteFromMember(userToken, req.params.clubId, diaryDateId, memberId, content); + const { clubId, noteId } = req.params; + await DiaryMemberService.removeNoteFromMember(userToken, clubId, noteId); const notes = await DiaryMemberService.getNotesForMember(userToken, req.params.clubId, diaryDateId, memberId); res.status(200).json(notes); } catch (error) { diff --git a/backend/controllers/predefinedActivityController.js b/backend/controllers/predefinedActivityController.js new file mode 100644 index 0000000..a5d27e9 --- /dev/null +++ b/backend/controllers/predefinedActivityController.js @@ -0,0 +1,48 @@ +import predefinedActivityService from '../services/predefinedActivityService.js'; + +export const createPredefinedActivity = async (req, res) => { + try { + const { name, description, durationText, duration } = req.body; + const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, description, durationText, duration }); + res.status(201).json(predefinedActivity); + } catch (error) { + console.error('[createPredefinedActivity] - Error:', error); + res.status(500).json({ error: 'Error creating predefined activity' }); + } +}; + +export const getAllPredefinedActivities = async (req, res) => { + try { + const predefinedActivities = await predefinedActivityService.getAllPredefinedActivities(); + res.status(200).json(predefinedActivities); + } catch (error) { + console.error('[getAllPredefinedActivities] - Error:', error); + res.status(500).json({ error: 'Error fetching predefined activities' }); + } +}; + +export const getPredefinedActivityById = async (req, res) => { + try { + const { id } = req.params; + const predefinedActivity = await predefinedActivityService.getPredefinedActivityById(id); + if (!predefinedActivity) { + return res.status(404).json({ error: 'Predefined activity not found' }); + } + res.status(200).json(predefinedActivity); + } catch (error) { + console.error('[getPredefinedActivityById] - Error:', error); + res.status(500).json({ error: 'Error fetching predefined activity' }); + } +}; + +export const updatePredefinedActivity = async (req, res) => { + try { + const { id } = req.params; + const { name, description, durationText, duration } = req.body; + const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, description, durationText, duration }); + res.status(200).json(updatedActivity); + } catch (error) { + console.error('[updatePredefinedActivity] - Error:', error); + res.status(500).json({ error: 'Error updating predefined activity' }); + } +}; diff --git a/backend/models/DiaryDateActivity.js b/backend/models/DiaryDateActivity.js new file mode 100644 index 0000000..2390262 --- /dev/null +++ b/backend/models/DiaryDateActivity.js @@ -0,0 +1,52 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; +import DiaryDate from './DiaryDates.js'; +import PredefinedActivity from './PredefinedActivity.js'; + +const DiaryDateActivity = sequelize.define('DiaryDateActivity', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + diaryDateId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: DiaryDate, + key: 'id', + }, + onDelete: 'CASCADE', + }, + predefinedActivityId: { + type: DataTypes.INTEGER, + allowNull: true, + references: { + model: PredefinedActivity, + key: 'id', + }, + onDelete: 'SET NULL', + }, + customActivityName: { + type: DataTypes.STRING, + allowNull: true, + }, + duration: { + type: DataTypes.INTEGER, + allowNull: true, + }, + durationText: { + type: DataTypes.STRING, + allowNull: true, + }, + orderId: { + type: DataTypes.INTEGER, + allowNull: false, + }, +}, { + tableName: 'diary_date_activities', + underscored: true, + timestamps: true, +}); + +export default DiaryDateActivity; diff --git a/backend/models/DiaryDates.js b/backend/models/DiaryDates.js index b586365..86082f5 100644 --- a/backend/models/DiaryDates.js +++ b/backend/models/DiaryDates.js @@ -30,5 +30,4 @@ const DiaryDate = sequelize.define('DiaryDate', { timestamps: true }); - export default DiaryDate; diff --git a/backend/models/DiaryMemberNote.js b/backend/models/DiaryMemberNote.js index 7bc50be..7d476c5 100644 --- a/backend/models/DiaryMemberNote.js +++ b/backend/models/DiaryMemberNote.js @@ -2,6 +2,7 @@ import { DataTypes } from 'sequelize'; import sequelize from '../database.js'; import Member from './Member.js'; import DiaryDate from './DiaryDates.js'; +import { encryptData, decryptData } from '../utils/encrypt.js'; const DiaryMemberNote = sequelize.define('DiaryMemberNote', { memberId: { @@ -25,6 +26,14 @@ const DiaryMemberNote = sequelize.define('DiaryMemberNote', { content: { type: DataTypes.STRING(4096), allowNull: false, + set(value) { + const encryptedValue = encryptData(value); + this.setDataValue('content', encryptedValue); + }, + get() { + const encryptedValue = this.getDataValue('content'); + return decryptData(encryptedValue); + } }, }, { underscored: true, diff --git a/backend/models/Participant.js b/backend/models/Participant.js index de50e3b..5bbcb85 100644 --- a/backend/models/Participant.js +++ b/backend/models/Participant.js @@ -2,6 +2,7 @@ import { DataTypes } from 'sequelize'; import sequelize from '../database.js'; import Member from './Member.js'; import DiaryDate from './DiaryDates.js'; +import { encryptData, decryptData } from '../utils/encrypt.js'; const Participant = sequelize.define('Participant', { id: { @@ -29,6 +30,18 @@ const Participant = sequelize.define('Participant', { notes: { type: DataTypes.STRING(4096), allowNull: true, + set(value) { + const encryptedValue = encryptData(value); + this.setDataValue('notes', encryptedValue); + }, + get() { + try { + const encryptedValue = this.getDataValue('notes'); + return encryptData ? decryptData(encryptedValue) : null; + } catch (error) { + return null; + } + } } }, { underscored: true, diff --git a/backend/models/PredefinedActivity.js b/backend/models/PredefinedActivity.js new file mode 100644 index 0000000..89f1253 --- /dev/null +++ b/backend/models/PredefinedActivity.js @@ -0,0 +1,32 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; + +const PredefinedActivity = sequelize.define('PredefinedActivity', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + description: { + type: DataTypes.TEXT, + allowNull: true, + }, + durationText: { + type: DataTypes.STRING, + allowNull: true, + }, + duration: { + type: DataTypes.INTEGER, + allowNull: true, + }, +}, { + tableName: 'predefined_activities', + timestamps: true, + underscored: true, +}); + +export default PredefinedActivity; diff --git a/backend/models/index.js b/backend/models/index.js index 57a2dc9..946c1c9 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -12,6 +12,8 @@ import MemberNote from './MemberNote.js'; import DiaryDateTag from './DiaryDateTag.js'; import DiaryMemberNote from './DiaryMemberNote.js'; import DiaryMemberTag from './DiaryMemberTag.js'; +import PredefinedActivity from './PredefinedActivity.js'; +import DiaryDateActivity from './DiaryDateActivity.js'; User.hasMany(Log, { foreignKey: 'userId' }); Log.belongsTo(User, { foreignKey: 'userId' }); @@ -45,10 +47,16 @@ DiaryTag.belongsToMany(DiaryDate, { through: DiaryDateTag, as: 'diaryDates', for DiaryDate.belongsToMany(Member, { through: DiaryMemberNote, as: 'noteMembers', foreignKey: 'diaryDateId' }); Member.belongsToMany(DiaryDate, { through: DiaryMemberNote, as: 'noteDates', foreignKey: 'memberId' }); - + DiaryTag.hasMany(DiaryMemberTag, { foreignKey: 'tagId', as: 'diaryMemberTags' }); DiaryMemberTag.belongsTo(DiaryTag, { foreignKey: 'tagId', as: 'tag' }); +DiaryDate.hasMany(DiaryDateActivity, { foreignKey: 'diaryDateId', as: 'diaryDateActivities' }); +DiaryDateActivity.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDate' }); + +PredefinedActivity.hasMany(DiaryDateActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivities' }); +DiaryDateActivity.belongsTo(PredefinedActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivity' }); + export { User, Log, @@ -65,4 +73,6 @@ export { DiaryDateTag, DiaryMemberNote, DiaryMemberTag, + PredefinedActivity, + DiaryDateActivity, }; diff --git a/backend/routes/diaryDateActivityRoutes.js b/backend/routes/diaryDateActivityRoutes.js new file mode 100644 index 0000000..6843aee --- /dev/null +++ b/backend/routes/diaryDateActivityRoutes.js @@ -0,0 +1,19 @@ +import express from 'express'; +import { + createDiaryDateActivity, + updateDiaryDateActivity, + deleteDiaryDateActivity, + updateDiaryDateActivityOrder, + getDiaryDateActivities, +} from '../controllers/diaryDateActivityController.js'; +import { authenticate } from '../middleware/authMiddleware.js'; + +const router = express.Router(); + +router.post('/:clubId/', authenticate, createDiaryDateActivity); +router.put('/:clubId/:id/order', authenticate, updateDiaryDateActivityOrder); +router.put('/:clubId/:id', authenticate, updateDiaryDateActivity); +router.delete('/:clubId/:id', authenticate, deleteDiaryDateActivity); +router.get('/:clubId/:diaryDateId', authenticate, getDiaryDateActivities); + +export default router; diff --git a/backend/routes/diaryMemberRoutes.js b/backend/routes/diaryMemberRoutes.js index 223f8ba..b512093 100644 --- a/backend/routes/diaryMemberRoutes.js +++ b/backend/routes/diaryMemberRoutes.js @@ -9,7 +9,7 @@ router.get('/:clubId/tag', authenticate, getMemberTags); router.get('/:clubId/note', authenticate, getMemberNotes); router.post('/:clubId/note', authenticate, addMemberNote); router.post('/:clubId/tag', authenticate, addMemberTag); -router.post('/:clubId/note/remove', authenticate, removeMemberNote); +router.delete('/:clubId/note/:noteId', authenticate, removeMemberNote); router.post('/:clubId/tag/remove', authenticate, removeMemberTag); export default router; diff --git a/backend/routes/predefinedActivityRoutes.js b/backend/routes/predefinedActivityRoutes.js new file mode 100644 index 0000000..9da53f9 --- /dev/null +++ b/backend/routes/predefinedActivityRoutes.js @@ -0,0 +1,16 @@ +import express from 'express'; +import { + createPredefinedActivity, + getAllPredefinedActivities, + getPredefinedActivityById, + updatePredefinedActivity, +} from '../controllers/predefinedActivityController.js'; + +const router = express.Router(); + +router.post('/', createPredefinedActivity); +router.get('/', getAllPredefinedActivities); +router.get('/:id', getPredefinedActivityById); +router.put('/:id', updatePredefinedActivity); + +export default router; diff --git a/backend/server.js b/backend/server.js index ddb7d6b..b910896 100644 --- a/backend/server.js +++ b/backend/server.js @@ -15,10 +15,14 @@ import activityRoutes from './routes/activityRoutes.js'; import memberNoteRoutes from './routes/memberNoteRoutes.js'; import diaryTagRoutes from './routes/diaryTagRoutes.js'; import diaryNoteRoutes from './routes/diaryNoteRoutes.js'; -import diaryMemberRoutes from './routes/diaryMemberRoutes.js'; // Neue Route +import diaryMemberRoutes from './routes/diaryMemberRoutes.js'; +import predefinedActivityRoutes from './routes/predefinedActivityRoutes.js'; +import PredefinedActivity from './models/PredefinedActivity.js'; +import DiaryDateActivity from './models/DiaryDateActivity.js'; +import diaryDateActivityRoutes from './routes/diaryDateActivityRoutes.js'; const app = express(); -const port = process.env.PORT || 3000; +const port = process.env.PORT || 3000; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -35,6 +39,8 @@ app.use('/api/membernotes', memberNoteRoutes); app.use('/api/diarynotes', diaryNoteRoutes); app.use('/api/tags', diaryTagRoutes); app.use('/api/diarymember', diaryMemberRoutes); // Neue Route für Diary-Member-Funktionalität +app.use('/api/predefined-activities', predefinedActivityRoutes); +app.use('/api/diary-date-activities', diaryDateActivityRoutes); app.use(express.static(path.join(__dirname, '../frontend/dist'))); @@ -61,6 +67,8 @@ app.get('*', (req, res) => { await DiaryDateTag.sync({ alter: true }); await DiaryMemberTag.sync({ alter: true }); await DiaryMemberNote.sync({ alter: true }); + await PredefinedActivity.sync({ alter: true }); + await DiaryDateActivity.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 new file mode 100644 index 0000000..9e29f8b --- /dev/null +++ b/backend/services/diaryDateActivityService.js @@ -0,0 +1,130 @@ +import DiaryDateActivity from '../models/DiaryDateActivity.js'; +import PredefinedActivity from '../models/PredefinedActivity.js'; +import { checkAccess } from '../utils/userUtils.js'; +import { Op } from 'sequelize'; + +class DiaryDateActivityService { + + async createActivity(userToken, clubId, data) { + console.log('[DiaryDateActivityService::createActivity] - check user access'); + await checkAccess(userToken, clubId); + console.log('[DiaryDateActivityService::createActivity] - add: ', data); + let predefinedActivity = await PredefinedActivity.findOne({ where: { name: data.activity } }); + if (!predefinedActivity) { + predefinedActivity = await PredefinedActivity.create({ + name: data.activity, + description: '', + duration: data.duration + }); + } + 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'); + await checkAccess(userToken, clubId); + console.log('[DiaryDateActivityService::updateActivity] - load activit', id); + const activity = await DiaryDateActivity.findByPk(id); + if (!activity) { + console.log('[DiaryDateActivityService::updateActivity] - activity not found'); + throw new Error('Activity not found'); + } + console.log('[DiaryDateActivityService::updateActivity] - update activity'); + return await activity.update(data); + } + + async deleteActivity(userToken, clubId, id) { + await checkAccess(userToken, clubId); + const activity = await DiaryDateActivity.findByPk(id); + if (!activity) { + throw new Error('Activity not found'); + } + return await activity.destroy(); + } + + 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) { + console.error('[DiaryDateActivityService::updateActivityOrder] - Activity not found, throwing error'); + 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( + { orderId: 1 }, + { + where: { + diaryDateId: activity.diaryDateId, + orderId: { [Op.gte]: newOrderId, [Op.lt]: currentOrderId }, + }, + } + ); + console.log(`[DiaryDateActivityService::updateActivityOrder] - Items shifted down`); + } else if (newOrderId > currentOrderId) { + console.log(`[DiaryDateActivityService::updateActivityOrder] - Shifting items up. Moving activities with orderId between ${currentOrderId + 1} and ${newOrderId}`); + await DiaryDateActivity.decrement( + { orderId: 1 }, + { + where: { + diaryDateId: activity.diaryDateId, + orderId: { [Op.lte]: newOrderId, [Op.gt]: currentOrderId }, + }, + } + ); + console.log(`[DiaryDateActivityService::updateActivityOrder] - Items shifted up`); + } 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; + } + + async getActivities(userToken, clubId, diaryDateId) { + console.log('[DiaryDateActivityService::getActivities] - check user access'); + await checkAccess(userToken, clubId); + console.log(`[DiaryDateActivityService::getActivities] - fetch activities for diaryDateId: ${diaryDateId}`); + const activities = await DiaryDateActivity.findAll({ + where: { diaryDateId }, + order: [['orderId', 'ASC']], + include: [ + { + model: PredefinedActivity, + as: 'predefinedActivity', // Assuming 'predefinedActivity' is the alias used in the model association + } + ] + }); + console.log(`[DiaryDateActivityService::getActivities] - found ${activities.length} activities`); + return activities; + } +} + +export default new DiaryDateActivityService(); diff --git a/backend/services/diaryMemberService.js b/backend/services/diaryMemberService.js index 411aab5..86c5d1b 100644 --- a/backend/services/diaryMemberService.js +++ b/backend/services/diaryMemberService.js @@ -33,10 +33,9 @@ class DiaryMemberService { }); } - async removeNoteFromMember(userToken, clubId, diaryDateId, memberId, content) { + async removeNoteFromMember(userToken, clubId, noteId) { await checkAccess(userToken, clubId); - - const note = await DiaryMemberNote.findOne({ where: { diaryDateId, memberId, content } }); + const note = await DiaryMemberNote.findOne({ where: { id: noteId } }); if (note) { await note.destroy(); } else { diff --git a/backend/services/predefinedActivityService.js b/backend/services/predefinedActivityService.js new file mode 100644 index 0000000..aa6a688 --- /dev/null +++ b/backend/services/predefinedActivityService.js @@ -0,0 +1,45 @@ +import PredefinedActivity from '../models/PredefinedActivity.js'; + +class PredefinedActivityService { + async createPredefinedActivity(data) { + console.log('[PredefinedActivityService::createPredefinedActivity] - Creating predefined activity'); + return await PredefinedActivity.create({ + name: data.name, + description: data.description, + durationText: data.durationText, + duration: data.duration, + }); + } + + async updatePredefinedActivity(id, data) { + console.log(`[PredefinedActivityService::updatePredefinedActivity] - Updating predefined activity with id: ${id}`); + const activity = await PredefinedActivity.findByPk(id); + if (!activity) { + console.log('[PredefinedActivityService::updatePredefinedActivity] - Activity not found'); + throw new Error('Predefined activity not found'); + } + return await activity.update({ + name: data.name, + description: data.description, + durationText: data.durationText, + duration: data.duration, + }); + } + + async getAllPredefinedActivities() { + console.log('[PredefinedActivityService::getAllPredefinedActivities] - Fetching all predefined activities'); + return await PredefinedActivity.findAll(); + } + + async getPredefinedActivityById(id) { + console.log(`[PredefinedActivityService::getPredefinedActivityById] - Fetching predefined activity with id: ${id}`); + const activity = await PredefinedActivity.findByPk(id); + if (!activity) { + console.log('[PredefinedActivityService::getPredefinedActivityById] - Activity not found'); + throw new Error('Predefined activity not found'); + } + return activity; + } +} + +export default new PredefinedActivityService(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 81953cd..c95c157 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "axios": "^1.7.3", "core-js": "^3.8.3", + "sortablejs": "^1.15.3", "vue": "^3.2.13", "vue-multiselect": "^3.0.0", "vue-router": "^4.4.0", @@ -33,11 +34,18 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -129,9 +137,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.0.tgz", - "integrity": "sha512-CzdIU9jdP0dg7HdyB+bHvDJGagUv+qtzZt5rYCWwW6tITNqV9odjp6Qu41gkG0ca5UfdDUWrKkiAnHHdGRnOrA==", + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dependencies": { + "@babel/types": "^7.25.6" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -139,6 +150,19 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/android-arm": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", @@ -1673,11 +1697,11 @@ "dev": true }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/mime-db": { @@ -2093,6 +2117,11 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/sortablejs": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.3.tgz", + "integrity": "sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg==" + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -2201,6 +2230,14 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8a00b47..e486f36 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "dependencies": { "axios": "^1.7.3", "core-js": "^3.8.3", + "sortablejs": "^1.15.3", "vue": "^3.2.13", "vue-multiselect": "^3.0.0", "vue-router": "^4.4.0", diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index 81735d6..b745cec 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -19,11 +19,11 @@
- +
- +
@@ -33,11 +33,11 @@
- +
- +
@@ -45,6 +45,52 @@
+
+

Trainingsplan

+ + + + + + + + + + + + + + + + + + + + + + + +
UhrzeitAktivitätLänge / Gesamtzeit (Min)
{{ calculatePlanItemTime(index) }}{{ planItem.predefinedActivity.name }} + - + {{ planItem.duration }} +
{{ calculateNextTime }} + + + + + + + +
+
+

Teilnehmer

    @@ -52,13 +98,11 @@ -
-
-

Aktivitäten

@@ -100,6 +144,7 @@ import { mapGetters } from 'vuex'; import apiClient from '../apiClient.js'; import Multiselect from 'vue-multiselect'; +import Sortable from 'sortablejs'; export default { name: 'DiaryView', @@ -125,22 +170,39 @@ export default { availableTags: [], previousActivityTags: [], previousMemberTags: [], + trainingPlan: [], + newPlanItem: { + activity: '', + duration: '', + durationText: '', + }, + predefinedActivities: [], + showDropdown: false, }; }, watch: { - selectedMemberTags(newTags, oldTags) { + selectedMemberTags(newTags) { this.updateMemberTags(newTags); }, - selectedMemberNotes(newNotes, oldNotes) { - const removedNotes = oldNotes.filter(note => !newNotes.includes(note)); - removedNotes.forEach(note => this.removeMemberNote(note.content)); - }, - selectedActivityTags(newTags, oldTags) { + selectedActivityTags(newTags) { this.updateActivityTags(newTags); }, }, computed: { ...mapGetters(['isAuthenticated', 'currentClub']), + calculateNextTime() { + let lastTime = this.trainingStart; + for (let item of this.trainingPlan) { + lastTime = this.addDurationToTime(lastTime, item.duration); + } + return lastTime; + }, + filteredPredefinedActivities() { + const input = this.newPlanItem.activity.toLowerCase(); + return this.predefinedActivities.filter(activity => + activity.name.toLowerCase().includes(input) + ); + }, }, methods: { async init() { @@ -148,16 +210,7 @@ export default { const response = await apiClient.get(`/diary/${this.currentClub}`); this.dates = response.data.map(entry => ({ id: entry.id, date: entry.date })); this.loadTags(); - } - }, - handleEnterKey(event) { - const newTagName = event.target.value.trim(); - if (newTagName) { - if (this.showNotesModal) { - this.addNewTagForMember(newTagName); - } else { - this.addNewTag(newTagName); - } + this.loadPredefinedActivities(); } }, async handleDateChange() { @@ -172,11 +225,16 @@ export default { id: tag.id, name: tag.name })); - this.previousActivityTags = [...this.selectedActivityTags]; // Hier setzen + this.previousActivityTags = [...this.selectedActivityTags]; // Hier setzen await this.loadMembers(); await this.loadParticipants(dateId); await this.loadActivities(dateId); + this.trainingPlan = await apiClient + .get(`/diary-date-activities/${this.currentClub}/${this.date.id}`) + .then(response => response.data); + + this.initializeSortable(); } else { this.newDate = ''; this.trainingStart = ''; @@ -184,9 +242,12 @@ export default { this.participants = []; } }, - setCurrentDate() { - const today = new Date().toISOString().split('T')[0]; - this.newDate = today; + initializeSortable() { + const el = this.$refs.sortableList; + Sortable.create(el, { + handle: ".drag-handle", + onEnd: this.onDragEnd, + }); }, async createDate() { try { @@ -202,7 +263,6 @@ export default { this.trainingStart = ''; this.trainingEnd = ''; } catch (error) { - console.error('Fehler beim Erstellen des Datums:', error); alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.'); } }, @@ -236,6 +296,14 @@ export default { const response = await apiClient.get('/tags'); this.availableTags = response.data; }, + async loadPredefinedActivities() { + try { + const response = await apiClient.get('/predefined-activities'); + this.predefinedActivities = response.data; + } catch (error) { + console.error('Fehler beim Laden der vordefinierten Aktivitäten:', error); + } + }, isParticipant(memberId) { return this.participants.includes(memberId); }, @@ -284,7 +352,7 @@ export default { params: { diaryDateId, memberId } }); this.selectedMemberTags = tagsResponse.data.map(tag => ({ - id: tag.tag.id, + id: tag.tag.id, name: tag.tag.name })); } catch (error) { @@ -294,7 +362,7 @@ export default { }, async addMemberNote() { if (this.newNoteContent) { - const response = await apiClient.post(`/diarymember/${this.currentClub}/notes`, { + const response = await apiClient.post(`/diarymember/${this.currentClub}/note`, { memberId: this.selectedMember.id, diaryDateId: this.date.id, content: this.newNoteContent @@ -305,8 +373,8 @@ export default { } }, async deleteNote(noteId) { - const response = await apiClient.delete(`/diarymember/note/${noteId}`, { - data: { clubId: this.currentClub } + const response = await apiClient.delete(`/diarymember/${this.currentClub}/note/${noteId}`, { + clubId: this.currentClub }); this.notes = response.data; }, @@ -362,7 +430,6 @@ export default { } }, async updateActivityTags(selectedTags) { - console.log('test'); try { for (let tag of selectedTags) { if (!this.previousActivityTags.includes(tag)) { @@ -426,8 +493,102 @@ export default { alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.'); } }, + handleActivityInput() { + if (this.newPlanItem.activity) { + this.showDropdown = true; + } else { + this.showDropdown = false; + } + }, + selectPredefinedActivity(activity) { + this.newPlanItem.activity = activity.name; + this.newPlanItem.durationText = activity.durationText; + this.newPlanItem.duration = activity.duration || ''; + this.showDropdown = false; + }, + 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: '' }; + 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 removePlanItem(planItemId) { + try { + await apiClient.delete(`/diary-date-activities/${this.currentClub}`, { + params: { planItemId } + }); + this.planItems = this.planItems.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); + } + 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; + } + hours = hours % 24; + return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }, + calculateDuration() { + const input = this.newPlanItem.durationInput; + let calculatedDuration = 0; + const multiplyPattern = /(\d+)\s*[x*]\s*(\d+)/i; + const match = input.match(multiplyPattern); + if (match) { + const [, num1, num2] = match; + calculatedDuration = parseInt(num1) * parseInt(num2); + } else if (!isNaN(input)) { + calculatedDuration = parseInt(input); + } + calculatedDuration = Math.ceil(calculatedDuration / 5) * 5; + if (!this.newPlanItem.durationText || this.newPlanItem.durationText === input) { + this.newPlanItem.duration = calculatedDuration; + this.newPlanItem = { ...this.newPlanItem, duration: calculatedDuration }; + } + }, + async removePlanItem(planItemId) { + try { + 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); + alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.'); + } + }, + async onDragEnd(evt) { + const movedItem = this.trainingPlan[evt.oldIndex]; + try { + await apiClient.put(`/diary-date-activities/${this.currentClub}/${movedItem.id}/order`, { + orderId: evt.newIndex + }); + } catch (error) { + console.error('Fehler beim Aktualisieren der Reihenfolge:', error); + alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.'); + } + }, }, async mounted() { + await this.init(); } }; @@ -532,4 +693,78 @@ li { margin-bottom: 10px; width: 100%; } + +table { + width: 100%; + border-collapse: collapse; +} + +th, +td { + padding: 8px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +input[type="text"] { + width: 100%; + padding: 5px; + box-sizing: border-box; +} + +input[type="time"] { + width: 7em; +} + +input[type="number"] { + width: 5em; + padding: 5px; + box-sizing: border-box; +} + +.dropdown { + border: 1px solid #ccc; + max-height: 200px; + overflow-y: auto; + position: absolute; + background-color: white; + z-index: 1000; + width: calc(100% - 20px); + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1); +} + +.dropdown div { + padding: 8px; + cursor: pointer; +} + +.dropdown div:hover { + background-color: #f0f0f0; +} + +.clickable { + cursor: pointer; + color: #45a049; +} + +.add-plan-item { + 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; +} + +.add-plan-item:hover { + background-color: #45a049; + color: white; +} + +.drag-handle { + cursor: pointer; +}