From 28bf98a16935258fd906459aa3e81acdd2b43b2c Mon Sep 17 00:00:00 2001 From: Torsten Schulz Date: Mon, 9 Sep 2024 22:51:37 +0200 Subject: [PATCH] =?UTF-8?q?Umfangreiche=20=C3=84nderungen=20f=C3=BCr=20Tra?= =?UTF-8?q?iningslogging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controllers/authController.js | 1 - backend/controllers/diaryController.js | 75 ++- backend/controllers/diaryMemberController.js | 84 ++++ backend/controllers/diaryNoteController.js | 44 ++ backend/controllers/diaryTagController.js | 33 ++ backend/controllers/memberNoteController.js | 47 ++ backend/models/DiaryDateTag.js | 29 ++ backend/models/DiaryMemberNote.js | 35 ++ backend/models/DiaryMemberTag.js | 41 ++ backend/models/DiaryNote.js | 43 ++ backend/models/DiaryTag.js | 46 ++ backend/models/MemberNote.js | 40 ++ backend/models/index.js | 48 +- backend/routes/diaryMemberRoutes.js | 15 + backend/routes/diaryNoteRoutes.js | 11 + backend/routes/diaryRoutes.js | 16 +- backend/routes/diaryTagRoutes.js | 11 + backend/routes/memberNoteRoutes.js | 11 + backend/server.js | 35 +- backend/services/diaryMemberService.js | 60 +++ backend/services/diaryService.js | 80 +++- backend/services/memberNoteService.js | 29 ++ frontend/package-lock.json | 16 +- frontend/package.json | 1 + frontend/src/App.vue | 1 - frontend/src/assets/css/vue-multiselect.css | 468 +++++++++++++++++++ frontend/src/main.js | 1 + frontend/src/store.js | 1 - frontend/src/views/ClubView.vue | 1 - frontend/src/views/DiaryView.vue | 306 +++++++++++- frontend/src/views/Login.vue | 1 - frontend/src/views/MembersView.vue | 126 ++++- 32 files changed, 1702 insertions(+), 54 deletions(-) create mode 100644 backend/controllers/diaryMemberController.js create mode 100644 backend/controllers/diaryNoteController.js create mode 100644 backend/controllers/diaryTagController.js create mode 100644 backend/controllers/memberNoteController.js create mode 100644 backend/models/DiaryDateTag.js create mode 100644 backend/models/DiaryMemberNote.js create mode 100644 backend/models/DiaryMemberTag.js create mode 100644 backend/models/DiaryNote.js create mode 100644 backend/models/DiaryTag.js create mode 100644 backend/models/MemberNote.js create mode 100644 backend/routes/diaryMemberRoutes.js create mode 100644 backend/routes/diaryNoteRoutes.js create mode 100644 backend/routes/diaryTagRoutes.js create mode 100644 backend/routes/memberNoteRoutes.js create mode 100644 backend/services/diaryMemberService.js create mode 100644 backend/services/memberNoteService.js create mode 100644 frontend/src/assets/css/vue-multiselect.css diff --git a/backend/controllers/authController.js b/backend/controllers/authController.js index dd60f6a..808f2d5 100644 --- a/backend/controllers/authController.js +++ b/backend/controllers/authController.js @@ -21,7 +21,6 @@ const activate = async (req, res, next) => { }; const loginUser = async (req, res, next) => { - console.log('login'); try { const { email, password } = req.body; const result = await login(email, password); diff --git a/backend/controllers/diaryController.js b/backend/controllers/diaryController.js index fcdc23f..82d71fb 100644 --- a/backend/controllers/diaryController.js +++ b/backend/controllers/diaryController.js @@ -1,11 +1,9 @@ -import DiaryService from '../services/diaryService.js'; +import diaryService from '../services/diaryService.js'; import HttpError from '../exceptions/HttpError.js'; -const diaryService = new DiaryService(); - const getDatesForClub = async (req, res) => { try { - const { clubId } = req.params; + const { clubId } = req.params; const { authcode: userToken } = req.headers; const dates = await diaryService.getDatesForClub(userToken, clubId); res.status(200).json(dates); @@ -34,7 +32,6 @@ const createDateForClub = async (req, res) => { } }; - const updateTrainingTimes = async (req, res) => { try { const { clubId } = req.params; @@ -51,4 +48,70 @@ const updateTrainingTimes = async (req, res) => { } }; -export { getDatesForClub, createDateForClub, updateTrainingTimes }; \ No newline at end of file +const addDiaryNote = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { diaryDateId, content } = req.body; + const notes = await diaryService.addNoteToDate(userToken, diaryDateId, content); + res.status(201).json(notes); + } catch (error) { + console.error('[addDiaryNote] - Error:', error); + res.status(500).json({ error: 'systemerror' }); + } +}; + +const deleteDiaryNote = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { noteId } = req.params; + const notes = await diaryService.deleteNoteFromDate(userToken, noteId); + res.status(200).json(notes); + } catch (error) { + console.error('[deleteDiaryNote] - Error:', error); + res.status(500).json({ error: 'systemerror' }); + } +}; + +const addDiaryTag = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { diaryDateId, tagName } = req.body; + const tags = await diaryService.addTagToDate(userToken, diaryDateId, tagName); + res.status(201).json(tags); + } catch (error) { + console.error('[addDiaryTag] - Error:', error); + res.status(500).json({ error: 'systemerror' }); + } +}; + +const addTagToDiaryDate = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { clubId } = req.params; + const { diaryDateId, tagId } = req.body; + if (!diaryDateId || !tagId) { + return res.status(400).json({ message: 'diaryDateId and tagId are required.' }); + } + const result = await diaryService.addTagToDiaryDate(userToken, clubId, diaryDateId, tagId); + res.status(200).json(result); + } catch (error) { + console.error('[addTagToDiaryDate] - Error:', error); + res.status(500).json({ message: 'An error occurred while adding the tag to the diary date.' }); + } +}; + +const deleteTagFromDiaryDate = async (req, res) => { + try { + const { tagId } = req.query; + const { authcode: userToken } = req.headers; + const { clubId } = req.params; + await diaryService.removeTagFromDiaryDate(userToken, clubId, tagId); + res.status(200).json({ message: 'Tag deleted' }); + } catch (error) { + console.error('[deleteTag] - Error:', error); + res.status(500).json({ error: 'Error deleting tag' }); + } +}; + +export { getDatesForClub, createDateForClub, updateTrainingTimes, addDiaryNote, deleteDiaryNote, addDiaryTag, + addTagToDiaryDate, deleteTagFromDiaryDate }; diff --git a/backend/controllers/diaryMemberController.js b/backend/controllers/diaryMemberController.js new file mode 100644 index 0000000..786967d --- /dev/null +++ b/backend/controllers/diaryMemberController.js @@ -0,0 +1,84 @@ +import DiaryMemberService from '../services/diaryMemberService.js'; + +const getMemberTags = async (req, res) => { + try { + const { diaryDateId, memberId } = req.query; + const { clubId } = req.params; + const { authcode: userToken } = req.headers; + const tags = await DiaryMemberService.getTagsForMemberAndDate(userToken, clubId, diaryDateId, memberId); + res.status(200).json(tags); + } catch (error) { + console.error('[getMemberTags] - Error: ', error); + res.status(500).json({ error: 'systemerror' }); + } +}; + +const getMemberNotes = async (req, res) => { + try { + const { diaryDateId, memberId } = req.query; + const { clubId } = req.params; + const { authcode: userToken } = req.headers; + console.log('---------->', userToken, clubId); + const notes = await DiaryMemberService.getNotesForMember(userToken, clubId, diaryDateId, memberId); + res.status(200).json(notes); + } catch (error) { + console.error('[getMemberNotes] - Error: ', error); + res.status(500).json({ error: 'systemerror' }); + } +}; + +const addMemberNote = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { memberId, diaryDateId, content } = req.body; + const { clubId } = req.params; + await DiaryMemberService.addNoteToMember(userToken, clubId, diaryDateId, memberId, content); + const notes = await DiaryMemberService.getNotesForMember(userToken, clubId, diaryDateId, memberId); + res.status(201).json(notes); + } catch (error) { + console.error('[addMemberNote] - Error: ', error); + res.status(500).json({ error: 'systemerror' }); + } +}; + +const addMemberTag = async (req, res) => { + try { + const { diaryDateId, memberId, tagId } = req.body; + const { authcode: userToken } = req.headers; + const { clubId } = req.params; + await DiaryMemberService.addTagToMemberAndDate(userToken, clubId, diaryDateId, memberId, tagId); + const tags = await DiaryMemberService.getTagsForMemberAndDate(userToken, clubId, diaryDateId, memberId); + res.status(201).json(tags); + } catch (error) { + console.error('[addMemberTag] - Error: ', error); + res.status(500).json({ error: 'systemerror' }); + } +}; + +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 notes = await DiaryMemberService.getNotesForMember(userToken, req.params.clubId, diaryDateId, memberId); + res.status(200).json(notes); + } catch (error) { + console.error('[removeMemberNote] - Error: ', error.message); + res.status(400).json({ error: error.message }); + } +}; + +const removeMemberTag = async (req, res) => { + try { + const { diaryDateId, memberId, tagId } = req.body; + const { authcode: userToken } = req.headers; + await DiaryMemberService.removeTagFromMemberAndDate(userToken, req.params.clubId, diaryDateId, memberId, tagId); + const tags = await DiaryMemberService.getTagsForMemberAndDate(userToken, req.params.clubId, diaryDateId, memberId); + res.status(200).json(tags); + } catch (error) { + console.error('[removeMemberTag] - Error: ', error.message); + res.status(400).json({ error: error.message }); + } +}; + +export { getMemberTags, getMemberNotes, addMemberNote, addMemberTag, removeMemberNote, removeMemberTag }; diff --git a/backend/controllers/diaryNoteController.js b/backend/controllers/diaryNoteController.js new file mode 100644 index 0000000..dee309c --- /dev/null +++ b/backend/controllers/diaryNoteController.js @@ -0,0 +1,44 @@ +import { DiaryNote, DiaryTag } from '../models/index.js'; +import diaryService from '../services/diaryService.js'; + +export const getNotes = async (req, res) => { + try { + const { diaryDateId, memberId } = req.query; + if (!diaryDateId || !memberId) { + return res.status(400).json({ error: 'diaryDateId and memberId are required.' }); + } + + const notes = await diaryService.getDiaryNotesForDateAndMember(diaryDateId, memberId); + res.status(200).json(notes); + } catch (error) { + console.error('[getNotes] - Error:', error); + res.status(500).json({ error: 'An error occurred while fetching the notes.' }); + } +}; + +export const createNote = async (req, res) => { + try { + const { memberId, content, tags } = req.body; + const newNote = await DiaryNote.create({ memberId, content }); + if (tags && tags.length > 0) { + const tagInstances = await DiaryTag.findAll({ where: { id: tags } }); + await newNote.addTags(tagInstances); + } + const noteWithTags = await DiaryNote.findByPk(newNote.id, { + include: [{ model: DiaryTag, as: 'tags' }], + }); + res.status(201).json(noteWithTags); + } catch (error) { + res.status(500).json({ error: 'Error creating note' }); + } +}; + +export const deleteNote = async (req, res) => { + try { + const { noteId } = req.params; + await DiaryNote.destroy({ where: { id: noteId } }); + res.status(200).json({ message: 'Note deleted' }); + } catch (error) { + res.status(500).json({ error: 'Error deleting note' }); + } +}; diff --git a/backend/controllers/diaryTagController.js b/backend/controllers/diaryTagController.js new file mode 100644 index 0000000..a5b3763 --- /dev/null +++ b/backend/controllers/diaryTagController.js @@ -0,0 +1,33 @@ +import { DiaryTag, DiaryDateTag } from '../models/index.js'; + +export const getTags = async (req, res) => { + try { + const tags = await DiaryTag.findAll(); + res.status(200).json(tags); + } catch (error) { + res.status(500).json({ error: 'Error fetching tags' }); + } +}; + +export const createTag = async (req, res) => { + try { + const { name } = req.body; + const newTag = await DiaryTag.create({ name }); + res.status(201).json(newTag); + } catch (error) { + res.status(500).json({ error: 'Error creating tag' }); + } +}; + +export const deleteTag = async (req, res) => { + try { + const { tagId } = req.params; + const { authcode: userToken } = req.headers; + const { clubId } = req.params; + await diaryService.removeTagFromDiaryDate(userToken, clubId, tagId); + res.status(200).json({ message: 'Tag deleted' }); + } catch (error) { + console.error('[deleteTag] - Error:', error); + res.status(500).json({ error: 'Error deleting tag' }); + } +}; diff --git a/backend/controllers/memberNoteController.js b/backend/controllers/memberNoteController.js new file mode 100644 index 0000000..a94bd8a --- /dev/null +++ b/backend/controllers/memberNoteController.js @@ -0,0 +1,47 @@ +import MemberNoteService from "../services/memberNoteService.js"; + +const getMemberNotes = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { memberId } = req.params; + const { clubId } = req.query; + console.log('[getMemberNotes]', userToken, memberId, clubId); + const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId); + res.status(200).json(notes); + } catch (error) { + console.log('[getMemberNotes] - Error: ', error); + res.status(500).json({ error: 'systemerror' }); + } +}; + +const addMemberNote = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { memberId, content, clubId } = req.body; + console.log('[addMemberNote]', userToken, memberId, content, clubId); + await MemberNoteService.addNoteToMember(userToken, clubId, memberId, content); + const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId); + res.status(201).json(notes); + } catch (error) { + console.log('[addMemberNote] - Error: ', error); + res.status(500).json({ error: 'systemerror' }); + } +}; + +const deleteMemberNote = async (req, res) => { + try { + const { authcode: userToken } = req.headers; + const { noteId } = req.params; + const { clubId } = req.body; + console.log('[deleteMemberNote]', userToken, noteId, clubId); + const memberId = await MemberNoteService.getMemberIdForNote(noteId); // Member ID ermitteln + await MemberNoteService.deleteNoteForMember(userToken, clubId, noteId); + const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId); + res.status(200).json(notes); + } catch (error) { + console.log('[deleteMemberNote] - Error: ', error); + res.status(500).json({ error: 'systemerror' }); + } +}; + +export { getMemberNotes, addMemberNote, deleteMemberNote }; diff --git a/backend/models/DiaryDateTag.js b/backend/models/DiaryDateTag.js new file mode 100644 index 0000000..8304b60 --- /dev/null +++ b/backend/models/DiaryDateTag.js @@ -0,0 +1,29 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; +import DiaryDate from './DiaryDates.js'; +import { DiaryTag } from './DiaryTag.js'; // Benannter Import + +const DiaryDateTag = sequelize.define('DiaryDateTag', { + diaryDateId: { + type: DataTypes.INTEGER, + references: { + model: DiaryDate, + key: 'id', + }, + onDelete: 'CASCADE', + }, + tagId: { + type: DataTypes.INTEGER, + references: { + model: DiaryTag, + key: 'id', + }, + onDelete: 'CASCADE', + }, +}, { + underscored: true, + tableName: 'diary_date_tags', + timestamps: true, +}); + +export default DiaryDateTag; diff --git a/backend/models/DiaryMemberNote.js b/backend/models/DiaryMemberNote.js new file mode 100644 index 0000000..7bc50be --- /dev/null +++ b/backend/models/DiaryMemberNote.js @@ -0,0 +1,35 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; +import Member from './Member.js'; +import DiaryDate from './DiaryDates.js'; + +const DiaryMemberNote = sequelize.define('DiaryMemberNote', { + memberId: { + type: DataTypes.INTEGER, + references: { + model: Member, + key: 'id', + }, + onDelete: 'CASCADE', + allowNull: false, + }, + diaryDateId: { + type: DataTypes.INTEGER, + references: { + model: DiaryDate, + key: 'id', + }, + onDelete: 'CASCADE', + allowNull: false, + }, + content: { + type: DataTypes.STRING(4096), + allowNull: false, + }, +}, { + underscored: true, + tableName: 'diary_member_notes', + timestamps: true, +}); + +export default DiaryMemberNote; diff --git a/backend/models/DiaryMemberTag.js b/backend/models/DiaryMemberTag.js new file mode 100644 index 0000000..5a0d6a8 --- /dev/null +++ b/backend/models/DiaryMemberTag.js @@ -0,0 +1,41 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; +import Member from './Member.js'; +import DiaryDate from './DiaryDates.js'; +import { DiaryTag } from './DiaryTag.js'; + +const DiaryMemberTag = sequelize.define('DiaryMemberTag', { + memberId: { + type: DataTypes.INTEGER, + references: { + model: Member, + key: 'id', + }, + onDelete: 'CASCADE', + allowNull: false, + }, + diaryDateId: { + type: DataTypes.INTEGER, + references: { + model: DiaryDate, + key: 'id', + }, + onDelete: 'CASCADE', + allowNull: false, + }, + tagId: { + type: DataTypes.INTEGER, + references: { + model: DiaryTag, + key: 'id', + }, + onDelete: 'CASCADE', + allowNull: false, + }, +}, { + underscored: true, + tableName: 'diary_member_tags', + timestamps: true, +}); + +export default DiaryMemberTag; diff --git a/backend/models/DiaryNote.js b/backend/models/DiaryNote.js new file mode 100644 index 0000000..3983eb8 --- /dev/null +++ b/backend/models/DiaryNote.js @@ -0,0 +1,43 @@ +// models/DiaryNote.js +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; +import Member from './Member.js'; + +const DiaryNote = sequelize.define('DiaryNote', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + content: { + type: DataTypes.TEXT, + allowNull: false, + }, + memberId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: Member, + key: 'id', + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, + diaryDateId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'diary_dates', + key: 'id', + }, + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + }, +}, { + underscored: true, + tableName: 'diary_notes', + timestamps: true, +}); + +export default DiaryNote; diff --git a/backend/models/DiaryTag.js b/backend/models/DiaryTag.js new file mode 100644 index 0000000..0898485 --- /dev/null +++ b/backend/models/DiaryTag.js @@ -0,0 +1,46 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; +import Member from './Member.js'; + +const DiaryTag = sequelize.define('DiaryTag', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, +}, { + underscored: true, + tableName: 'diary_tags', + timestamps: true, +}); + +const MemberDiaryTag = sequelize.define('MemberDiaryTag', { + memberId: { + type: DataTypes.INTEGER, + references: { + model: Member, + key: 'id', + }, + onDelete: 'CASCADE', + }, + tagId: { + type: DataTypes.INTEGER, + references: { + model: DiaryTag, + key: 'id', + }, + onDelete: 'CASCADE', + }, +}, { + underscored: true, + tableName: 'member_diary_tags', + timestamps: true, +}); + +export { DiaryTag, MemberDiaryTag }; diff --git a/backend/models/MemberNote.js b/backend/models/MemberNote.js new file mode 100644 index 0000000..a8f0374 --- /dev/null +++ b/backend/models/MemberNote.js @@ -0,0 +1,40 @@ +import { DataTypes } from 'sequelize'; +import sequelize from '../database.js'; +import Member from './Member.js'; +import { encryptData, decryptData } from '../utils/encrypt.js'; + +const MemberNote = sequelize.define('MemberNote', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false, + }, + content: { + type: DataTypes.TEXT, + allowNull: false, + set(value) { + const encryptedValue = encryptData(value); + this.setDataValue('content', encryptedValue); + }, + get() { + const encryptedValue = this.getDataValue('content'); + return decryptData(encryptedValue); + } + }, + memberId: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: Member, + key: 'id', + }, + onDelete: 'CASCADE', + }, +}, { + underscored: true, + tableName: 'member_notes', + timestamps: true, +}); + +export default MemberNote; diff --git a/backend/models/index.js b/backend/models/index.js index 44acd43..57a2dc9 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -6,6 +6,12 @@ import DiaryDate from './DiaryDates.js'; import Participant from './Participant.js'; import Member from './Member.js'; import Activity from './Activity.js'; +import DiaryNote from './DiaryNote.js'; +import { DiaryTag, MemberDiaryTag } from './DiaryTag.js'; +import MemberNote from './MemberNote.js'; +import DiaryDateTag from './DiaryDateTag.js'; +import DiaryMemberNote from './DiaryMemberNote.js'; +import DiaryMemberTag from './DiaryMemberTag.js'; User.hasMany(Log, { foreignKey: 'userId' }); Log.belongsTo(User, { foreignKey: 'userId' }); @@ -16,11 +22,47 @@ Club.belongsToMany(User, { through: UserClub, foreignKey: 'clubId' }); DiaryDate.belongsTo(Club, { foreignKey: 'clubId' }); Club.hasMany(DiaryDate, { foreignKey: 'clubId' }); -DiaryDate.belongsToMany(Member, { through: Participant, as: 'participants' }); -Member.belongsToMany(DiaryDate, { through: Participant, as: 'diaryDates' }); +DiaryDate.belongsToMany(Member, { through: Participant, as: 'participants', foreignKey: 'diaryDateId' }); +Member.belongsToMany(DiaryDate, { through: Participant, as: 'diaryDates', foreignKey: 'memberId' }); DiaryDate.hasMany(Activity, { as: 'activities', foreignKey: 'diaryDateId' }); Activity.belongsTo(DiaryDate, { as: 'diaryDate', foreignKey: 'diaryDateId' }); +Member.hasMany(DiaryNote, { as: 'diaryNotes', foreignKey: 'memberId' }); +DiaryNote.belongsTo(Member, { foreignKey: 'memberId' }); -export { User, Log, Club, UserClub }; +Member.hasMany(MemberNote, { as: 'memberNotes', foreignKey: 'memberId' }); +MemberNote.belongsTo(Member, { foreignKey: 'memberId' }); + +DiaryDate.hasMany(DiaryNote, { as: 'diaryNotes', foreignKey: 'diaryDateId' }); +DiaryNote.belongsTo(DiaryDate, { foreignKey: 'diaryDateId' }); + +Member.belongsToMany(DiaryTag, { through: MemberDiaryTag, as: 'tags', foreignKey: 'memberId' }); +DiaryTag.belongsToMany(Member, { through: MemberDiaryTag, as: 'members', foreignKey: 'tagId' }); + +DiaryDate.belongsToMany(DiaryTag, { through: DiaryDateTag, as: 'diaryTags', foreignKey: 'diaryDateId' }); +DiaryTag.belongsToMany(DiaryDate, { through: DiaryDateTag, as: 'diaryDates', foreignKey: 'tagId' }); + +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' }); + +export { + User, + Log, + Club, + UserClub, + Member, + DiaryDate, + Participant, + Activity, + DiaryNote, + DiaryTag, + MemberDiaryTag, + MemberNote, + DiaryDateTag, + DiaryMemberNote, + DiaryMemberTag, +}; diff --git a/backend/routes/diaryMemberRoutes.js b/backend/routes/diaryMemberRoutes.js new file mode 100644 index 0000000..223f8ba --- /dev/null +++ b/backend/routes/diaryMemberRoutes.js @@ -0,0 +1,15 @@ +import express from 'express'; +import { authenticate } from '../middleware/authMiddleware.js'; +import { getMemberTags, getMemberNotes, addMemberNote, addMemberTag, + removeMemberNote, removeMemberTag } from '../controllers/diaryMemberController.js'; + +const router = express.Router(); + +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.post('/:clubId/tag/remove', authenticate, removeMemberTag); + +export default router; diff --git a/backend/routes/diaryNoteRoutes.js b/backend/routes/diaryNoteRoutes.js new file mode 100644 index 0000000..c49bc82 --- /dev/null +++ b/backend/routes/diaryNoteRoutes.js @@ -0,0 +1,11 @@ +import express from 'express'; +import { getNotes, createNote, deleteNote } from '../controllers/diaryNoteController.js'; +import { authenticate } from '../middleware/authMiddleware.js'; + +const router = express.Router(); + +router.get('/', authenticate, getNotes); +router.post('/', authenticate, createNote); +router.delete('/:noteId', authenticate, deleteNote); + +export default router; diff --git a/backend/routes/diaryRoutes.js b/backend/routes/diaryRoutes.js index f63dec9..cc17bc9 100644 --- a/backend/routes/diaryRoutes.js +++ b/backend/routes/diaryRoutes.js @@ -1,9 +1,23 @@ import express from 'express'; import { authenticate } from '../middleware/authMiddleware.js'; -import { getDatesForClub, createDateForClub, updateTrainingTimes } from '../controllers/diaryController.js'; +import { + getDatesForClub, + createDateForClub, + updateTrainingTimes, + addDiaryNote, + deleteDiaryNote, + addDiaryTag, + addTagToDiaryDate, + deleteTagFromDiaryDate +} from '../controllers/diaryController.js'; const router = express.Router(); +router.post('/note', authenticate, addDiaryNote); +router.delete('/note/:noteId', authenticate, deleteDiaryNote); +router.post('/tag', authenticate, addDiaryTag); +router.post('/tag/:clubId/add-tag', authenticate, addTagToDiaryDate); +router.delete('/:clubId/tag', authenticate, deleteTagFromDiaryDate); router.get('/:clubId', authenticate, getDatesForClub); router.post('/:clubId', authenticate, createDateForClub); router.put('/:clubId', authenticate, updateTrainingTimes); diff --git a/backend/routes/diaryTagRoutes.js b/backend/routes/diaryTagRoutes.js new file mode 100644 index 0000000..856d6cf --- /dev/null +++ b/backend/routes/diaryTagRoutes.js @@ -0,0 +1,11 @@ +import express from 'express'; +import { getTags, createTag, deleteTag } from '../controllers/diaryTagController.js'; +import { authenticate } from '../middleware/authMiddleware.js'; + +const router = express.Router(); + +router.get('/', authenticate, getTags); // Route to get all tags +router.post('/', authenticate, createTag); // Route to create a new tag +router.delete('/:tagId', authenticate, deleteTag); // Neue Route zum Löschen eines Tags + +export default router; diff --git a/backend/routes/memberNoteRoutes.js b/backend/routes/memberNoteRoutes.js new file mode 100644 index 0000000..a2f4bc6 --- /dev/null +++ b/backend/routes/memberNoteRoutes.js @@ -0,0 +1,11 @@ +import express from 'express'; +import { getMemberNotes, addMemberNote, deleteMemberNote } from '../controllers/memberNoteController.js'; +import { authenticate } from '../middleware/authMiddleware.js'; + +const router = express.Router(); + +router.post('/', authenticate, addMemberNote); +router.get('/:memberId', authenticate, getMemberNotes); +router.delete('/:noteId', authenticate, deleteMemberNote); + +export default router; diff --git a/backend/server.js b/backend/server.js index 81ab393..ddb7d6b 100644 --- a/backend/server.js +++ b/backend/server.js @@ -2,17 +2,20 @@ import express from 'express'; import path from 'path'; import { fileURLToPath } from 'url'; import sequelize from './database.js'; -import { User, Log, Club, UserClub } from './models/index.js'; +import { + User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote, + DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag +} from './models/index.js'; import authRoutes from './routes/authRoutes.js'; import clubRoutes from './routes/clubRoutes.js'; import diaryRoutes from './routes/diaryRoutes.js'; import memberRoutes from './routes/memberRoutes.js'; -import participantRoutes from './routes/participantRoutes.js'; -import activityRoutes from './routes/activityRoutes.js'; -import Member from './models/Member.js'; -import DiaryDate from './models/DiaryDates.js'; -import Participant from './models/Participant.js'; -import Activity from './models/Activity.js'; +import participantRoutes from './routes/participantRoutes.js'; +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 const app = express(); const port = process.env.PORT || 3000; @@ -21,12 +24,17 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); app.use(express.json()); + app.use('/api/auth', authRoutes); app.use('/api/clubs', clubRoutes); app.use('/api/clubmembers', memberRoutes); app.use('/api/diary', diaryRoutes); -app.use('/api/participants', participantRoutes); -app.use('/api/activities', activityRoutes); +app.use('/api/participants', participantRoutes); +app.use('/api/activities', activityRoutes); +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(express.static(path.join(__dirname, '../frontend/dist'))); @@ -37,6 +45,7 @@ app.get('*', (req, res) => { (async () => { try { await sequelize.authenticate(); + await User.sync({ alter: true }); await Club.sync({ alter: true }); await UserClub.sync({ alter: true }); @@ -45,6 +54,14 @@ app.get('*', (req, res) => { await DiaryDate.sync({ alter: true }); await Participant.sync({ alter: true }); await Activity.sync({ alter: true }); + await MemberNote.sync({ alter: true }); + await DiaryNote.sync({ alter: true }); + await DiaryTag.sync({ alter: true }); + await MemberDiaryTag.sync({ alter: true }); + await DiaryDateTag.sync({ alter: true }); + await DiaryMemberTag.sync({ alter: true }); + await DiaryMemberNote.sync({ alter: true }); + app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); }); diff --git a/backend/services/diaryMemberService.js b/backend/services/diaryMemberService.js new file mode 100644 index 0000000..411aab5 --- /dev/null +++ b/backend/services/diaryMemberService.js @@ -0,0 +1,60 @@ +import DiaryMemberNote from '../models/DiaryMemberNote.js'; +import DiaryMemberTag from '../models/DiaryMemberTag.js'; +import { DiaryTag } from '../models/DiaryTag.js'; +import { checkAccess } from '../utils/userUtils.js'; + +class DiaryMemberService { + async addNoteToMember(userToken, clubId, diaryDateId, memberId, content) { + await checkAccess(userToken, clubId); + const existingNote = await DiaryMemberNote.findOne({ where: { diaryDateId, memberId, content } }); + if (!existingNote) { + await DiaryMemberNote.create({ diaryDateId, memberId, content }); + } + } + + async getNotesForMember(userToken, clubId, diaryDateId, memberId) { + await checkAccess(userToken, clubId); + return await DiaryMemberNote.findAll({ where: { diaryDateId, memberId }, order: [['createdAt', 'DESC']] }); + } + + async addTagToMemberAndDate(userToken, clubId, diaryDateId, memberId, tagId) { + await checkAccess(userToken, clubId); + const existingTag = await DiaryMemberTag.findOne({ where: { diaryDateId, memberId, tagId } }); + if (!existingTag) { + await DiaryMemberTag.create({ diaryDateId, memberId, tagId }); + } + } + + async getTagsForMemberAndDate(userToken, clubId, diaryDateId, memberId) { + await checkAccess(userToken, clubId); + return await DiaryMemberTag.findAll({ + where: { diaryDateId, memberId }, + include: [{ model: DiaryTag, as: 'tag' }] + }); + } + + async removeNoteFromMember(userToken, clubId, diaryDateId, memberId, content) { + await checkAccess(userToken, clubId); + + const note = await DiaryMemberNote.findOne({ where: { diaryDateId, memberId, content } }); + if (note) { + await note.destroy(); + } else { + throw new Error('Die Notiz existiert nicht.'); + } + } + + async removeTagFromMemberAndDate(userToken, clubId, diaryDateId, memberId, tag) { + await checkAccess(userToken, clubId); + const tagLink = await DiaryMemberTag.findOne({ where: { diaryDateId, memberId, tagId: tag.id } }); + if (tagLink) { + await tagLink.destroy(); + } else { + console.log(diaryDateId, memberId, tagId); + throw new Error('Das Tag ist nicht verknüpft.'); + } + } + +} + +export default new DiaryMemberService(); diff --git a/backend/services/diaryService.js b/backend/services/diaryService.js index 8a7a55f..8a5ed04 100644 --- a/backend/services/diaryService.js +++ b/backend/services/diaryService.js @@ -1,5 +1,8 @@ import DiaryDate from '../models/DiaryDates.js'; import Club from '../models/Club.js'; +import DiaryNote from '../models/DiaryNote.js'; +import { DiaryTag } from '../models/DiaryTag.js'; +import DiaryDateTag from '../models/DiaryDateTag.js'; import { checkAccess } from '../utils/userUtils.js'; import HttpError from '../exceptions/HttpError.js'; @@ -15,9 +18,12 @@ class DiaryService { console.log('[DiaryService::getDatesForClub] - Load diary dates'); const dates = await DiaryDate.findAll({ where: { clubId }, + include: [ + { model: DiaryNote, as: 'diaryNotes' }, // Der Alias für DiaryNote ist korrekt + { model: DiaryTag, as: 'diaryTags' }, // Hier muss der Alias auf 'diaryTags' geändert werden + ], order: [['date', 'ASC'], ['trainingStart', 'ASC']] }); - return dates; } @@ -42,7 +48,7 @@ class DiaryService { date: parsedDate, clubId, trainingStart: trainingStart || null, - trainingEnd: trainingEnd || null, + trainingEnd: trainingEnd || null, }); return newDate; @@ -65,6 +71,74 @@ class DiaryService { await diaryDate.save(); return diaryDate; } + + async addNoteToDate(userToken, diaryDateId, content) { + console.log('[DiaryService::addNoteToDate] - Add note'); + await checkAccess(userToken, diaryDateId); + await DiaryNote.create({ diaryDateId, content }); + return await DiaryNote.findAll({ where: { diaryDateId }, order: [['createdAt', 'DESC']] }); + } + + async deleteNoteFromDate(userToken, noteId) { + console.log('[DiaryService::deleteNoteFromDate] - Delete note'); + const note = await DiaryNote.findByPk(noteId); + if (!note) { + throw new HttpError('Note not found', 404); + } + await checkAccess(userToken, note.diaryDateId); + await note.destroy(); + return await DiaryNote.findAll({ where: { diaryDateId: note.diaryDateId }, order: [['createdAt', 'DESC']] }); + } + + async addTagToDate(userToken, diaryDateId, tagName) { + console.log('[DiaryService::addTagToDate] - Add tag'); + await checkAccess(userToken, diaryDateId); + let tag = await DiaryTag.findOne({ where: { name: tagName } }); + if (!tag) { + tag = await DiaryTag.create({ name: tagName }); + } + const diaryDate = await DiaryDate.findByPk(diaryDateId); + await diaryDate.addTag(tag); + return await diaryDate.getTags(); + } + + async addTagToDiaryDate(userToken, clubId, diaryDateId, tagId) { + checkAccess(userToken, clubId); + console.log(`[DiaryService::addTagToDiaryDate] - diaryDateId: ${diaryDateId}, tagId: ${tagId}`); + const diaryDate = await DiaryDate.findByPk(diaryDateId); + if (!diaryDate) { + throw new HttpError('DiaryDate not found', 404); + } + const existingEntry = await DiaryDateTag.findOne({ + where: { diaryDateId, tagId } + }); + if (existingEntry) { + return; + } + const tag = await DiaryTag.findByPk(tagId); + if (!tag) { + throw new HttpError('Tag not found', 404); + } + await DiaryDateTag.create({ + diaryDateId, + tagId + }) + return diaryDate.getDiaryTags(); + } + + async getDiaryNotesForDateAndMember(diaryDateId, memberId) { + console.log('[DiaryService::getDiaryNotesForDateAndMember] - Fetching notes'); + return await DiaryNote.findAll({ + where: { diaryDateId, memberId }, + order: [['createdAt', 'DESC']] + }); + } + + async removeTagFromDiaryDate(userToken, clubId, tagId) { + await checkAccess(userToken, clubId); + await DiaryDateTag.destroy({ where: { tagId } }); + } + } -export default DiaryService; +export default new DiaryService(); diff --git a/backend/services/memberNoteService.js b/backend/services/memberNoteService.js new file mode 100644 index 0000000..43a6bf8 --- /dev/null +++ b/backend/services/memberNoteService.js @@ -0,0 +1,29 @@ +import MemberNote from '../models/MemberNote.js'; +import { checkAccess } from '../utils/userUtils.js'; + +class MemberNoteService { + async addNoteToMember(userToken, clubId, memberId, content) { + await checkAccess(userToken, clubId); + return await MemberNote.create({ memberId, content }); + } + + async getNotesForMember(userToken, clubId, memberId) { + console.log(userToken, clubId); + await checkAccess(userToken, clubId); + return await MemberNote.findAll({ + where: { memberId }, + order: [['createdAt', 'DESC']] + }); + } + + async deleteNoteForMember(userToken, clubId, noteId) { + await checkAccess(userToken, clubId); + const note = await MemberNote.findByPk(noteId); + if (!note) { + throw new Error('Note not found'); + } + await note.destroy(); + } +} + +export default new MemberNoteService(); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3deb21a..81953cd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.7.3", "core-js": "^3.8.3", "vue": "^3.2.13", + "vue-multiselect": "^3.0.0", "vue-router": "^4.4.0", "vuex": "^4.1.0" }, @@ -753,9 +754,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", - "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2422,6 +2423,15 @@ "node": ">=4.0" } }, + "node_modules/vue-multiselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.0.0.tgz", + "integrity": "sha512-uupKdINgz7j83lQToCL7KkgQQxvG43el++hsR39YT9pCe1DwzUGmKzPxjVP6rqskXed5P6DtUASYAlCliW740Q==", + "engines": { + "node": ">= 14.18.1", + "npm": ">= 6.14.15" + } + }, "node_modules/vue-router": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 004fb60..8a00b47 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "axios": "^1.7.3", "core-js": "^3.8.3", "vue": "^3.2.13", + "vue-multiselect": "^3.0.0", "vue-router": "^4.4.0", "vuex": "^4.1.0" }, diff --git a/frontend/src/App.vue b/frontend/src/App.vue index f1411d2..1b27110 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -34,7 +34,6 @@ import apiClient from './apiClient.js'; export default { name: 'App', data() { - console.log(import.meta.env); return { selectedClub: null, }; diff --git a/frontend/src/assets/css/vue-multiselect.css b/frontend/src/assets/css/vue-multiselect.css new file mode 100644 index 0000000..9c9a6ad --- /dev/null +++ b/frontend/src/assets/css/vue-multiselect.css @@ -0,0 +1,468 @@ + + + fieldset[disabled] .multiselect { + pointer-events: none; + } + + .multiselect__spinner { + position: absolute; + right: 1px; + top: 1px; + width: 40px; + height: 38px; + background: #fff; + display: block; + } + + .multiselect__spinner::before, + .multiselect__spinner::after { + position: absolute; + content: ""; + top: 50%; + left: 50%; + margin: -8px 0 0 -8px; + width: 16px; + height: 16px; + border-radius: 100%; + border-color: #41b883 transparent transparent; + border-style: solid; + border-width: 2px; + box-shadow: 0 0 0 1px transparent; + } + + .multiselect__spinner::before { + animation: spinning 2.4s cubic-bezier(0.41, 0.26, 0.2, 0.62); + animation-iteration-count: infinite; + } + + .multiselect__spinner::after { + animation: spinning 2.4s cubic-bezier(0.51, 0.09, 0.21, 0.8); + animation-iteration-count: infinite; + } + + .multiselect__loading-enter-active, + .multiselect__loading-leave-active { + transition: opacity 0.4s ease-in-out; + opacity: 1; + } + + .multiselect__loading-enter, + .multiselect__loading-leave-active { + opacity: 0; + } + + .multiselect, + .multiselect__input, + .multiselect__single { + font-family: inherit; + font-size: 16px; + touch-action: manipulation; + } + + .multiselect { + box-sizing: content-box; + display: block; + position: relative; + width: 100%; + min-height: 40px; + text-align: left; + color: #35495e; + } + + .multiselect * { + box-sizing: border-box; + } + + .multiselect:focus { + outline: none; + } + + .multiselect--disabled { + background: #ededed; + pointer-events: none; + opacity: 0.6; + } + + .multiselect--active { + z-index: 50; + } + + .multiselect--active:not(.multiselect--above) .multiselect__current, + .multiselect--active:not(.multiselect--above) .multiselect__input, + .multiselect--active:not(.multiselect--above) .multiselect__tags { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + + .multiselect--active .multiselect__select { + transform: rotateZ(180deg); + } + + .multiselect--above.multiselect--active .multiselect__current, + .multiselect--above.multiselect--active .multiselect__input, + .multiselect--above.multiselect--active .multiselect__tags { + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + .multiselect__input, + .multiselect__single { + position: relative; + display: inline-block; + min-height: 20px; + line-height: 20px; + border: none; + border-radius: 5px; + background: #fff; + padding: 0 0 0 5px; + width: calc(100%); + transition: border 0.1s ease; + box-sizing: border-box; + margin-bottom: 8px; + vertical-align: top; + } + + .multiselect__input::placeholder { + color: #35495e; + } + + .multiselect__tag ~ .multiselect__input, + .multiselect__tag ~ .multiselect__single { + width: auto; + } + + .multiselect__input:hover, + .multiselect__single:hover { + border-color: #cfcfcf; + } + + .multiselect__input:focus, + .multiselect__single:focus { + border-color: #a8a8a8; + outline: none; + } + + .multiselect__single { + padding-left: 5px; + margin-bottom: 8px; + } + + .multiselect__tags-wrap { + display: inline; + } + + .multiselect__tags { + min-height: 40px; + display: block; + padding: 8px 40px 0 8px; + border-radius: 5px; + border: 1px solid #e8e8e8; + background: #fff; + font-size: 14px; + } + + .multiselect__tag { + position: relative; + display: inline-block; + padding: 4px 26px 4px 10px; + border-radius: 5px; + margin-right: 10px; + color: #fff; + line-height: 1; + background: #41b883; + margin-bottom: 5px; + white-space: nowrap; + overflow: hidden; + max-width: 100%; + text-overflow: ellipsis; + } + + .multiselect__tag-icon { + cursor: pointer; + margin-left: 7px; + position: absolute; + right: 0; + top: 0; + bottom: 0; + font-weight: 700; + font-style: initial; + width: 22px; + text-align: center; + line-height: 22px; + transition: all 0.2s ease; + border-radius: 5px; + } + + .multiselect__tag-icon::after { + content: "×"; + color: #266d4d; + font-size: 14px; + } + + /* // Remove these lines to avoid green closing button + //.multiselect__tag-icon:focus, + //.multiselect__tag-icon:hover { + // background: #369a6e; + //} */ + + .multiselect__tag-icon:focus::after, + .multiselect__tag-icon:hover::after { + color: white; + } + + .multiselect__current { + line-height: 16px; + min-height: 40px; + box-sizing: border-box; + display: block; + overflow: hidden; + padding: 8px 12px 0; + padding-right: 30px; + white-space: nowrap; + margin: 0; + text-decoration: none; + border-radius: 5px; + border: 1px solid #e8e8e8; + cursor: pointer; + } + + .multiselect__select { + line-height: 16px; + display: block; + position: absolute; + box-sizing: border-box; + width: 40px; + height: 38px; + right: 1px; + top: 1px; + padding: 4px 8px; + margin: 0; + text-decoration: none; + text-align: center; + cursor: pointer; + transition: transform 0.2s ease; + } + + .multiselect__select::before { + position: relative; + right: 0; + top: 65%; + color: #999; + margin-top: 4px; + border-style: solid; + border-width: 5px 5px 0 5px; + border-color: #999 transparent transparent transparent; + content: ""; + } + + .multiselect__placeholder { + color: #adadad; + display: inline-block; + margin-bottom: 10px; + padding-top: 2px; + } + + .multiselect--active .multiselect__placeholder { + display: none; + } + + .multiselect__content-wrapper { + position: absolute; + display: block; + background: #fff; + width: 100%; + max-height: 240px; + overflow: auto; + border: 1px solid #e8e8e8; + border-top: none; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + z-index: 50; + -webkit-overflow-scrolling: touch; + } + + .multiselect__content { + list-style: none; + display: inline-block; + padding: 0; + margin: 0; + min-width: 100%; + vertical-align: top; + } + + .multiselect--above .multiselect__content-wrapper { + bottom: 100%; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + border-bottom: none; + border-top: 1px solid #e8e8e8; + } + + .multiselect__content::-webkit-scrollbar { + display: none; + } + + .multiselect__element { + display: block; + } + + .multiselect__option { + display: block; + padding: 12px; + min-height: 40px; + line-height: 16px; + text-decoration: none; + text-transform: none; + vertical-align: middle; + position: relative; + cursor: pointer; + white-space: nowrap; + } + + .multiselect__option::after { + top: 0; + right: 0; + position: absolute; + line-height: 40px; + padding-right: 12px; + padding-left: 20px; + font-size: 13px; + } + + .multiselect__option--highlight { + background: #41b883; + outline: none; + color: white; + } + + .multiselect__option--highlight::after { + content: attr(data-select); + background: #41b883; + color: white; + } + + .multiselect__option--selected { + background: #f3f3f3; + color: #35495e; + font-weight: bold; + } + + .multiselect__option--selected::after { + content: attr(data-selected); + color: silver; + background: inherit; + } + + .multiselect__option--selected.multiselect__option--highlight { + background: #ff6a6a; + color: #fff; + } + + .multiselect__option--selected.multiselect__option--highlight::after { + background: #ff6a6a; + content: attr(data-deselect); + color: #fff; + } + + .multiselect--disabled .multiselect__current, + .multiselect--disabled .multiselect__select { + background: #ededed; + color: #a6a6a6; + } + + .multiselect__option--disabled { + background: #ededed !important; + color: #a6a6a6 !important; + cursor: text; + pointer-events: none; + } + + .multiselect__option--group { + background: #ededed; + color: #35495e; + } + + .multiselect__option--group.multiselect__option--highlight { + background: #35495e; + color: #fff; + } + + .multiselect__option--group.multiselect__option--highlight::after { + background: #35495e; + } + + .multiselect__option--disabled.multiselect__option--highlight { + background: #dedede; + } + + .multiselect__option--group-selected.multiselect__option--highlight { + background: #ff6a6a; + color: #fff; + } + + .multiselect__option--group-selected.multiselect__option--highlight::after { + background: #ff6a6a; + content: attr(data-deselect); + color: #fff; + } + + .multiselect-enter-active, + .multiselect-leave-active { + transition: all 0.15s ease; + } + + .multiselect-enter, + .multiselect-leave-active { + opacity: 0; + } + + .multiselect__strong { + margin-bottom: 8px; + line-height: 20px; + display: inline-block; + vertical-align: top; + } + + *[dir="rtl"] .multiselect { + text-align: right; + } + + *[dir="rtl"] .multiselect__select { + right: auto; + left: 1px; + } + + *[dir="rtl"] .multiselect__tags { + padding: 8px 8px 0 40px; + } + + *[dir="rtl"] .multiselect__content { + text-align: right; + } + + *[dir="rtl"] .multiselect__option::after { + right: auto; + left: 0; + } + + *[dir="rtl"] .multiselect__clear { + right: auto; + left: 12px; + } + + *[dir="rtl"] .multiselect__spinner { + right: auto; + left: 1px; + } + + @keyframes spinning { + from { + transform: rotate(0); + } + + to { + transform: rotate(2turn); + } + } diff --git a/frontend/src/main.js b/frontend/src/main.js index 46ef42b..ff1e984 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -3,6 +3,7 @@ import App from './App.vue'; import router from './router'; import store from './store'; import '@/assets/css/main.scss'; +import './assets/css/vue-multiselect.css'; createApp(App) .use(router) diff --git a/frontend/src/store.js b/frontend/src/store.js index 2270908..90caadb 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -47,7 +47,6 @@ const store = createStore({ router.push("/"); }, setCurrentClub({ commit }, club) { - console.log('action', club); commit('setClub', club); }, setClubs({ commit }, clubs) { diff --git a/frontend/src/views/ClubView.vue b/frontend/src/views/ClubView.vue index 54b7492..d7b90c8 100644 --- a/frontend/src/views/ClubView.vue +++ b/frontend/src/views/ClubView.vue @@ -62,7 +62,6 @@ export default { } }, async requestAccess() { - console.log('start request'); const response = await apiClient.get(`/clubs/request/${this.currentClub}`); if (response.status === 200) { alert('Zugriff wurde angefragt'); diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index ae6990e..81735d6 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -43,7 +43,6 @@ -
@@ -55,6 +54,7 @@ :checked="isParticipant(member.id)"> {{ member.firstName }} {{ member.lastName }} +
@@ -62,6 +62,9 @@

Aktivitäten

+
  • {{ activity.description }} @@ -70,15 +73,37 @@
+ +