diff --git a/backend/controllers/diaryNoteController.js b/backend/controllers/diaryNoteController.js
index dee309c..590d1a0 100644
--- a/backend/controllers/diaryNoteController.js
+++ b/backend/controllers/diaryNoteController.js
@@ -18,16 +18,24 @@ export const getNotes = async (req, res) => {
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 { memberId, diaryDateId, content, tags } = req.body;
+
+ if (!memberId || !diaryDateId || !content) {
+ return res.status(400).json({ error: 'memberId, diaryDateId und content sind erforderlich.' });
+ }
+
+ const newNote = await DiaryNote.create({ memberId, diaryDateId, content });
+
+ if (Array.isArray(tags) && tags.length > 0 && typeof newNote.addTags === 'function') {
const tagInstances = await DiaryTag.findAll({ where: { id: tags } });
await newNote.addTags(tagInstances);
}
+
const noteWithTags = await DiaryNote.findByPk(newNote.id, {
- include: [{ model: DiaryTag, as: 'tags' }],
+ include: [{ model: DiaryTag, as: 'tags', required: false }],
});
- res.status(201).json(noteWithTags);
+
+ res.status(201).json(noteWithTags ?? newNote);
} catch (error) {
res.status(500).json({ error: 'Error creating note' });
}
diff --git a/backend/controllers/diaryTagController.js b/backend/controllers/diaryTagController.js
index 422e6b7..c0249d5 100644
--- a/backend/controllers/diaryTagController.js
+++ b/backend/controllers/diaryTagController.js
@@ -1,6 +1,5 @@
import { DiaryTag, DiaryDateTag } from '../models/index.js';
-import { devLog } from '../utils/logger.js';
export const getTags = async (req, res) => {
try {
const tags = await DiaryTag.findAll();
@@ -13,9 +12,12 @@ export const getTags = async (req, res) => {
export const createTag = async (req, res) => {
try {
const { name } = req.body;
- devLog(name);
- const newTag = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } });
- res.status(201).json(newTag);
+ if (!name) {
+ return res.status(400).json({ error: 'Der Name des Tags ist erforderlich.' });
+ }
+
+ const [tag, created] = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } });
+ res.status(created ? 201 : 200).json(tag);
} catch (error) {
res.status(500).json({ error: 'Error creating tag' });
}
@@ -24,9 +26,14 @@ export const createTag = async (req, res) => {
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);
+
+ await DiaryDateTag.destroy({ where: { tagId } });
+ const deleted = await DiaryTag.destroy({ where: { id: tagId } });
+
+ if (!deleted) {
+ return res.status(404).json({ error: 'Tag nicht gefunden' });
+ }
+
res.status(200).json({ message: 'Tag deleted' });
} catch (error) {
console.error('[deleteTag] - Error:', error);
diff --git a/backend/services/autoFetchMatchResultsService.js b/backend/services/autoFetchMatchResultsService.js
index 170ddf5..66d3e78 100644
--- a/backend/services/autoFetchMatchResultsService.js
+++ b/backend/services/autoFetchMatchResultsService.js
@@ -26,7 +26,7 @@ class AutoFetchMatchResultsService {
autoUpdateRatings: true, // Nutze das gleiche Flag
savePassword: true // Must have saved password
},
- attributes: ['id', 'userId', 'email', 'encryptedPassword', 'accessToken', 'expiresAt', 'cookie']
+ attributes: ['id', 'userId', 'email', 'savePassword', 'encryptedPassword', 'accessToken', 'expiresAt', 'cookie']
});
devLog(`Found ${accounts.length} accounts with auto-updates enabled for match results`);
@@ -86,6 +86,7 @@ class AutoFetchMatchResultsService {
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
+ account.savePassword = true; // ensure flag persists when saving
await account.save();
devLog(`Successfully re-logged in for ${account.email}`);
diff --git a/backend/services/autoUpdateRatingsService.js b/backend/services/autoUpdateRatingsService.js
index 80fbb6f..387b06d 100644
--- a/backend/services/autoUpdateRatingsService.js
+++ b/backend/services/autoUpdateRatingsService.js
@@ -20,7 +20,7 @@ class AutoUpdateRatingsService {
autoUpdateRatings: true,
savePassword: true // Must have saved password
},
- attributes: ['id', 'userId', 'email', 'encryptedPassword', 'accessToken', 'expiresAt', 'cookie']
+ attributes: ['id', 'userId', 'email', 'savePassword', 'encryptedPassword', 'accessToken', 'expiresAt', 'cookie']
});
devLog(`Found ${accounts.length} accounts with auto-updates enabled`);
@@ -78,6 +78,7 @@ class AutoUpdateRatingsService {
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
+ account.savePassword = true; // Ensure flag persists when saving limited attributes
await account.save();
devLog(`Successfully re-logged in for ${account.email}`);
diff --git a/backend/tests/diaryDateActivityRoutes.test.js b/backend/tests/diaryDateActivityRoutes.test.js
new file mode 100644
index 0000000..37bf017
--- /dev/null
+++ b/backend/tests/diaryDateActivityRoutes.test.js
@@ -0,0 +1,117 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import request from 'supertest';
+
+vi.mock('../services/emailService.js', () => ({
+ sendActivationEmail: vi.fn().mockResolvedValue(),
+}));
+
+import app from './testApp.js';
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import DiaryDate from '../models/DiaryDates.js';
+import Group from '../models/Group.js';
+import GroupActivity from '../models/GroupActivity.js';
+import DiaryDateActivity from '../models/DiaryDateActivity.js';
+import User from '../models/User.js';
+
+const registerAndActivate = async (email) => {
+ const password = 'Test123!';
+ await request(app).post('/api/auth/register').send({ email, password });
+ const user = await User.findOne({ where: { email } });
+ await user.update({ isActive: true });
+ return { user, credentials: { email, password } };
+};
+
+const loginAndGetToken = async (credentials) => {
+ const response = await request(app).post('/api/auth/login').send(credentials);
+ return response.body.token;
+};
+
+const authHeaders = (token) => ({
+ Authorization: `Bearer ${token}`,
+ authcode: token,
+});
+
+describe('DiaryDateActivity Routes', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ });
+
+ it('verwaltet Aktivitäten einschließlich Reihenfolge und Gruppenaktionen', async () => {
+ const { credentials } = await registerAndActivate('dda-routes@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const clubResponse = await request(app)
+ .post('/api/clubs')
+ .set(authHeaders(token))
+ .send({ name: 'Activity Club' });
+ const clubId = clubResponse.body.id;
+
+ const diaryDateResponse = await request(app)
+ .post(`/api/diary/${clubId}`)
+ .set(authHeaders(token))
+ .send({ date: '2026-02-01' });
+ const diaryDateId = diaryDateResponse.body.id;
+
+ const timeblockResponse = await request(app)
+ .post(`/api/diary-date-activities/${clubId}/`)
+ .set(authHeaders(token))
+ .send({ diaryDateId, activity: 'Warmup', isTimeblock: true });
+ const timeblockId = timeblockResponse.body.id;
+
+ const activityResponse = await request(app)
+ .post(`/api/diary-date-activities/${clubId}/`)
+ .set(authHeaders(token))
+ .send({ diaryDateId, activity: 'Topspin üben', duration: '30' });
+ const activityId = activityResponse.body.id;
+
+ const updateResponse = await request(app)
+ .put(`/api/diary-date-activities/${clubId}/${activityId}`)
+ .set(authHeaders(token))
+ .send({ customActivityName: 'Topspin intensiv', duration: 35 });
+
+ expect(updateResponse.status).toBe(200);
+ expect(updateResponse.body.predefinedActivityId).toBeTruthy();
+
+ const reorderResponse = await request(app)
+ .put(`/api/diary-date-activities/${clubId}/${activityId}/order`)
+ .set(authHeaders(token))
+ .send({ orderId: 1 });
+
+ expect(reorderResponse.status).toBe(200);
+ const persisted = await DiaryDateActivity.findByPk(activityId);
+ expect(persisted.orderId).toBe(1);
+
+ const group = await Group.create({ diaryDateId, name: 'Gruppe A' });
+
+ const addGroupResponse = await request(app)
+ .post('/api/diary-date-activities/group')
+ .set(authHeaders(token))
+ .send({ clubId, diaryDateId, groupId: group.id, activity: 'Match', timeblockId });
+
+ expect(addGroupResponse.status).toBe(201);
+ const groupActivityId = addGroupResponse.body.id;
+
+ const listResponse = await request(app)
+ .get(`/api/diary-date-activities/${clubId}/${diaryDateId}`)
+ .set(authHeaders(token));
+
+ expect(listResponse.status).toBe(200);
+ expect(listResponse.body.length).toBeGreaterThanOrEqual(2);
+
+ const deleteGroupResponse = await request(app)
+ .delete(`/api/diary-date-activities/group/${clubId}/${groupActivityId}`)
+ .set(authHeaders(token));
+
+ expect(deleteGroupResponse.status).toBe(200);
+ const groupExists = await GroupActivity.findByPk(groupActivityId);
+ expect(groupExists).toBeNull();
+
+ const deleteResponse = await request(app)
+ .delete(`/api/diary-date-activities/${clubId}/${activityId}`)
+ .set(authHeaders(token));
+
+ expect(deleteResponse.status).toBe(200);
+ });
+});
diff --git a/backend/tests/diaryDateActivityService.test.js b/backend/tests/diaryDateActivityService.test.js
new file mode 100644
index 0000000..80b94a2
--- /dev/null
+++ b/backend/tests/diaryDateActivityService.test.js
@@ -0,0 +1,182 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+vi.mock('../utils/userUtils.js', async () => {
+ const actual = await vi.importActual('../utils/userUtils.js');
+ return {
+ ...actual,
+ checkAccess: vi.fn().mockResolvedValue(true),
+ };
+});
+
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import diaryDateActivityService from '../services/diaryDateActivityService.js';
+import { checkAccess } from '../utils/userUtils.js';
+import Club from '../models/Club.js';
+import DiaryDate from '../models/DiaryDates.js';
+import DiaryDateActivity from '../models/DiaryDateActivity.js';
+import PredefinedActivity from '../models/PredefinedActivity.js';
+import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
+import Group from '../models/Group.js';
+import GroupActivity from '../models/GroupActivity.js';
+
+const createClubAndDate = async () => {
+ const club = await Club.create({ name: 'Service Club' });
+ const diaryDate = await DiaryDate.create({ date: '2026-01-01', clubId: club.id });
+ return { club, diaryDate };
+};
+
+describe('diaryDateActivityService', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ vi.clearAllMocks();
+ });
+
+ it('erstellt Aktivitäten mit automatisch berechneter Reihenfolge', async () => {
+ const { club, diaryDate } = await createClubAndDate();
+
+ const first = await diaryDateActivityService.createActivity('token', club.id, {
+ diaryDateId: diaryDate.id,
+ activity: 'Warmup',
+ duration: '30',
+ isTimeblock: false,
+ });
+
+ const second = await diaryDateActivityService.createActivity('token', club.id, {
+ diaryDateId: diaryDate.id,
+ activity: 'Drills',
+ duration: '45',
+ isTimeblock: false,
+ });
+
+ expect(checkAccess).toHaveBeenCalledTimes(2);
+ expect(first.orderId).toBe(1);
+ expect(second.orderId).toBe(2);
+ const all = await DiaryDateActivity.findAll({ where: { diaryDateId: diaryDate.id } });
+ expect(all).toHaveLength(2);
+ });
+
+ it('aktualisiert Aktivitäten und legt neue vordefinierte Aktivitäten an', async () => {
+ const { club, diaryDate } = await createClubAndDate();
+ const activity = await diaryDateActivityService.createActivity('token', club.id, {
+ diaryDateId: diaryDate.id,
+ activity: 'Blocken',
+ duration: '20',
+ isTimeblock: false,
+ });
+
+ const updated = await diaryDateActivityService.updateActivity('token', club.id, activity.id, {
+ customActivityName: 'Topspin',
+ duration: 25,
+ });
+
+ const predefined = await PredefinedActivity.findOne({ where: { name: 'Topspin' } });
+
+ expect(updated.predefinedActivityId).toBe(predefined.id);
+ expect(predefined.duration).toBe(25);
+ });
+
+ it('ändert die Reihenfolge und verschiebt Nachbarn korrekt', async () => {
+ const { club, diaryDate } = await createClubAndDate();
+ const first = await diaryDateActivityService.createActivity('token', club.id, {
+ diaryDateId: diaryDate.id,
+ activity: 'A',
+ });
+ const second = await diaryDateActivityService.createActivity('token', club.id, {
+ diaryDateId: diaryDate.id,
+ activity: 'B',
+ });
+ const third = await diaryDateActivityService.createActivity('token', club.id, {
+ diaryDateId: diaryDate.id,
+ activity: 'C',
+ });
+
+ await diaryDateActivityService.updateActivityOrder('token', club.id, third.id, 1);
+
+ const reloaded = await DiaryDateActivity.findAll({
+ where: { diaryDateId: diaryDate.id },
+ order: [['orderId', 'ASC']],
+ });
+
+ expect(reloaded[0].id).toBe(third.id);
+ expect(reloaded.map((item) => item.orderId)).toEqual([1, 2, 3]);
+ });
+
+ it('liefert Aktivitäten mit Bild-Links und Gruppendaten', async () => {
+ const { club, diaryDate } = await createClubAndDate();
+ const predefined = await PredefinedActivity.create({ name: 'Vorhand', code: 'FH' });
+ const activity = await DiaryDateActivity.create({
+ diaryDateId: diaryDate.id,
+ predefinedActivityId: predefined.id,
+ orderId: 1,
+ isTimeblock: false,
+ });
+ await PredefinedActivityImage.create({
+ predefinedActivityId: predefined.id,
+ drawingData: JSON.stringify({ circles: 2 }),
+ mimeType: 'image/png',
+ fileName: 'test.png',
+ });
+
+ const group = await Group.create({ diaryDateId: diaryDate.id, name: 'Gruppe 1' });
+ const groupPredefined = await PredefinedActivity.create({ name: 'Rally', code: 'RL' });
+ await GroupActivity.create({
+ diaryDateActivity: activity.id,
+ groupId: group.id,
+ customActivity: groupPredefined.id,
+ });
+
+ const result = await diaryDateActivityService.getActivities('token', club.id, diaryDate.id);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].predefinedActivity.imageUrl).toContain(`/api/predefined-activities/${predefined.id}/image/`);
+ expect(result[0].groupActivities).toHaveLength(1);
+ });
+
+ it('fügt Gruppenaktivitäten in Zeitblöcke ein', async () => {
+ const { club, diaryDate } = await createClubAndDate();
+ const timeblock = await DiaryDateActivity.create({
+ diaryDateId: diaryDate.id,
+ isTimeblock: true,
+ orderId: 1,
+ });
+ const group = await Group.create({ diaryDateId: diaryDate.id, name: 'Gruppe 2' });
+
+ const created = await diaryDateActivityService.addGroupActivity('token', club.id, diaryDate.id, group.id, 'Abschlussspiel', timeblock.id);
+
+ expect(created.diaryDateActivity).toBe(timeblock.id);
+ expect(created.groupId).toBe(group.id);
+ });
+
+ it('löscht Aktivitäten und zugehörige Gruppeneinträge', async () => {
+ const { club, diaryDate } = await createClubAndDate();
+ const activity = await diaryDateActivityService.createActivity('token', club.id, {
+ diaryDateId: diaryDate.id,
+ activity: 'Auslaufen',
+ });
+
+ await diaryDateActivityService.deleteActivity('token', club.id, activity.id);
+ const remaining = await DiaryDateActivity.findByPk(activity.id);
+ expect(remaining).toBeNull();
+ });
+
+ it('löscht Gruppenaktivitäten', async () => {
+ const { club, diaryDate } = await createClubAndDate();
+ const timeblock = await DiaryDateActivity.create({
+ diaryDateId: diaryDate.id,
+ isTimeblock: true,
+ orderId: 1,
+ });
+ const group = await Group.create({ diaryDateId: diaryDate.id, name: 'Gruppe 3' });
+ const created = await GroupActivity.create({
+ diaryDateActivity: timeblock.id,
+ groupId: group.id,
+ customActivity: null,
+ });
+
+ await diaryDateActivityService.deleteGroupActivity('token', club.id, created.id);
+ const exists = await GroupActivity.findByPk(created.id);
+ expect(exists).toBeNull();
+ });
+});
diff --git a/backend/tests/diaryMemberActivityRoutes.test.js b/backend/tests/diaryMemberActivityRoutes.test.js
new file mode 100644
index 0000000..eecf4a7
--- /dev/null
+++ b/backend/tests/diaryMemberActivityRoutes.test.js
@@ -0,0 +1,129 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import request from 'supertest';
+
+vi.mock('../services/emailService.js', () => ({
+ sendActivationEmail: vi.fn().mockResolvedValue(),
+}));
+
+import app from './testApp.js';
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import Member from '../models/Member.js';
+import Participant from '../models/Participant.js';
+import DiaryDate from '../models/DiaryDates.js';
+import DiaryDateActivity from '../models/DiaryDateActivity.js';
+import DiaryMemberActivity from '../models/DiaryMemberActivity.js';
+import User from '../models/User.js';
+
+const registerAndActivate = async (email) => {
+ const password = 'Test123!';
+ await request(app).post('/api/auth/register').send({ email, password });
+ const user = await User.findOne({ where: { email } });
+ await user.update({ isActive: true });
+ return { user, credentials: { email, password } };
+};
+
+const loginAndGetToken = async (credentials) => {
+ const response = await request(app).post('/api/auth/login').send(credentials);
+ return response.body.token;
+};
+
+const authHeaders = (token) => ({
+ Authorization: `Bearer ${token}`,
+ authcode: token,
+});
+
+const createMember = async (clubId, firstName, lastName, email) => {
+ return Member.create({
+ firstName,
+ lastName,
+ phone: '0123456789',
+ street: 'Teststraße 1',
+ city: 'Teststadt',
+ postalCode: '12345',
+ email,
+ clubId,
+ birthDate: '2000-01-01',
+ active: true,
+ testMembership: false,
+ picsInInternetAllowed: false,
+ gender: 'female',
+ });
+};
+
+describe('DiaryMemberActivity Routes', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ });
+
+ it('fügt Teilnehmer zu Aktivitäten hinzu, listet und entfernt sie wieder', async () => {
+ const { credentials } = await registerAndActivate('dma@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const clubResponse = await request(app)
+ .post('/api/clubs')
+ .set(authHeaders(token))
+ .send({ name: 'Member Activity Club' });
+ const clubId = clubResponse.body.id;
+
+ const diaryDateResponse = await request(app)
+ .post(`/api/diary/${clubId}`)
+ .set(authHeaders(token))
+ .send({ date: '2026-03-01' });
+ const diaryDateId = diaryDateResponse.body.id;
+
+ const activityResponse = await request(app)
+ .post(`/api/diary-date-activities/${clubId}/`)
+ .set(authHeaders(token))
+ .send({ diaryDateId, activity: 'Koordination', isTimeblock: false });
+ const activityId = activityResponse.body.id;
+
+ const member = await createMember(clubId, 'Anna', 'Trainer', 'anna@example.com');
+ const participant = await Participant.create({ diaryDateId, memberId: member.id });
+
+ const addResponse = await request(app)
+ .post(`/api/diary-member-activities/${clubId}/${activityId}`)
+ .set(authHeaders(token))
+ .send({ participantIds: [participant.id] });
+
+ expect(addResponse.status).toBe(201);
+ expect(addResponse.body).toHaveLength(1);
+
+ const listResponse = await request(app)
+ .get(`/api/diary-member-activities/${clubId}/${activityId}`)
+ .set(authHeaders(token));
+
+ expect(listResponse.status).toBe(200);
+ expect(listResponse.body[0].participantId).toBe(participant.id);
+
+ const deleteResponse = await request(app)
+ .delete(`/api/diary-member-activities/${clubId}/${activityId}/${participant.id}`)
+ .set(authHeaders(token));
+
+ expect(deleteResponse.status).toBe(200);
+ const remaining = await DiaryMemberActivity.findAll({ where: { diaryDateActivityId: activityId } });
+ expect(remaining).toHaveLength(0);
+ });
+
+ it('prüft ungültige Nutzlasten', async () => {
+ const { credentials } = await registerAndActivate('dma-invalid@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const clubResponse = await request(app)
+ .post('/api/clubs')
+ .set(authHeaders(token))
+ .send({ name: 'Invalid Payload Club' });
+ const clubId = clubResponse.body.id;
+
+ const diaryDate = await DiaryDate.create({ date: '2026-04-01', clubId });
+ const activity = await DiaryDateActivity.create({ diaryDateId: diaryDate.id, orderId: 1, isTimeblock: false });
+
+ const response = await request(app)
+ .post(`/api/diary-member-activities/${clubId}/${activity.id}`)
+ .set(authHeaders(token))
+ .send({ participantIds: 'nicht-array' });
+
+ expect(response.status).toBe(400);
+ });
+});
diff --git a/backend/tests/diaryNoteRoutes.test.js b/backend/tests/diaryNoteRoutes.test.js
new file mode 100644
index 0000000..50b23ee
--- /dev/null
+++ b/backend/tests/diaryNoteRoutes.test.js
@@ -0,0 +1,114 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import request from 'supertest';
+
+vi.mock('../services/emailService.js', () => ({
+ sendActivationEmail: vi.fn().mockResolvedValue(),
+}));
+
+import app from './testApp.js';
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import Member from '../models/Member.js';
+import DiaryDate from '../models/DiaryDates.js';
+import DiaryNote from '../models/DiaryNote.js';
+import User from '../models/User.js';
+
+const registerAndActivate = async (email) => {
+ const password = 'Test123!';
+ await request(app).post('/api/auth/register').send({ email, password });
+ const user = await User.findOne({ where: { email } });
+ await user.update({ isActive: true });
+ return { user, credentials: { email, password } };
+};
+
+const loginAndGetToken = async (credentials) => {
+ const response = await request(app).post('/api/auth/login').send(credentials);
+ return response.body.token;
+};
+
+const authHeaders = (token) => ({
+ Authorization: `Bearer ${token}`,
+ authcode: token,
+});
+
+const createMember = (clubId, email) =>
+ Member.create({
+ firstName: 'Note',
+ lastName: 'Tester',
+ phone: '0123456789',
+ street: 'Straße 1',
+ city: 'Stadt',
+ postalCode: '12345',
+ email,
+ clubId,
+ birthDate: '2000-01-01',
+ active: true,
+ testMembership: false,
+ picsInInternetAllowed: false,
+ gender: 'female',
+ });
+
+describe('DiaryNote Routes', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ });
+
+ it('erstellt, listet und löscht Notizen', async () => {
+ const { credentials } = await registerAndActivate('diarynote@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const clubResponse = await request(app)
+ .post('/api/clubs')
+ .set(authHeaders(token))
+ .send({ name: 'Notiz Club' });
+ const clubId = clubResponse.body.id;
+
+ const diaryDateResponse = await request(app)
+ .post(`/api/diary/${clubId}`)
+ .set(authHeaders(token))
+ .send({ date: '2026-05-01' });
+ const diaryDateId = diaryDateResponse.body.id;
+
+ const member = await createMember(clubId, 'note.member@example.com');
+
+ const createResponse = await request(app)
+ .post('/api/diary-notes')
+ .set(authHeaders(token))
+ .send({ memberId: member.id, diaryDateId, content: 'Gute Einheit' });
+
+ expect(createResponse.status).toBe(201);
+ expect(createResponse.body.content).toBe('Gute Einheit');
+
+ const listResponse = await request(app)
+ .get('/api/diary-notes')
+ .set(authHeaders(token))
+ .query({ diaryDateId, memberId: member.id });
+
+ expect(listResponse.status).toBe(200);
+ expect(listResponse.body).toHaveLength(1);
+ expect(listResponse.body[0].content).toBe('Gute Einheit');
+
+ const noteId = createResponse.body.id;
+
+ const deleteResponse = await request(app)
+ .delete(`/api/diary-notes/${noteId}`)
+ .set(authHeaders(token));
+
+ expect(deleteResponse.status).toBe(200);
+ const remaining = await DiaryNote.findByPk(noteId);
+ expect(remaining).toBeNull();
+ });
+
+ it('validiert Pflichtfelder beim Erstellen', async () => {
+ const { credentials } = await registerAndActivate('diarynote-invalid@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const response = await request(app)
+ .post('/api/diary-notes')
+ .set(authHeaders(token))
+ .send({ content: 'Fehlende Felder' });
+
+ expect(response.status).toBe(400);
+ });
+});
diff --git a/backend/tests/diaryService.test.js b/backend/tests/diaryService.test.js
index 029f984..cb42687 100644
--- a/backend/tests/diaryService.test.js
+++ b/backend/tests/diaryService.test.js
@@ -50,6 +50,10 @@ describe('diaryService', () => {
await expect(
diaryService.createDateForClub('token', club.id, '2025-02-03', '20:00', '19:00')
).rejects.toThrow('Training start time must be before training end time');
+
+ await expect(
+ diaryService.createDateForClub('token', club.id, 'ungültig', '20:00', '21:00')
+ ).rejects.toThrow('Invalid date format');
});
it('aktualisiert Trainingszeiten und wirft Fehler bei fehlendem Eintrag', async () => {
@@ -121,4 +125,10 @@ describe('diaryService', () => {
const exists = await DiaryDate.findByPk(date.id);
expect(exists).toBeNull();
});
+
+ it('meldet Fehler bei fehlenden Clubs oder Einträgen', async () => {
+ await expect(diaryService.getDatesForClub('token', 9999)).rejects.toThrow('Club not found');
+ const club = await Club.create({ name: '404 Club' });
+ await expect(diaryService.removeDateForClub('token', club.id, 9999)).rejects.toThrow('Diary entry not found');
+ });
});
diff --git a/backend/tests/diaryTagRoutes.test.js b/backend/tests/diaryTagRoutes.test.js
new file mode 100644
index 0000000..f631b5e
--- /dev/null
+++ b/backend/tests/diaryTagRoutes.test.js
@@ -0,0 +1,80 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import request from 'supertest';
+
+vi.mock('../services/emailService.js', () => ({
+ sendActivationEmail: vi.fn().mockResolvedValue(),
+}));
+
+import app from './testApp.js';
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import DiaryTag from '../models/DiaryTag.js';
+import DiaryDateTag from '../models/DiaryDateTag.js';
+import DiaryDate from '../models/DiaryDates.js';
+import Club from '../models/Club.js';
+import User from '../models/User.js';
+
+const registerAndActivate = async (email) => {
+ const password = 'Test123!';
+ await request(app).post('/api/auth/register').send({ email, password });
+ const user = await User.findOne({ where: { email } });
+ await user.update({ isActive: true });
+ return { user, credentials: { email, password } };
+};
+
+const loginAndGetToken = async (credentials) => {
+ const response = await request(app).post('/api/auth/login').send(credentials);
+ return response.body.token;
+};
+
+const authHeaders = (token) => ({
+ Authorization: `Bearer ${token}`,
+ authcode: token,
+});
+
+describe('DiaryTag Routes', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ });
+
+ it('erstellt und listet Tags', async () => {
+ const { credentials } = await registerAndActivate('diarytag@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const createResponse = await request(app)
+ .post('/api/diary-tags')
+ .set(authHeaders(token))
+ .send({ name: 'Technik' });
+
+ expect(createResponse.status).toBe(201);
+ expect(createResponse.body.name).toBe('Technik');
+
+ const listResponse = await request(app)
+ .get('/api/diary-tags')
+ .set(authHeaders(token));
+
+ expect(listResponse.status).toBe(200);
+ expect(listResponse.body).toHaveLength(1);
+ });
+
+ it('löscht Tags inklusive Zuordnungen', async () => {
+ const { credentials } = await registerAndActivate('diarytag-delete@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const club = await Club.create({ name: 'Tag Club' });
+ const diaryDate = await DiaryDate.create({ date: '2026-06-01', clubId: club.id });
+ const tag = await DiaryTag.create({ name: 'Ausdauer' });
+ await DiaryDateTag.create({ diaryDateId: diaryDate.id, tagId: tag.id });
+
+ const deleteResponse = await request(app)
+ .delete(`/api/diary-tags/${tag.id}`)
+ .set(authHeaders(token));
+
+ expect(deleteResponse.status).toBe(200);
+ const tagExists = await DiaryTag.findByPk(tag.id);
+ expect(tagExists).toBeNull();
+ const relations = await DiaryDateTag.findAll({ where: { tagId: tag.id } });
+ expect(relations).toHaveLength(0);
+ });
+});
diff --git a/backend/tests/groupRoutes.test.js b/backend/tests/groupRoutes.test.js
new file mode 100644
index 0000000..04d658e
--- /dev/null
+++ b/backend/tests/groupRoutes.test.js
@@ -0,0 +1,78 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import request from 'supertest';
+
+vi.mock('../services/emailService.js', () => ({
+ sendActivationEmail: vi.fn().mockResolvedValue(),
+}));
+
+import app from './testApp.js';
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import User from '../models/User.js';
+import DiaryDate from '../models/DiaryDates.js';
+
+const registerAndActivate = async (email) => {
+ const password = 'Test123!';
+ await request(app).post('/api/auth/register').send({ email, password });
+ const user = await User.findOne({ where: { email } });
+ await user.update({ isActive: true });
+ return { user, credentials: { email, password } };
+};
+
+const loginAndGetToken = async (credentials) => {
+ const response = await request(app).post('/api/auth/login').send(credentials);
+ return response.body.token;
+};
+
+const authHeaders = (token) => ({
+ Authorization: `Bearer ${token}`,
+ authcode: token,
+});
+
+describe('Group Routes', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ });
+
+ it('erstellt, listet und aktualisiert Gruppen', async () => {
+ const { credentials } = await registerAndActivate('groups@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const clubResponse = await request(app)
+ .post('/api/clubs')
+ .set(authHeaders(token))
+ .send({ name: 'Group Route Club' });
+ const clubId = clubResponse.body.id;
+
+ const diaryDateResponse = await request(app)
+ .post(`/api/diary/${clubId}`)
+ .set(authHeaders(token))
+ .send({ date: '2026-08-01' });
+ const diaryDateId = diaryDateResponse.body.id;
+
+ const createResponse = await request(app)
+ .post('/api/groups')
+ .set(authHeaders(token))
+ .send({ clubid: clubId, dateid: diaryDateId, name: 'Gruppe 1', lead: 'Coach' });
+
+ expect(createResponse.status).toBe(201);
+ const groupId = createResponse.body.id;
+
+ const listResponse = await request(app)
+ .get(`/api/groups/${clubId}/${diaryDateId}`)
+ .set(authHeaders(token));
+
+ expect(listResponse.status).toBe(200);
+ expect(listResponse.body).toHaveLength(1);
+ expect(listResponse.body[0].name).toBe('Gruppe 1');
+
+ const updateResponse = await request(app)
+ .put(`/api/groups/${groupId}`)
+ .set(authHeaders(token))
+ .send({ clubid: clubId, dateid: diaryDateId, name: 'Gruppe 1', lead: 'Neue Leitung' });
+
+ expect(updateResponse.status).toBe(200);
+ expect(updateResponse.body.lead).toBe('Neue Leitung');
+ });
+});
diff --git a/backend/tests/groupService.test.js b/backend/tests/groupService.test.js
new file mode 100644
index 0000000..19ef650
--- /dev/null
+++ b/backend/tests/groupService.test.js
@@ -0,0 +1,57 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+vi.mock('../utils/userUtils.js', async () => {
+ const actual = await vi.importActual('../utils/userUtils.js');
+ return {
+ ...actual,
+ checkAccess: vi.fn().mockResolvedValue(true),
+ };
+});
+
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import groupService from '../services/groupService.js';
+import { checkAccess } from '../utils/userUtils.js';
+import Club from '../models/Club.js';
+import DiaryDate from '../models/DiaryDates.js';
+import Group from '../models/Group.js';
+
+describe('groupService', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ vi.clearAllMocks();
+ });
+
+ const setupClubAndDate = async () => {
+ const club = await Club.create({ name: 'Group Club' });
+ const diaryDate = await DiaryDate.create({ date: '2026-07-01', clubId: club.id });
+ return { club, diaryDate };
+ };
+
+ it('legt Gruppen an und gibt sie zurück', async () => {
+ const { club, diaryDate } = await setupClubAndDate();
+
+ const group = await groupService.addGroup('token', club.id, diaryDate.id, 'Team Blau', 'Coach');
+
+ expect(checkAccess).toHaveBeenCalledWith('token', club.id);
+ expect(group.name).toBe('Team Blau');
+
+ const groups = await groupService.getGroups('token', club.id, diaryDate.id);
+ expect(groups).toHaveLength(1);
+ expect(groups[0].lead).toBe('Coach');
+ });
+
+ it('aktualisiert Gruppenangaben und validiert Zugehörigkeit', async () => {
+ const { club, diaryDate } = await setupClubAndDate();
+ const group = await groupService.addGroup('token', club.id, diaryDate.id, 'Team Rot', 'Trainerin');
+
+ const updated = await groupService.changeGroup('token', group.id, club.id, diaryDate.id, 'Team Rot', 'Neuer Lead');
+
+ expect(updated.lead).toBe('Neuer Lead');
+
+ await expect(
+ groupService.changeGroup('token', group.id, club.id, diaryDate.id + 1, 'Fail', 'Lead')
+ ).rejects.toThrow('Datum nicht gefunden oder passt nicht zum Verein');
+ });
+});
diff --git a/backend/tests/matchRoutes.test.js b/backend/tests/matchRoutes.test.js
new file mode 100644
index 0000000..ab34492
--- /dev/null
+++ b/backend/tests/matchRoutes.test.js
@@ -0,0 +1,85 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import request from 'supertest';
+
+vi.mock('../services/emailService.js', () => ({
+ sendActivationEmail: vi.fn().mockResolvedValue(),
+}));
+
+import app from './testApp.js';
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import Season from '../models/Season.js';
+import League from '../models/League.js';
+import Team from '../models/Team.js';
+import Match from '../models/Match.js';
+import Club from '../models/Club.js';
+import Location from '../models/Location.js';
+import User from '../models/User.js';
+
+const registerAndActivate = async (email) => {
+ const password = 'Test123!';
+ await request(app).post('/api/auth/register').send({ email, password });
+ const user = await User.findOne({ where: { email } });
+ await user.update({ isActive: true });
+ return { user, credentials: { email, password } };
+};
+
+const loginAndGetToken = async (credentials) => {
+ const response = await request(app).post('/api/auth/login').send(credentials);
+ return response.body.token;
+};
+
+const authHeaders = (token) => ({
+ Authorization: `Bearer ${token}`,
+ authcode: token,
+});
+
+describe('Match Routes', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ });
+
+ it('liefert Matches und aktualisiert Spielerlisten', async () => {
+ const { credentials } = await registerAndActivate('matchroutes@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const clubResponse = await request(app)
+ .post('/api/clubs')
+ .set(authHeaders(token))
+ .send({ name: 'Match Route Club' });
+ const clubId = clubResponse.body.id;
+
+ const season = await Season.create({ season: '2025/2026' });
+ const league = await League.create({ name: 'Route Liga', clubId, seasonId: season.id });
+ const homeTeam = await Team.create({ name: 'Route Club I', clubId, leagueId: league.id, seasonId: season.id });
+ const guestTeam = await Team.create({ name: 'Route Club II', clubId, leagueId: league.id, seasonId: season.id });
+ const location = await Location.create({ name: 'Route Halle', address: 'Straße 1', city: 'Stadt', zip: '12345' });
+ const match = await Match.create({
+ clubId,
+ leagueId: league.id,
+ homeTeamId: homeTeam.id,
+ guestTeamId: guestTeam.id,
+ locationId: location.id,
+ date: new Date('2025-09-01T18:00:00Z'),
+ time: '18:00',
+ });
+
+ const listResponse = await request(app)
+ .get(`/api/matches/leagues/${clubId}/matches`)
+ .set(authHeaders(token))
+ .query({ seasonid: season.id });
+
+ expect(listResponse.status).toBe(200);
+ expect(listResponse.body).toHaveLength(1);
+ expect(listResponse.body[0].homeTeam.name).toBe('Route Club I');
+
+ const patchResponse = await request(app)
+ .patch(`/api/matches/${match.id}/players`)
+ .set(authHeaders(token))
+ .send({ playersReady: ['Alice'], playersPlanned: ['Bob'], playersPlayed: ['Charlie'] });
+
+ expect(patchResponse.status).toBe(200);
+ expect(patchResponse.body.data.playersReady).toEqual(['Alice']);
+ });
+});
diff --git a/backend/tests/matchService.test.js b/backend/tests/matchService.test.js
new file mode 100644
index 0000000..6e5b144
--- /dev/null
+++ b/backend/tests/matchService.test.js
@@ -0,0 +1,83 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+vi.mock('../utils/userUtils.js', async () => {
+ const actual = await vi.importActual('../utils/userUtils.js');
+ return {
+ ...actual,
+ checkAccess: vi.fn().mockResolvedValue(true),
+ };
+});
+
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import matchService from '../services/matchService.js';
+import { checkAccess } from '../utils/userUtils.js';
+import Season from '../models/Season.js';
+import League from '../models/League.js';
+import Team from '../models/Team.js';
+import Match from '../models/Match.js';
+import Club from '../models/Club.js';
+import Location from '../models/Location.js';
+
+describe('matchService', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ vi.clearAllMocks();
+ });
+
+ it('formatiert Team-Namen anhand der Altersklasse', () => {
+ expect(matchService.formatTeamNameWithAgeClass('Harheimer TC', 'Jugend 11')).toBe('Harheimer TC (J11)');
+ expect(matchService.formatTeamNameWithAgeClass('Harheimer TC', 'Senioren')).toBe('Harheimer TC (S)');
+ expect(matchService.formatTeamNameWithAgeClass('Harheimer TC', 'Erwachsene')).toBe('Harheimer TC');
+ });
+
+ it('erstellt Teams und gibt bestehende Einträge zurück', async () => {
+ const club = await Club.create({ name: 'Match Club' });
+ const season = await Season.create({ season: '2025/2026' });
+ const league = await League.create({ name: 'Verbandsliga', clubId: club.id, seasonId: season.id });
+
+ const teamId = await matchService.getOrCreateTeamId('Harheimer TC', 'Jugend 11', club.id, league.id, season.id);
+ const sameTeamId = await matchService.getOrCreateTeamId('Harheimer TC', 'Jugend 11', club.id, league.id, season.id);
+
+ expect(teamId).toBeTruthy();
+ expect(teamId).toBe(sameTeamId);
+ const team = await Team.findByPk(teamId);
+ expect(team.name).toBe('Harheimer TC (J11)');
+ });
+
+ it('liefert Ligen und Matches für eine Saison und aktualisiert Spielerlisten', async () => {
+ const club = await Club.create({ name: 'Season Club' });
+ const season = await Season.create({ season: '2025/2026' });
+ const league = await League.create({ name: 'Oberliga', clubId: club.id, seasonId: season.id });
+ const homeTeam = await Team.create({ name: 'Season Club I', clubId: club.id, leagueId: league.id, seasonId: season.id });
+ const guestTeam = await Team.create({ name: 'Season Club II', clubId: club.id, leagueId: league.id, seasonId: season.id });
+ const location = await Location.create({ name: 'Halle A', address: 'Straße 1', city: 'Stadt', zip: '12345' });
+
+ const match = await Match.create({
+ clubId: club.id,
+ leagueId: league.id,
+ homeTeamId: homeTeam.id,
+ guestTeamId: guestTeam.id,
+ locationId: location.id,
+ date: new Date('2025-09-01T18:00:00Z'),
+ time: '18:00',
+ });
+
+ const leagues = await matchService.getLeaguesForCurrentSeason('token', club.id, season.id);
+ expect(checkAccess).toHaveBeenCalledWith('token', club.id);
+ expect(leagues).toHaveLength(1);
+ expect(leagues[0].name).toBe('Oberliga');
+
+ const matches = await matchService.getMatchesForLeagues('token', club.id, season.id);
+ expect(matches).toHaveLength(1);
+ expect(matches[0].homeTeam.name).toBe('Season Club I');
+ expect(matches[0].location.name).toBe('Halle A');
+
+ await matchService.updateMatchPlayers('token', match.id, ['Alice'], ['Bob'], ['Charlie']);
+ const updated = await Match.findByPk(match.id);
+ expect(updated.playersReady).toEqual(['Alice']);
+ expect(updated.playersPlanned).toEqual(['Bob']);
+ expect(updated.playersPlayed).toEqual(['Charlie']);
+ });
+});
diff --git a/backend/tests/memberActivityRoutes.test.js b/backend/tests/memberActivityRoutes.test.js
new file mode 100644
index 0000000..9da0d59
--- /dev/null
+++ b/backend/tests/memberActivityRoutes.test.js
@@ -0,0 +1,131 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import request from 'supertest';
+
+vi.mock('../services/emailService.js', () => ({
+ sendActivationEmail: vi.fn().mockResolvedValue(),
+}));
+
+import app from './testApp.js';
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import User from '../models/User.js';
+import Club from '../models/Club.js';
+import Member from '../models/Member.js';
+import DiaryDate from '../models/DiaryDates.js';
+import DiaryDateActivity from '../models/DiaryDateActivity.js';
+import DiaryMemberActivity from '../models/DiaryMemberActivity.js';
+import Participant from '../models/Participant.js';
+import PredefinedActivity from '../models/PredefinedActivity.js';
+import Group from '../models/Group.js';
+import GroupActivity from '../models/GroupActivity.js';
+
+const registerAndActivate = async (email) => {
+ const password = 'Test123!';
+ await request(app).post('/api/auth/register').send({ email, password });
+ const user = await User.findOne({ where: { email } });
+ await user.update({ isActive: true });
+ return { user, credentials: { email, password } };
+};
+
+const loginAndGetToken = async (credentials) => {
+ const response = await request(app).post('/api/auth/login').send(credentials);
+ return response.body.token;
+};
+
+const authHeaders = (token) => ({
+ Authorization: `Bearer ${token}`,
+ authcode: token,
+});
+
+describe('MemberActivity Routes', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ });
+
+ it('aggregiert Aktivitäten und liefert letzte Teilnahmen', async () => {
+ const { credentials } = await registerAndActivate('memberactivity@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const clubResponse = await request(app)
+ .post('/api/clubs')
+ .set(authHeaders(token))
+ .send({ name: 'Activity Club' });
+ const clubId = clubResponse.body.id;
+
+ const member = await Member.create({
+ firstName: 'Max',
+ lastName: 'Mustermann',
+ phone: '0123456789',
+ street: 'Straße 1',
+ city: 'Stadt',
+ postalCode: '12345',
+ email: 'max@example.com',
+ clubId,
+ birthDate: '2005-01-01',
+ active: true,
+ testMembership: true,
+ picsInInternetAllowed: false,
+ gender: 'male',
+ });
+
+ const diaryDateRecent = await DiaryDate.create({ date: '2026-09-01', clubId });
+ const diaryDateOlder = await DiaryDate.create({ date: '2026-06-01', clubId });
+ const group = await Group.create({ diaryDateId: diaryDateRecent.id, name: 'Gruppe A' });
+ const otherGroup = await Group.create({ diaryDateId: diaryDateOlder.id, name: 'Gruppe B' });
+
+ const participant = await Participant.create({ diaryDateId: diaryDateRecent.id, memberId: member.id, groupId: group.id });
+
+ const activityGeneralDef = await PredefinedActivity.create({ name: 'Allgemeines Training' });
+ const activityGroupDef = await PredefinedActivity.create({ name: 'Gruppenübung' });
+ const activityOtherDef = await PredefinedActivity.create({ name: 'Falsche Gruppe' });
+
+ const generalActivity = await DiaryDateActivity.create({
+ diaryDateId: diaryDateRecent.id,
+ predefinedActivityId: activityGeneralDef.id,
+ orderId: 1,
+ isTimeblock: false,
+ });
+
+ const groupActivity = await DiaryDateActivity.create({
+ diaryDateId: diaryDateRecent.id,
+ predefinedActivityId: activityGroupDef.id,
+ orderId: 2,
+ isTimeblock: false,
+ });
+
+ await GroupActivity.create({ diaryDateActivity: groupActivity.id, groupId: group.id, customActivity: null });
+
+ const otherActivity = await DiaryDateActivity.create({
+ diaryDateId: diaryDateOlder.id,
+ predefinedActivityId: activityOtherDef.id,
+ orderId: 1,
+ isTimeblock: false,
+ });
+ await GroupActivity.create({ diaryDateActivity: otherActivity.id, groupId: otherGroup.id, customActivity: null });
+
+ await DiaryMemberActivity.bulkCreate([
+ { diaryDateActivityId: generalActivity.id, participantId: participant.id },
+ { diaryDateActivityId: groupActivity.id, participantId: participant.id },
+ { diaryDateActivityId: otherActivity.id, participantId: participant.id },
+ ]);
+
+ const activitiesResponse = await request(app)
+ .get(`/api/member-activities/${clubId}/${member.id}`)
+ .set(authHeaders(token))
+ .query({ period: 'year' });
+
+ expect(activitiesResponse.status).toBe(200);
+ const names = activitiesResponse.body.map((entry) => entry.name).sort();
+ expect(names).toEqual(['Allgemeines Training', 'Gruppenübung']);
+
+ const lastResponse = await request(app)
+ .get(`/api/member-activities/${clubId}/${member.id}/last-participations`)
+ .set(authHeaders(token))
+ .query({ limit: 1 });
+
+ expect(lastResponse.status).toBe(200);
+ expect(lastResponse.body).toHaveLength(1);
+ expect(lastResponse.body[0].activityName).toBe('Gruppenübung');
+ });
+});
diff --git a/backend/tests/memberNoteRoutes.test.js b/backend/tests/memberNoteRoutes.test.js
new file mode 100644
index 0000000..2678686
--- /dev/null
+++ b/backend/tests/memberNoteRoutes.test.js
@@ -0,0 +1,93 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import request from 'supertest';
+
+vi.mock('../services/emailService.js', () => ({
+ sendActivationEmail: vi.fn().mockResolvedValue(),
+}));
+
+import app from './testApp.js';
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import User from '../models/User.js';
+import Member from '../models/Member.js';
+
+const registerAndActivate = async (email) => {
+ const password = 'Test123!';
+ await request(app).post('/api/auth/register').send({ email, password });
+ const user = await User.findOne({ where: { email } });
+ await user.update({ isActive: true });
+ return { user, credentials: { email, password } };
+};
+
+const loginAndGetToken = async (credentials) => {
+ const response = await request(app).post('/api/auth/login').send(credentials);
+ return response.body.token;
+};
+
+const authHeaders = (token) => ({
+ Authorization: `Bearer ${token}`,
+ authcode: token,
+});
+
+const createMember = async (clubId) => {
+ return Member.create({
+ firstName: 'Nora',
+ lastName: 'Notiz',
+ phone: '0123456789',
+ street: 'Straße 3',
+ city: 'Stadt',
+ postalCode: '12345',
+ email: 'nora@example.com',
+ clubId,
+ birthDate: '1999-01-01',
+ active: true,
+ testMembership: false,
+ picsInInternetAllowed: false,
+ gender: 'female',
+ });
+};
+
+describe('MemberNote Routes', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ });
+
+ it('erstellt, listet und löscht Member Notes', async () => {
+ const { credentials } = await registerAndActivate('membernote@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const clubResponse = await request(app)
+ .post('/api/clubs')
+ .set(authHeaders(token))
+ .send({ name: 'Note Club' });
+ const clubId = clubResponse.body.id;
+
+ const member = await createMember(clubId);
+
+ const createResponse = await request(app)
+ .post('/api/member-notes')
+ .set(authHeaders(token))
+ .send({ memberId: member.id, clubId, content: 'Erste Notiz' });
+
+ expect(createResponse.status).toBe(201);
+ expect(createResponse.body[0].content).toBe('Erste Notiz');
+
+ const listResponse = await request(app)
+ .get(`/api/member-notes/${member.id}`)
+ .set(authHeaders(token))
+ .query({ clubId });
+
+ expect(listResponse.status).toBe(200);
+ expect(listResponse.body).toHaveLength(1);
+
+ const noteId = createResponse.body[0].id;
+ const deleteResponse = await request(app)
+ .delete(`/api/member-notes/${noteId}`)
+ .set(authHeaders(token))
+ .send({ clubId });
+
+ expect(deleteResponse.status).toBe(200);
+ expect(deleteResponse.body).toHaveLength(0);
+ });
+});
diff --git a/backend/tests/memberRoutes.test.js b/backend/tests/memberRoutes.test.js
new file mode 100644
index 0000000..ac56476
--- /dev/null
+++ b/backend/tests/memberRoutes.test.js
@@ -0,0 +1,93 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import request from 'supertest';
+
+vi.mock('../services/emailService.js', () => ({
+ sendActivationEmail: vi.fn().mockResolvedValue(),
+}));
+
+import app from './testApp.js';
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import User from '../models/User.js';
+import Member from '../models/Member.js';
+
+const registerAndActivate = async (email) => {
+ const password = 'Test123!';
+ await request(app).post('/api/auth/register').send({ email, password });
+ const user = await User.findOne({ where: { email } });
+ await user.update({ isActive: true });
+ return { user, credentials: { email, password } };
+};
+
+const loginAndGetToken = async (credentials) => {
+ const response = await request(app).post('/api/auth/login').send(credentials);
+ return response.body.token;
+};
+
+const authHeaders = (token) => ({
+ Authorization: `Bearer ${token}`,
+ authcode: token,
+});
+
+const createMember = async (clubId, overrides = {}) => {
+ return Member.create({
+ firstName: 'Lara',
+ lastName: 'Lang',
+ phone: '0123456789',
+ street: 'Straße 2',
+ city: 'Stadt',
+ postalCode: '12345',
+ email: 'lara@example.com',
+ clubId,
+ birthDate: '2001-01-01',
+ active: true,
+ testMembership: true,
+ picsInInternetAllowed: false,
+ gender: 'female',
+ ...overrides,
+ });
+};
+
+describe('Member quick action routes', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ });
+
+ it('setzt Testmitgliedschaft, Formularstatus und deaktiviert Mitglieder', async () => {
+ const { credentials } = await registerAndActivate('memberroutes@example.com');
+ const token = await loginAndGetToken(credentials);
+
+ const clubResponse = await request(app)
+ .post('/api/clubs')
+ .set(authHeaders(token))
+ .send({ name: 'Member Route Club' });
+ const clubId = clubResponse.body.id;
+
+ const member = await createMember(clubId, { testMembership: true, memberFormHandedOver: false, active: true });
+
+ const testResponse = await request(app)
+ .post(`/api/members/quick-update-test-membership/${clubId}/${member.id}`)
+ .set(authHeaders(token));
+
+ expect(testResponse.status).toBe(200);
+ await member.reload();
+ expect(member.testMembership).toBe(false);
+
+ const formResponse = await request(app)
+ .post(`/api/members/quick-update-member-form/${clubId}/${member.id}`)
+ .set(authHeaders(token));
+
+ expect(formResponse.status).toBe(200);
+ await member.reload();
+ expect(member.memberFormHandedOver).toBe(true);
+
+ const deactivateResponse = await request(app)
+ .post(`/api/members/quick-deactivate/${clubId}/${member.id}`)
+ .set(authHeaders(token));
+
+ expect(deactivateResponse.status).toBe(200);
+ await member.reload();
+ expect(member.active).toBe(false);
+ });
+});
diff --git a/backend/tests/memberService.test.js b/backend/tests/memberService.test.js
new file mode 100644
index 0000000..6c1aa09
--- /dev/null
+++ b/backend/tests/memberService.test.js
@@ -0,0 +1,75 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+vi.mock('../utils/userUtils.js', async () => {
+ const actual = await vi.importActual('../utils/userUtils.js');
+ return {
+ ...actual,
+ checkAccess: vi.fn().mockResolvedValue(true),
+ };
+});
+
+import sequelize from '../database.js';
+import '../models/index.js';
+
+import memberService from '../services/memberService.js';
+import { checkAccess } from '../utils/userUtils.js';
+import Member from '../models/Member.js';
+
+describe('memberService quick updates', () => {
+ beforeEach(async () => {
+ await sequelize.sync({ force: true });
+ vi.clearAllMocks();
+ });
+
+ const createMember = async (overrides = {}) => {
+ return Member.create({
+ firstName: 'Erika',
+ lastName: 'Musterfrau',
+ phone: '0123456789',
+ street: 'Straße 1',
+ city: 'Stadt',
+ postalCode: '12345',
+ email: 'erika@example.com',
+ clubId: 1,
+ birthDate: '2000-01-01',
+ active: true,
+ testMembership: true,
+ picsInInternetAllowed: false,
+ gender: 'female',
+ ...overrides,
+ });
+ };
+
+ it('entfernt Testmitgliedschaften und behandelt Fehlerfälle', async () => {
+ const member = await createMember({ testMembership: true });
+
+ const result = await memberService.quickUpdateTestMembership('token', member.clubId, member.id);
+
+ expect(checkAccess).toHaveBeenCalledWith('token', member.clubId);
+ expect(result.status).toBe(200);
+ await member.reload();
+ expect(member.testMembership).toBe(false);
+
+ const alreadyLive = await memberService.quickUpdateTestMembership('token', member.clubId, member.id);
+ expect(alreadyLive.status).toBe(400);
+ });
+
+ it('markiert Formular-Status und deaktiviert Mitglieder', async () => {
+ const member = await createMember({ testMembership: false, memberFormHandedOver: false });
+
+ const formResult = await memberService.quickUpdateMemberFormHandedOver('token', member.clubId, member.id);
+ expect(formResult.status).toBe(200);
+ await member.reload();
+ expect(member.memberFormHandedOver).toBe(true);
+
+ const deactivateResult = await memberService.quickDeactivateMember('token', member.clubId, member.id);
+ expect(deactivateResult.status).toBe(200);
+ await member.reload();
+ expect(member.active).toBe(false);
+ });
+
+ it('meldet 404 für fehlende Mitglieder', async () => {
+ const result = await memberService.quickDeactivateMember('token', 99, 123);
+ expect(result.status).toBe(404);
+ });
+});
diff --git a/backend/tests/setupTestEnv.js b/backend/tests/setupTestEnv.js
index dec1678..be05963 100644
--- a/backend/tests/setupTestEnv.js
+++ b/backend/tests/setupTestEnv.js
@@ -7,6 +7,7 @@ process.env.DB_STORAGE = process.env.DB_STORAGE || ':memory:';
process.env.JWT_SECRET = process.env.JWT_SECRET || 'test-secret';
process.env.EMAIL_USER = process.env.EMAIL_USER || 'noreply@example.com';
process.env.BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
+process.env.ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
afterAll(async () => {
await sequelize.close();
diff --git a/backend/tests/testApp.js b/backend/tests/testApp.js
index 3b63435..7fbdee2 100644
--- a/backend/tests/testApp.js
+++ b/backend/tests/testApp.js
@@ -7,6 +7,15 @@ import apiLogRoutes from '../routes/apiLogRoutes.js';
import clubRoutes from '../routes/clubRoutes.js';
import clubTeamRoutes from '../routes/clubTeamRoutes.js';
import diaryRoutes from '../routes/diaryRoutes.js';
+import diaryDateActivityRoutes from '../routes/diaryDateActivityRoutes.js';
+import diaryMemberActivityRoutes from '../routes/diaryMemberActivityRoutes.js';
+import diaryNoteRoutes from '../routes/diaryNoteRoutes.js';
+import diaryTagRoutes from '../routes/diaryTagRoutes.js';
+import groupRoutes from '../routes/groupRoutes.js';
+import matchRoutes from '../routes/matchRoutes.js';
+import memberActivityRoutes from '../routes/memberActivityRoutes.js';
+import memberRoutes from '../routes/memberRoutes.js';
+import memberNoteRoutes from '../routes/memberNoteRoutes.js';
const app = express();
@@ -19,6 +28,15 @@ app.use('/api/logs', apiLogRoutes);
app.use('/api/clubs', clubRoutes);
app.use('/api/clubteam', clubTeamRoutes);
app.use('/api/diary', diaryRoutes);
+app.use('/api/diary-date-activities', diaryDateActivityRoutes);
+app.use('/api/diary-member-activities', diaryMemberActivityRoutes);
+app.use('/api/diary-notes', diaryNoteRoutes);
+app.use('/api/diary-tags', diaryTagRoutes);
+app.use('/api/groups', groupRoutes);
+app.use('/api/matches', matchRoutes);
+app.use('/api/member-activities', memberActivityRoutes);
+app.use('/api/members', memberRoutes);
+app.use('/api/member-notes', memberNoteRoutes);
app.use((err, req, res, next) => {
const status = err?.status || err?.statusCode || 500;
diff --git a/frontend/src/utils/debounce.js b/frontend/src/utils/debounce.js
new file mode 100644
index 0000000..df8e671
--- /dev/null
+++ b/frontend/src/utils/debounce.js
@@ -0,0 +1,10 @@
+export function debounce(fn, wait = 300) {
+ let timeoutId;
+ return function (...args) {
+ const context = this;
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => {
+ fn.apply(context, args);
+ }, wait);
+ };
+}
diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue
index 29eb606..6ce604b 100644
--- a/frontend/src/views/DiaryView.vue
+++ b/frontend/src/views/DiaryView.vue
@@ -519,11 +519,6 @@
@@ -1292,4 +1318,13 @@ li {
.btn-cancel:hover {
background-color: #5a6268;
}
+
+.output ul li.active {
+ font-weight: 600;
+ color: var(--primary-color, #2b7cff);
+}
+
+.team-league {
+ color: var(--text-muted, #6c757d);
+}
diff --git a/frontend/src/views/TournamentsView.vue b/frontend/src/views/TournamentsView.vue
index 893433b..3979af6 100644
--- a/frontend/src/views/TournamentsView.vue
+++ b/frontend/src/views/TournamentsView.vue
@@ -670,20 +670,9 @@