Enhance diary note and tag management in backend controllers

Updated the diaryNoteController to require diaryDateId in note creation and improved error handling for missing fields. Enhanced the createTag function in diaryTagController to validate tag names and return appropriate responses. Additionally, refined the deleteTag function to ensure proper error handling when a tag is not found. These changes improve the robustness and usability of the diary management features.
This commit is contained in:
Torsten Schulz (local)
2025-11-11 08:29:18 +01:00
parent b8191e41ee
commit 20f204e70b
25 changed files with 1451 additions and 59 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -519,11 +519,6 @@
<script>
import { mapGetters } from 'vuex';
import apiClient from '../apiClient.js';
import apiClientAdmin from '../apiClientAdmin.js';
import apiClientMembers from '../apiClientMembers.js';
import apiClientDiary from '../apiClientDiary.js';
import apiClientTournaments from '../apiClientTournaments.js';
import { getSafeErrorMessage } from '../utils/errorMessages.js';
import Multiselect from 'vue-multiselect';
import Sortable from 'sortablejs';
import PDFGenerator from '../components/PDFGenerator.js';

View File

@@ -699,7 +699,7 @@ address={{address}}`;
});
},
parseAndImportTemplate() {
async parseAndImportTemplate() {
if (!this.importTemplate.trim()) {
return;
}

View File

@@ -15,9 +15,11 @@
<li class="special-link" @click="loadAllMatches">Gesamtspielplan</li>
<li class="special-link" @click="loadAdultMatches">Spielplan Erwachsene</li>
<li class="divider"></li>
<li v-for="league in leagues" :key="league.id" @click="loadMatchesForLeague(league.id, league.name)">{{
league.name }}</li>
<li v-if="leagues.length === 0" class="no-leagues">Keine Ligen für diese Saison gefunden</li>
<li v-for="team in teams" :key="team.id" @click="loadMatchesForTeam(team)"
:class="{ active: selectedTeam && selectedTeam.id === team.id }">
{{ team.name }}<span class="team-league" v-if="team.league && team.league.name"> ({{ team.league.name }})</span>
</li>
<li v-if="teams.length === 0" class="no-leagues">Keine Teams für diese Saison gefunden</li>
</ul>
<div class="flex-item" ref="scheduleContainer">
@@ -286,9 +288,10 @@ export default {
},
showImportModal: false,
selectedFile: null,
leagues: [],
teams: [],
matches: [],
selectedLeague: '',
selectedTeam: null,
hoveredMatch: null,
selectedSeasonId: null,
currentSeason: null,
@@ -483,13 +486,16 @@ export default {
});
this.showInfo('Erfolg', 'Spielplan erfolgreich importiert!', '', 'success');
this.closeImportModal();
this.loadLeagues();
this.loadTeams();
} catch (error) {
this.showInfo('Fehler', 'Fehler beim Importieren der CSV-Datei', '', 'error');
}
},
// Sortierfunktion für Ligen
sortLeagues(leagues) {
if (!Array.isArray(leagues)) {
return [];
}
// Ligen-Priorität
const leagueOrder = [
'1. Bundesliga',
@@ -547,32 +553,55 @@ export default {
});
},
async loadLeagues() {
async loadTeams() {
try {
const clubId = this.currentClub;
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/current/${clubId}${seasonParam}`);
this.leagues = this.sortLeagues(response.data);
const response = await apiClient.get(`/club-teams/club/${clubId}${seasonParam}`);
const teams = response.data.filter(team => !!team.league);
const uniqueLeaguesMap = new Map();
teams.forEach(team => {
if (team.league && !uniqueLeaguesMap.has(team.league.id)) {
uniqueLeaguesMap.set(team.league.id, team.league);
}
});
const sortedLeagues = this.sortLeagues(Array.from(uniqueLeaguesMap.values()));
const leagueOrder = sortedLeagues.map(l => l.id);
this.teams = teams.sort((a, b) => {
const idxA = leagueOrder.indexOf(a.league.id);
const idxB = leagueOrder.indexOf(b.league.id);
if (idxA !== idxB) return idxA - idxB;
return a.name.localeCompare(b.name);
});
} catch (error) {
console.error('ScheduleView: Error loading leagues:', error);
this.showInfo('Fehler', 'Fehler beim Laden der Ligen', '', 'error');
console.error('ScheduleView: Error loading teams:', error);
this.showInfo('Fehler', 'Fehler beim Laden der Teams', '', 'error');
}
},
onSeasonChange(season) {
this.currentSeason = season;
this.loadLeagues();
this.loadTeams();
// Leere die aktuellen Matches, da sie für eine andere Saison sind
this.matches = [];
this.selectedLeague = '';
this.selectedTeam = null;
},
async loadMatchesForLeague(leagueId, leagueName) {
this.selectedLeague = leagueName;
async loadMatchesForTeam(team) {
if (!team || !team.league) {
this.showInfo('Hinweis', 'Für dieses Team ist keine Liga hinterlegt. Bitte zuerst eine Liga zuordnen.', '', 'warning');
return;
}
this.selectedTeam = team;
this.selectedLeague = `${team.name}${team.league?.name ? ` (${team.league.name})` : ''}`;
this.activeTab = 'schedule';
try {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches/${leagueId}`);
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches/${team.league.id}`);
this.matches = response.data;
// Lade auch die Tabellendaten für diese Liga
await this.loadLeagueTable(leagueId);
await this.loadLeagueTable(team.league.id);
} catch (error) {
this.showInfo('Fehler', 'Fehler beim Laden der Matches', '', 'error');
this.matches = [];
@@ -580,6 +609,7 @@ export default {
},
async loadAllMatches() {
this.selectedLeague = 'Gesamtspielplan';
this.selectedTeam = null;
try {
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches${seasonParam}`);
@@ -591,6 +621,7 @@ export default {
},
async loadAdultMatches() {
this.selectedLeague = 'Spielplan Erwachsene';
this.selectedTeam = null;
try {
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches${seasonParam}`);
@@ -778,21 +809,15 @@ export default {
},
async fetchTableFromMyTischtennis() {
if (!this.selectedLeague || this.selectedLeague === 'Gesamtspielplan' || this.selectedLeague === 'Spielplan Erwachsene') {
if (!this.selectedTeam || !this.selectedTeam.league || this.selectedLeague === 'Gesamtspielplan' || this.selectedLeague === 'Spielplan Erwachsene') {
this.showInfo('Info', 'Bitte wählen Sie eine spezifische Liga aus, um die Tabelle zu laden.', '', 'info');
return;
}
this.fetchingTable = true;
try {
// Find the league ID for the current selected league
const league = this.leagues.find(l => l.name === this.selectedLeague);
if (!league) {
this.showInfo('Fehler', 'Liga nicht gefunden', '', 'error');
return;
}
const response = await apiClient.post(`/matches/leagues/${this.currentClub}/table/${league.id}/fetch`);
const leagueId = this.selectedTeam.league.id;
const response = await apiClient.post(`/matches/leagues/${this.currentClub}/table/${leagueId}/fetch`);
this.leagueTable = response.data.data;
this.showInfo('Erfolg', 'Tabellendaten erfolgreich von MyTischtennis geladen!', '', 'success');
} catch (error) {
@@ -804,9 +829,10 @@ export default {
},
},
async created() {
// Ligen werden geladen, sobald eine Saison ausgewählt ist
// Die SeasonSelector-Komponente wird automatisch die aktuelle Saison auswählen
// und dann onSeasonChange aufrufen, was loadLeagues() triggert
// Teams werden geladen, sobald eine Saison ausgewählt ist
// Die SeasonSelector-Komponente wählt automatisch die aktuelle Saison aus
// und ruft anschließend onSeasonChange auf, was loadTeams() ausführt
this.loadTeams();
}
};
</script>
@@ -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);
}
</style>

View File

@@ -670,20 +670,9 @@
<script>
import { mapGetters } from 'vuex';
import apiClient from '../apiClient';
import apiClient from '../apiClient.js';
import InfoDialog from '../components/InfoDialog.vue';
import ConfirmDialog from '../components/ConfirmDialog.vue';
import CsvImportDialog from '../components/CsvImportDialog.vue';
import MemberSelectionDialog from '../components/MemberSelectionDialog.vue';
import TournamentGroupEditor from '../components/TournamentGroupEditor.vue';
import TournamentKoEditor from '../components/TournamentKoEditor.vue';
import TournamentSeedingDialog from '../components/TournamentSeedingDialog.vue';
import TournamentParticipantMatrix from '../components/TournamentParticipantMatrix.vue';
import TournamentMatchMatrix from '../components/TournamentMatchMatrix.vue';
import TournamentKoBracket from '../components/TournamentKoBracket.vue';
import TournamentExportDialog from '../components/TournamentExportDialog.vue';
import TournamentParticipantStats from '../components/TournamentParticipantStats.vue';
import TournamentImportHistory from '../components/TournamentImportHistory.vue';
import { buildInfoConfig, buildConfirmConfig, safeErrorMessage } from '../utils/dialogUtils.js';
export default {
name: 'TournamentsView',