Umfangreiche Änderungen für Trainingslogging

This commit is contained in:
Torsten Schulz
2024-09-09 22:51:37 +02:00
parent c65c629210
commit 28bf98a169
32 changed files with 1702 additions and 54 deletions

View File

@@ -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);

View File

@@ -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 };
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 };

View File

@@ -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 };

View File

@@ -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' });
}
};

View File

@@ -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' });
}
};

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}`);
});

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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",

View File

@@ -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"
},

View File

@@ -34,7 +34,6 @@ import apiClient from './apiClient.js';
export default {
name: 'App',
data() {
console.log(import.meta.env);
return {
selectedClub: null,
};

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -47,7 +47,6 @@ const store = createStore({
router.push("/");
},
setCurrentClub({ commit }, club) {
console.log('action', club);
commit('setClub', club);
},
setClubs({ commit }, clubs) {

View File

@@ -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');

View File

@@ -43,7 +43,6 @@
</form>
</div>
<!-- Teilnehmer und Aktivitäten -->
<div v-if="date !== 'new' && date !== null">
<div class="columns">
<div class="column">
@@ -55,6 +54,7 @@
:checked="isParticipant(member.id)">
{{ member.firstName }} {{ member.lastName }}
</label>
<button @click="openNotesModal(member)">Notizen</button>
</li>
</ul>
</div>
@@ -62,6 +62,9 @@
<h3>Aktivitäten</h3>
<textarea v-model="newActivity"></textarea>
<button @click="addActivity">Aktivität hinzufügen</button>
<multiselect v-model="selectedActivityTags" :options="availableTags" placeholder="Tags auswählen"
label="name" track-by="id" multiple :close-on-select="true" @tag="addNewTag"
@remove="removeActivityTag" @input="updateActivityTags" :allow-empty="false" />
<ul>
<li v-for="activity in activities" :key="activity.id">
{{ activity.description }}
@@ -70,15 +73,37 @@
</div>
</div>
</div>
<div v-if="showNotesModal" class="modal">
<div class="modal-content">
<span class="close" @click="closeNotesModal">&times;</span>
<h3>Notizen für {{ selectedMember.firstName }} {{ selectedMember.lastName }}</h3>
<multiselect v-model="selectedMemberTags" :options="availableTags" placeholder="Tags auswählen"
label="name" track-by="id" multiple :close-on-select="true" @tag="addNewTagForMember"
@remove="removeMemberTag" @input="updateMemberTags" :allow-empty="false" />
<div>
<textarea v-model="newNoteContent" placeholder="Neue Notiz" rows="4" cols="30"></textarea>
<button @click="addMemberNote">Hinzufügen</button>
</div>
<ul>
<li v-for="note in notes" :key="note.id">
<button @click="deleteNote(note.id)">Löschen</button>
{{ note.content }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import apiClient from '../apiClient.js';
import Multiselect from 'vue-multiselect';
export default {
name: 'DiaryView',
components: { Multiselect },
data() {
return {
date: null,
@@ -91,8 +116,29 @@ export default {
participants: [],
newActivity: '',
activities: [],
notes: [],
newNoteContent: '',
selectedMember: null,
showNotesModal: false,
selectedActivityTags: [],
selectedMemberTags: [],
availableTags: [],
previousActivityTags: [],
previousMemberTags: [],
};
},
watch: {
selectedMemberTags(newTags, oldTags) {
this.updateMemberTags(newTags);
},
selectedMemberNotes(newNotes, oldNotes) {
const removedNotes = oldNotes.filter(note => !newNotes.includes(note));
removedNotes.forEach(note => this.removeMemberNote(note.content));
},
selectedActivityTags(newTags, oldTags) {
this.updateActivityTags(newTags);
},
},
computed: {
...mapGetters(['isAuthenticated', 'currentClub']),
},
@@ -101,6 +147,17 @@ 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();
}
},
handleEnterKey(event) {
const newTagName = event.target.value.trim();
if (newTagName) {
if (this.showNotesModal) {
this.addNewTagForMember(newTagName);
} else {
this.addNewTag(newTagName);
}
}
},
async handleDateChange() {
@@ -111,10 +168,15 @@ export default {
const dateData = response.data.find(entry => entry.id === dateId);
this.trainingStart = dateData.trainingStart;
this.trainingEnd = dateData.trainingEnd;
this.selectedActivityTags = dateData.diaryTags.map(tag => ({
id: tag.id,
name: tag.name
}));
this.previousActivityTags = [...this.selectedActivityTags]; // Hier setzen
await this.loadMembers();
await this.loadParticipants(dateId);
await this.loadActivities(dateId);
await this.loadActivities(dateId);
} else {
this.newDate = '';
this.trainingStart = '';
@@ -139,7 +201,6 @@ export default {
this.newDate = '';
this.trainingStart = '';
this.trainingEnd = '';
console.log(this.date);
} catch (error) {
console.error('Fehler beim Erstellen des Datums:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
@@ -147,13 +208,12 @@ export default {
},
async updateTrainingTimes() {
try {
const dateId = this.date.id;
const response = await apiClient.put(`/diary/${this.currentClub}`, {
const dateId = this.date.id;
await apiClient.put(`/diary/${this.currentClub}`, {
dateId,
trainingStart: this.trainingStart || null,
trainingEnd: this.trainingEnd || null,
});
alert('Trainingszeiten erfolgreich aktualisiert.');
} catch (error) {
console.error('Fehler beim Aktualisieren der Trainingszeiten:', error);
@@ -168,12 +228,20 @@ export default {
const response = await apiClient.get(`/participants/${dateId}`);
this.participants = response.data.map(participant => participant.memberId);
},
async loadActivities(dateId) {
const response = await apiClient.get(`/activities/${dateId}`);
this.activities = response.data;
},
async loadTags() {
const response = await apiClient.get('/tags');
this.availableTags = response.data;
},
isParticipant(memberId) {
return this.participants.includes(memberId);
},
async toggleParticipant(memberId) {
const isParticipant = this.isParticipant(memberId);
const dateId = this.date.id;
const dateId = this.date.id;
if (isParticipant) {
await apiClient.post('/participants/remove', {
diaryDateId: dateId,
@@ -188,21 +256,176 @@ export default {
this.participants.push(memberId);
}
},
async loadActivities(dateId) {
const response = await apiClient.get(`/activities/${dateId}`);
this.activities = response.data;
},
async addActivity() {
const dateId = this.date.id;
const dateId = this.date.id;
if (this.newActivity) {
const response = await apiClient.post('/activities/add', {
diaryDateId: dateId,
description: this.newActivity
description: this.newActivity,
tags: this.selectedActivityTags.map(tag => tag.id)
});
this.activities.push(response.data);
this.newActivity = '';
this.selectedActivityTags = [];
}
}
},
openNotesModal(member) {
this.selectedMember = member;
this.loadMemberNotesAndTags(this.date.id, member.id);
this.showNotesModal = true;
},
async loadMemberNotesAndTags(diaryDateId, memberId) {
try {
const notesResponse = await apiClient.get(`/diarymember/${this.currentClub}/note`, {
params: { diaryDateId, memberId }
});
this.notes = notesResponse.data;
const tagsResponse = await apiClient.get(`/diarymember/${this.currentClub}/tag`, {
params: { diaryDateId, memberId }
});
this.selectedMemberTags = tagsResponse.data.map(tag => ({
id: tag.tag.id,
name: tag.tag.name
}));
} catch (error) {
console.error('Error loading member notes and tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async addMemberNote() {
if (this.newNoteContent) {
const response = await apiClient.post(`/diarymember/${this.currentClub}/notes`, {
memberId: this.selectedMember.id,
diaryDateId: this.date.id,
content: this.newNoteContent
});
this.notes = response.data;
this.newNoteContent = '';
this.selectedTagsNotes = [];
}
},
async deleteNote(noteId) {
const response = await apiClient.delete(`/diarymember/note/${noteId}`, {
data: { clubId: this.currentClub }
});
this.notes = response.data;
},
closeNotesModal() {
this.showNotesModal = false;
},
async addNewTag(newTagName) {
try {
const response = await apiClient.post('/tags', { name: newTagName });
const newTag = response.data;
this.availableTags.push(newTag);
this.selectedActivityTags.push(newTag);
} catch (error) {
console.error('Fehler beim Hinzufügen eines neuen Tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async addNewTagForMember(newTagName) {
try {
const response = await apiClient.post('/tags', { name: newTagName });
const newTag = response.data;
this.availableTags.push(newTag);
this.selectedMemberTags.push(newTag);
await this.linkTagToMemberAndDate(newTag.id);
} catch (error) {
console.error('Fehler beim Hinzufügen eines neuen Tags für das Mitglied:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async linkTagToDiaryDate(tag) {
try {
const tagId = tag.id;
await apiClient.post(`/diary/tag/${this.currentClub}/add-tag`, {
diaryDateId: this.date.id,
tagId: tagId
});
} catch (error) {
console.error('Fehler beim Verknüpfen des Tags mit dem Trainingstag:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async linkTagToMemberAndDate(tag) {
try {
const tagId = tag.id;
await apiClient.post(`/diarymember/${this.currentClub}/tag`, {
diaryDateId: this.date.id,
memberId: this.selectedMember.id,
tagId: tagId
});
} catch (error) {
console.error('Fehler beim Verknüpfen des Tags mit dem Mitglied und Datum:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async updateActivityTags(selectedTags) {
console.log('test');
try {
for (let tag of selectedTags) {
if (!this.previousActivityTags.includes(tag)) {
await this.linkTagToDiaryDate(tag);
}
}
this.previousActivityTags = [...selectedTags];
} catch (error) {
console.error('Fehler beim Verknüpfen der Tags mit dem Trainingstag:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async updateMemberTags(selectedTags) {
try {
for (let tag of selectedTags) {
if (!this.previousMemberTags.includes(tag)) {
await this.linkTagToMemberAndDate(tag);
}
}
this.previousMemberTags = [...selectedTags];
} catch (error) {
console.error('Fehler beim Verknüpfen der Tags mit dem Mitglied und Datum:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async removeMemberTag(tagId) {
try {
await apiClient.post(`/diarymember/${this.currentClub}/tag/remove`, {
diaryDateId: this.date.id,
memberId: this.selectedMember.id,
tagId: tagId
});
this.selectedMemberTags = this.selectedMemberTags.filter(tag => tag.id !== tagId);
} catch (error) {
console.error('Fehler beim Entfernen des Tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async removeMemberNote(noteContent) {
try {
await apiClient.post(`/diarymember/${this.currentClub}/note/remove`, {
diaryDateId: this.date.id,
memberId: this.selectedMember.id,
content: noteContent
});
this.notes = this.notes.filter(note => note.content !== noteContent);
} catch (error) {
console.error('Fehler beim Entfernen der Notiz:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async removeActivityTag(tag) {
try {
const tagId = tag.id;
await apiClient.delete(`/diary/${this.currentClub}/tag`, {
params: { tagId }
});
this.selectedActivityTags = this.selectedActivityTags.filter(t => t.id !== tagId);
} catch (error) {
console.error('Fehler beim Entfernen des Tags:', error);
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
},
async mounted() {
await this.init();
@@ -254,4 +477,59 @@ ul {
padding: 0;
}
.modal {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
z-index: 1000;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 50%;
height: 50%;
overflow: auto;
background-color: rgba(200, 200, 200, 0.5);
}
.modal-content {
background-color: #fefefe;
padding: 20px;
border: 1px solid #555;
width: 100%;
height: 100%;
position: relative;
box-shadow: 4px 3px 2px #999;
}
.close {
position: absolute;
top: 10px;
right: 15px;
color: #aaa;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 0;
margin-bottom: 5px;
}
.multiselect {
margin-bottom: 10px;
width: 100%;
}
</style>

View File

@@ -24,7 +24,6 @@ export default {
...mapActions(['login']),
async executeLogin() {
try {
console.log(import.meta.env.VITE_BACKEND);
const response = await axios.post(`${import.meta.env.VITE_BACKEND}/api/auth/login`, { email: this.email, password: this.password }, {
timeout: 5000,
});

View File

@@ -3,7 +3,7 @@
<h2>Mitglieder</h2>
<div class="newmember">
<div class="toggle-new-member"><span @click="toggleNewMember"><span class="add">{{ memberFormIsOpen ? '-' :
'+' }}</span>{{ memberToEdit === null ? "Neues Mitglied" : "Mitglied bearbeiten" }}</span>
'+' }}</span>{{ memberToEdit === null ? "Neues Mitglied" : "Mitglied bearbeiten" }}</span>
<button v-if="memberToEdit !== null" @click="resetToNewMember">Neues Mitglied anlegen</button>
</div>
<div v-if="memberFormIsOpen" class="new-member-form">
@@ -39,10 +39,28 @@
<td>{{ member.birthDate }}</td>
<td>{{ member.phone }}</td>
<td>{{ member.email }}</td>
<td><button @click.stop="openNotesModal(member)">Notizen</button></td>
</tr>
</tbody>
</table>
</div>
<div v-if="showNotesModal" class="modal">
<div class="modal-content">
<span class="close" @click="closeNotesModal">&times;</span>
<h3>Notizen für {{ memberToEdit.firstName }} {{ memberToEdit.lastName }}</h3>
<div>
<textarea v-model="newNoteContent" placeholder="Neue Notiz" rows="4" cols="30"></textarea>
<button @click="addNote">Hinzufügen</button>
</div>
<ul>
<li v-for="note in notes" :key="note.id">
<button @click="deleteNote(note.id)">Löschen</button>
{{ note.content }}
</li>
</ul>
</div>
</div>
</div>
</template>
@@ -66,8 +84,11 @@ export default {
newBirthdate: '01.01.2010',
newPhone: '',
newEmail: '',
newActive: true, // Neues Feld für Active-Status
memberToEdit: null
newActive: true,
memberToEdit: null,
notes: [],
newNoteContent: '',
showNotesModal: false,
}
},
async mounted() {
@@ -89,7 +110,7 @@ export default {
this.newBirthdate = '01.01.2010';
this.newPhone = '';
this.newEmail = '';
this.newActive = true; // Standardmäßig aktiv
this.newActive = true;
},
async addNewMember() {
const response = await apiClient.post(`/clubmembers/${this.currentClub}`, {
@@ -100,7 +121,7 @@ export default {
birthdate: this.newBirthdate,
phone: this.newPhone,
email: this.newEmail,
active: this.newActive, // Übermitteln des Active-Status
active: this.newActive,
id: this.memberToEdit ? this.memberToEdit.id : null,
});
this.members = response.data;
@@ -116,7 +137,8 @@ export default {
this.newCity = member.city;
this.newPhone = member.phone;
this.newEmail = member.email;
this.newActive = member.active; // Den aktuellen Status laden
this.newActive = member.active;
this.loadNotes(member);
},
resetToNewMember() {
this.memberToEdit = null;
@@ -127,7 +149,38 @@ export default {
this.newBirthdate = '01.01.2010';
this.newPhone = '';
this.newEmail = '';
this.newActive = true; // Standardmäßig aktiv
this.newActive = true;
this.memberNotes = [];
},
async loadNotes(member) {
this.selectedMember = member;
const response = await apiClient.get(`/membernotes/${member.id}`, {
params: { clubId: this.currentClub }
});
this.notes = response.data;
this.showNotesModal = true;
},
async addNote() {
const response = await apiClient.post('/membernotes', {
memberId: this.selectedMember.id,
content: this.newNoteContent,
clubId: this.currentClub
});
this.notes = response.data;
this.newNoteContent = '';
},
async deleteNote(noteId) {
await apiClient.delete(`/membernotes/${noteId}`, {
data: { clubId: this.currentClub }
});
this.notes = response.data;
},
openNotesModal(member) {
this.editMember(member);
this.showNotesModal = true;
},
closeNotesModal() {
this.showNotesModal = false;
}
}
}
@@ -138,7 +191,7 @@ table {
border-collapse: collapse;
}
thead > tr> th {
thead>tr>th {
border-bottom: 1px solid #000;
}
@@ -172,12 +225,65 @@ table td {
border: 1px solid #999;
margin-bottom: 2em;
}
.new-member-form {
display: flex;
flex-direction: column;
}
.new-member-form > label > span {
.new-member-form>label>span {
width: 10em;
display: inline-block;
}
</style>
.modal {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(200, 200, 200, 0.5);
}
.modal-content {
background-color: #fefefe;
padding: 20px;
border: 1px solid #555;
width: 50%;
max-width: 500px;
position: relative;
box-shadow: 4px 3px 2px #999;
}
.close {
position: absolute;
top: 10px;
right: 15px;
color: #aaa;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
ul {
list-style-type: none;
padding: 0;
}
li {
padding: 0;
/* Kein Padding für Listenelemente */
margin-bottom: 5px;
}
</style>