Erweitert den MatchReportApiDialog um neue Funktionen zur Verwaltung von Spielberichten. Implementiert eine verbesserte Logik zur Berechnung der Gesamtpunkte und Sätze sowie zur Validierung von Eingaben. Fügt visuelle Hinweise für den Abschlussstatus und Warnungen bei fehlerhaften Eingaben hinzu. Optimiert die Benutzeroberfläche mit neuen CSS-Stilen für eine bessere Benutzererfahrung.

This commit is contained in:
Torsten Schulz (local)
2025-11-12 13:40:55 +01:00
524 changed files with 55207 additions and 17236 deletions

View File

@@ -0,0 +1,85 @@
import apiLogService from '../services/apiLogService.js';
import HttpError from '../exceptions/HttpError.js';
class ApiLogController {
/**
* GET /api/logs
* Get API logs with optional filters
*/
async getLogs(req, res, next) {
try {
const {
userId,
logType,
method,
path,
statusCode,
startDate,
endDate,
limit = 100,
offset = 0
} = req.query;
const result = await apiLogService.getLogs({
userId: userId ? parseInt(userId) : null,
logType,
method,
path,
statusCode: statusCode ? parseInt(statusCode) : null,
startDate,
endDate,
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json({
success: true,
data: result
});
} catch (error) {
next(error);
}
}
/**
* GET /api/logs/:id
* Get a single log entry by ID
*/
async getLogById(req, res, next) {
try {
const { id } = req.params;
const log = await apiLogService.getLogById(parseInt(id));
if (!log) {
throw new HttpError('Log entry not found', 404);
}
res.json({
success: true,
data: log
});
} catch (error) {
next(error);
}
}
/**
* GET /api/logs/scheduler/last-executions
* Get last execution info for scheduler jobs
*/
async getLastSchedulerExecutions(req, res, next) {
try {
const { clubId } = req.query;
const results = await apiLogService.getLastSchedulerExecutions(clubId ? parseInt(clubId) : null);
res.json({
success: true,
data: results
});
} catch (error) {
next(error);
}
}
}
export default new ApiLogController();

View File

@@ -25,7 +25,7 @@ export const addClub = async (req, res) => {
}
const newClub = await ClubService.createClub(clubName);
await ClubService.addUserToClub(user.id, newClub.id);
await ClubService.addUserToClub(user.id, newClub.id, true); // true = isOwner
res.status(200).json(newClub);
} catch (error) {
console.error('[addClub] - error:', error);

View File

@@ -79,11 +79,23 @@ export const getDiaryDateActivities = async (req, res) => {
export const addGroupActivity = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateId, groupId, activity } = req.body;
const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity);
const { clubId, diaryDateId, groupId, activity, timeblockId } = req.body;
const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, timeblockId);
res.status(201).json(activityItem);
} catch (error) {
devLog(error);
res.status(500).json({ error: 'Error adding group activity' });
}
}
export const deleteGroupActivity = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupActivityId } = req.params;
await diaryDateActivityService.deleteGroupActivity(userToken, clubId, groupActivityId);
res.status(200).json({ message: 'Group activity deleted' });
} catch (error) {
devLog(error);
res.status(500).json({ error: 'Error deleting group activity' });
}
}

View File

@@ -19,20 +19,32 @@ export const addMembersToActivity = async (req, res) => {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateActivityId } = req.params;
const { participantIds } = req.body; // array of participant ids
await checkAccess(userToken, clubId);
if (!participantIds || !Array.isArray(participantIds)) {
console.error('[addMembersToActivity] Invalid participantIds:', participantIds);
return res.status(400).json({ error: 'participantIds must be an array' });
}
const validParticipants = await Participant.findAll({ where: { id: participantIds } });
const validIds = new Set(validParticipants.map(p => p.id));
const created = [];
for (const pid of participantIds) {
if (!validIds.has(pid)) continue;
if (!validIds.has(pid)) {
continue;
}
const existing = await DiaryMemberActivity.findOne({ where: { diaryDateActivityId, participantId: pid } });
if (!existing) {
const rec = await DiaryMemberActivity.create({ diaryDateActivityId, participantId: pid });
created.push(rec);
} else {
}
}
res.status(201).json(created);
} catch (e) {
console.error('[addMembersToActivity] Error:', e);
res.status(500).json({ error: 'Error adding members to activity' });
}
};

View File

@@ -18,17 +18,28 @@ 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', required: false }],
});
return res.status(201).json(noteWithTags ?? newNote);
}
const noteWithTags = await DiaryNote.findByPk(newNote.id, {
include: [{ model: DiaryTag, as: 'tags' }],
});
res.status(201).json(noteWithTags);
res.status(201).json(newNote);
} catch (error) {
console.error('[createNote] - Error:', 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

@@ -58,3 +58,90 @@ export const getMatchesForLeague = async (req, res) => {
return res.status(500).json({ error: 'Failed to retrieve matches' });
}
};
export const getLeagueTable = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, leagueId } = req.params;
const table = await MatchService.getLeagueTable(userToken, clubId, leagueId);
return res.status(200).json(table);
} catch (error) {
console.error('Error retrieving league table:', error);
return res.status(500).json({ error: 'Failed to retrieve league table' });
}
};
export const fetchLeagueTableFromMyTischtennis = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, leagueId } = req.params;
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const autoFetchService = (await import('../services/autoFetchMatchResultsService.js')).default;
await autoFetchService.fetchAndUpdateLeagueTable(userId, leagueId);
// Return updated table data
const table = await MatchService.getLeagueTable(userToken, clubId, leagueId);
return res.status(200).json({
message: 'League table updated from MyTischtennis',
data: table
});
} catch (error) {
console.error('Error fetching league table from MyTischtennis:', error);
return res.status(500).json({ error: 'Failed to fetch league table from MyTischtennis' });
}
};
export const updateMatchPlayers = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { matchId } = req.params;
const { playersReady, playersPlanned, playersPlayed } = req.body;
const result = await MatchService.updateMatchPlayers(
userToken,
matchId,
playersReady,
playersPlanned,
playersPlayed
);
return res.status(200).json({
message: 'Match players updated successfully',
data: result
});
} catch (error) {
console.error('Error updating match players:', error);
return res.status(error.statusCode || 500).json({
error: error.message || 'Failed to update match players'
});
}
};
export const getPlayerMatchStats = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, leagueId } = req.params;
const { seasonid: seasonId } = req.query;
const stats = await MatchService.getPlayerMatchStats(userToken, clubId, leagueId, seasonId);
return res.status(200).json(stats);
} catch (error) {
console.error('Error retrieving player match stats:', error);
return res.status(error.statusCode || 500).json({
error: error.message || 'Failed to retrieve player match stats'
});
}
};

View File

@@ -0,0 +1,232 @@
import { checkAccess } from '../utils/userUtils.js';
import DiaryMemberActivity from '../models/DiaryMemberActivity.js';
import DiaryDateActivity from '../models/DiaryDateActivity.js';
import DiaryDates from '../models/DiaryDates.js';
import Participant from '../models/Participant.js';
import PredefinedActivity from '../models/PredefinedActivity.js';
import GroupActivity from '../models/GroupActivity.js';
import { Op } from 'sequelize';
export const getMemberActivities = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, memberId } = req.params;
const { period } = req.query; // 'month', '3months', '6months', 'year', 'all'
await checkAccess(userToken, clubId);
// Calculate date range based on period
const now = new Date();
let startDate = null;
switch (period) {
case 'month':
startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
break;
case '3months':
startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate());
break;
case '6months':
startDate = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate());
break;
case 'year':
startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
break;
case 'all':
default:
startDate = null;
break;
}
// Get participant ID for this member
const participants = await Participant.findAll({
where: { memberId: memberId }
});
if (participants.length === 0) {
return res.status(200).json([]);
}
const participantIds = participants.map(p => p.id);
// Get all diary member activities for this member
const whereClause = {
participantId: participantIds
};
const memberActivities = await DiaryMemberActivity.findAll({
where: whereClause,
include: [
{
model: Participant,
as: 'participant',
attributes: ['id', 'groupId', 'diaryDateId']
},
{
model: DiaryDateActivity,
as: 'activity',
include: [
{
model: DiaryDates,
as: 'diaryDate',
where: startDate ? {
date: {
[Op.gte]: startDate
}
} : {}
},
{
model: PredefinedActivity,
as: 'predefinedActivity'
},
{
model: GroupActivity,
as: 'groupActivities',
required: false
}
]
}
],
order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']]
});
// Group activities by name and count occurrences, considering group assignment
const activityMap = new Map();
for (const ma of memberActivities) {
if (!ma.activity || !ma.activity.predefinedActivity || !ma.participant) {
continue;
}
// Check group assignment
const participantGroupId = ma.participant.groupId;
const activityGroupIds = ma.activity.groupActivities?.map(ga => ga.groupId) || [];
// Filter: Only count if:
// 1. Activity has no group assignment (empty activityGroupIds) - activity is for all groups OR
// 2. Participant's group matches one of the activity's groups
const shouldCount = activityGroupIds.length === 0 ||
(participantGroupId !== null && activityGroupIds.includes(participantGroupId));
if (!shouldCount) {
continue;
}
const activity = ma.activity.predefinedActivity;
const activityName = activity.name;
const date = ma.activity.diaryDate?.date;
if (!activityMap.has(activityName)) {
activityMap.set(activityName, {
name: activityName,
count: 0,
dates: []
});
}
const activityData = activityMap.get(activityName);
activityData.count++;
if (date) {
activityData.dates.push(date);
}
}
// Convert map to array and sort by count
const activities = Array.from(activityMap.values())
.sort((a, b) => b.count - a.count);
return res.status(200).json(activities);
} catch (error) {
console.error('Error fetching member activities:', error);
return res.status(500).json({ error: 'Failed to fetch member activities' });
}
};
export const getMemberLastParticipations = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, memberId } = req.params;
const { limit = 3 } = req.query;
await checkAccess(userToken, clubId);
// Get participant ID for this member
const participants = await Participant.findAll({
where: { memberId: memberId }
});
if (participants.length === 0) {
return res.status(200).json([]);
}
const participantIds = participants.map(p => p.id);
// Get last participations for this member
const memberActivities = await DiaryMemberActivity.findAll({
where: {
participantId: participantIds
},
include: [
{
model: Participant,
as: 'participant',
attributes: ['id', 'groupId', 'diaryDateId']
},
{
model: DiaryDateActivity,
as: 'activity',
include: [
{
model: DiaryDates,
as: 'diaryDate'
},
{
model: PredefinedActivity,
as: 'predefinedActivity'
},
{
model: GroupActivity,
as: 'groupActivities',
required: false
}
]
}
],
order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']],
limit: parseInt(limit) * 10 // Get more to filter by group
});
// Format the results, considering group assignment
const participations = memberActivities
.filter(ma => {
if (!ma.activity || !ma.activity.predefinedActivity || !ma.activity.diaryDate || !ma.participant) {
return false;
}
// Check group assignment
const participantGroupId = ma.participant.groupId;
const activityGroupIds = ma.activity.groupActivities?.map(ga => ga.groupId) || [];
// Filter: Only count if:
// 1. Activity has no group assignment (empty activityGroupIds) - activity is for all groups OR
// 2. Participant's group matches one of the activity's groups
return activityGroupIds.length === 0 ||
(participantGroupId !== null && activityGroupIds.includes(participantGroupId));
})
.slice(0, parseInt(limit)) // Limit after filtering
.map(ma => ({
id: ma.id,
activityName: ma.activity.predefinedActivity.name,
date: ma.activity.diaryDate.date,
diaryDateId: ma.activity.diaryDate.id
}));
return res.status(200).json(participations);
} catch (error) {
console.error('Error fetching member last participations:', error);
return res.status(500).json({ error: 'Failed to fetch member last participations' });
}
};

View File

@@ -1,13 +1,12 @@
import MemberService from "../services/memberService.js";
import MemberTransferService from "../services/memberTransferService.js";
import { devLog } from '../utils/logger.js';
const getClubMembers = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { id: clubId, showAll } = req.params;
if (showAll === null) {
showAll = false;
}
const { id: clubId } = req.params;
const showAll = req.params.showAll ?? 'false';
res.status(200).json(await MemberService.getClubMembers(userToken, clubId, showAll));
} catch(error) {
res.status(500).json({ error: 'systemerror' });
@@ -27,16 +26,16 @@ const getWaitingApprovals = async(req, res) => {
const setClubMembers = async (req, res) => {
try {
const { id: memberId, firstname: firstName, lastname: lastName, street, city, birthdate, phone, email, active,
testMembership, picsInInternetAllowed, gender, ttr, qttr } = req.body;
const { id: memberId, firstname: firstName, lastname: lastName, street, city, postalCode, birthdate, phone, email, active,
testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, contacts } = req.body;
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate,
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr);
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, postalCode, birthdate,
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, contacts);
res.status(addResult.status || 500).json(addResult.response);
} catch (error) {
console.error('[setClubMembers] - Error:', error);
res.status(500).json({ error: 'Failed to upload image' });
res.status(500).json({ error: 'Failed to save member' });
}
}
@@ -44,8 +43,12 @@ const uploadMemberImage = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.uploadMemberImage(userToken, clubId, memberId, req.file.buffer);
res.status(result.status).json(result.message ? { message: result.message } : { error: result.error });
const makePrimary =
req.body?.makePrimary === true ||
req.body?.makePrimary === 'true' ||
req.query?.makePrimary === 'true';
const result = await MemberService.uploadMemberImage(userToken, clubId, memberId, req.file.buffer, { makePrimary });
res.status(result.status).json(result.response ?? { success: false, error: 'Unknown upload result' });
} catch (error) {
console.error('[uploadMemberImage] - Error:', error);
res.status(500).json({ error: 'Failed to upload image' });
@@ -54,9 +57,11 @@ const uploadMemberImage = async (req, res) => {
const getMemberImage = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { clubId, memberId, imageId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.getMemberImage(userToken, clubId, memberId);
// Support "latest" as imageId to get the latest image
const actualImageId = imageId === 'latest' ? null : (imageId || null);
const result = await MemberService.getMemberImage(userToken, clubId, memberId, actualImageId);
if (result.status === 200) {
res.sendFile(result.imagePath);
} else {
@@ -80,4 +85,162 @@ const updateRatingsFromMyTischtennis = async (req, res) => {
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis };
const rotateMemberImage = async (req, res) => {
try {
const { clubId, memberId, imageId } = req.params;
const { direction } = req.body;
const { authcode: userToken } = req.headers;
if (!direction || !['left', 'right'].includes(direction)) {
return res.status(400).json({
success: false,
error: 'Ungültige Drehrichtung. Verwenden Sie "left" oder "right".'
});
}
const result = await MemberService.rotateMemberImage(userToken, clubId, memberId, imageId, direction);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[rotateMemberImage] - Error:', error);
res.status(500).json({ success: false, error: 'Failed to rotate image' });
}
};
const deleteMemberImage = async (req, res) => {
try {
const { clubId, memberId, imageId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.deleteMemberImage(userToken, clubId, memberId, imageId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[deleteMemberImage] - Error:', error);
res.status(500).json({ success: false, error: 'Failed to delete image' });
}
};
const generateMemberGallery = async (req, res) => {
try {
const { clubId } = req.params;
const { authcode: userToken } = req.headers;
const size = parseInt(req.query.size) || 200; // Default: 200x200
const format = req.query.format || 'image'; // 'image' or 'json'
const result = await MemberService.generateMemberGallery(userToken, clubId, size);
if (result.status === 200) {
if (format === 'json') {
// Return member information for interactive gallery
return res.status(200).json({
members: result.galleryEntries.map(entry => ({
memberId: entry.memberId,
firstName: entry.firstName,
lastName: entry.lastName,
fullName: entry.fullName
}))
});
}
res.setHeader('Content-Type', 'image/png');
res.setHeader('Cache-Control', 'no-store');
return res.status(200).send(result.buffer);
}
return res.status(result.status).json({ error: result.error || 'Galerie konnte nicht erstellt werden' });
} catch (error) {
console.error('[generateMemberGallery] - Error:', error);
res.status(500).json({ error: 'Failed to generate member gallery' });
}
};
const setPrimaryMemberImage = async (req, res) => {
try {
const { clubId, memberId, imageId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.setPrimaryMemberImage(userToken, clubId, memberId, imageId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[setPrimaryMemberImage] - Error:', error);
res.status(500).json({ success: false, error: 'Failed to update primary image' });
}
};
const quickUpdateTestMembership = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.quickUpdateTestMembership(userToken, clubId, memberId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[quickUpdateTestMembership] - Error:', error);
res.status(500).json({ error: 'Failed to update test membership' });
}
};
const quickUpdateMemberFormHandedOver = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.quickUpdateMemberFormHandedOver(userToken, clubId, memberId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[quickUpdateMemberFormHandedOver] - Error:', error);
res.status(500).json({ error: 'Failed to update member form status' });
}
};
const quickDeactivateMember = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.quickDeactivateMember(userToken, clubId, memberId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[quickDeactivateMember] - Error:', error);
res.status(500).json({ error: 'Failed to deactivate member' });
}
};
const transferMembers = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const config = req.body;
// Validierung
if (!config.transferEndpoint) {
return res.status(400).json({
success: false,
error: 'Übertragungs-Endpoint ist erforderlich'
});
}
if (!config.transferTemplate) {
return res.status(400).json({
success: false,
error: 'Übertragungs-Template ist erforderlich'
});
}
const result = await MemberTransferService.transferMembers(userToken, clubId, config);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[transferMembers] - Error:', error);
res.status(500).json({
success: false,
error: 'Fehler bei der Übertragung: ' + error.message
});
}
};
export {
getClubMembers,
getWaitingApprovals,
setClubMembers,
uploadMemberImage,
getMemberImage,
updateRatingsFromMyTischtennis,
rotateMemberImage,
transferMembers,
quickUpdateTestMembership,
quickUpdateMemberFormHandedOver,
quickDeactivateMember,
deleteMemberImage,
setPrimaryMemberImage,
generateMemberGallery
};

View File

@@ -1,4 +1,5 @@
import MemberNoteService from "../services/memberNoteService.js";
import MemberNote from '../models/MemberNote.js';
import { devLog } from '../utils/logger.js';
const getMemberNotes = async (req, res) => {
@@ -21,6 +22,7 @@ const addMemberNote = async (req, res) => {
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(201).json(notes);
} catch (error) {
console.error('[addMemberNote] - Error:', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -30,11 +32,16 @@ const deleteMemberNote = async (req, res) => {
const { authcode: userToken } = req.headers;
const { noteId } = req.params;
const { clubId } = req.body;
const memberId = await MemberNoteService.getMemberIdForNote(noteId); // Member ID ermitteln
const note = await MemberNote.findByPk(noteId);
if (!note) {
return res.status(404).json({ error: 'notfound' });
}
const memberId = note.memberId;
await MemberNoteService.deleteNoteForMember(userToken, clubId, noteId);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
console.error('[deleteMemberNote] - Error:', error);
res.status(500).json({ error: 'systemerror' });
}
};

View File

@@ -0,0 +1,51 @@
import MemberTransferConfigService from '../services/memberTransferConfigService.js';
export const getConfig = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberTransferConfigService.getConfig(userToken, clubId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[getConfig] Error:', error);
res.status(500).json({
success: false,
error: 'Fehler beim Laden der Konfiguration'
});
}
};
export const saveConfig = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const configData = req.body;
const result = await MemberTransferConfigService.saveConfig(userToken, clubId, configData);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[saveConfig] Error:', error);
res.status(500).json({
success: false,
error: 'Fehler beim Speichern der Konfiguration'
});
}
};
export const deleteConfig = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberTransferConfigService.deleteConfig(userToken, clubId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[deleteConfig] Error:', error);
res.status(500).json({
success: false,
error: 'Fehler beim Löschen der Konfiguration'
});
}
};

View File

@@ -42,15 +42,15 @@ class MyTischtennisController {
async upsertAccount(req, res, next) {
try {
const userId = req.user.id;
const { email, password, savePassword, userPassword } = req.body;
const { email, password, savePassword, autoUpdateRatings, userPassword } = req.body;
if (!email) {
throw new HttpError(400, 'E-Mail-Adresse erforderlich');
throw new HttpError('E-Mail-Adresse erforderlich', 400);
}
// Wenn ein Passwort gesetzt wird, muss das App-Passwort angegeben werden
if (password && !userPassword) {
throw new HttpError(400, 'App-Passwort erforderlich zum Setzen des myTischtennis-Passworts');
throw new HttpError('App-Passwort erforderlich zum Setzen des myTischtennis-Passworts', 400);
}
const account = await myTischtennisService.upsertAccount(
@@ -58,6 +58,7 @@ class MyTischtennisController {
email,
password,
savePassword || false,
autoUpdateRatings || false,
userPassword
);
@@ -80,7 +81,7 @@ class MyTischtennisController {
const deleted = await myTischtennisService.deleteAccount(userId);
if (!deleted) {
throw new HttpError(404, 'Kein myTischtennis-Account gefunden');
throw new HttpError('Kein myTischtennis-Account gefunden', 404);
}
res.status(200).json({ message: 'myTischtennis-Account gelöscht' });
@@ -127,6 +128,77 @@ class MyTischtennisController {
next(error);
}
}
/**
* GET /api/mytischtennis/update-history
* Get update ratings history
*/
async getUpdateHistory(req, res, next) {
try {
const userId = req.user.id;
const history = await myTischtennisService.getUpdateHistory(userId);
res.status(200).json({ history });
} catch (error) {
next(error);
}
}
/**
* Get fetch logs for current user
*/
async getFetchLogs(req, res, next) {
try {
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
const logs = await fetchLogService.getFetchLogs(userId, {
limit: req.query.limit ? parseInt(req.query.limit) : 50,
fetchType: req.query.type
});
res.status(200).json({ logs });
} catch (error) {
next(error);
}
}
/**
* Get latest successful fetches for each type
*/
async getLatestFetches(req, res, next) {
try {
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
const latestFetches = await fetchLogService.getLatestSuccessfulFetches(userId);
res.status(200).json({ latestFetches });
} catch (error) {
next(error);
}
}
}
export default new MyTischtennisController();

View File

@@ -0,0 +1,601 @@
import myTischtennisUrlParserService from '../services/myTischtennisUrlParserService.js';
import myTischtennisService from '../services/myTischtennisService.js';
import MemberService from '../services/memberService.js';
import autoFetchMatchResultsService from '../services/autoFetchMatchResultsService.js';
import apiLogService from '../services/apiLogService.js';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
import User from '../models/User.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
class MyTischtennisUrlController {
/**
* Parse myTischtennis URL and return configuration data
* POST /api/mytischtennis/parse-url
* Body: { url: string }
*/
async parseUrl(req, res, next) {
try {
const { url } = req.body;
if (!url) {
throw new HttpError('URL is required', 400);
}
// Validate URL
if (!myTischtennisUrlParserService.isValidTeamUrl(url)) {
throw new HttpError('Invalid myTischtennis URL format', 400);
}
// Parse URL
const parsedData = myTischtennisUrlParserService.parseUrl(url);
// Try to fetch additional data if user is authenticated
const userIdOrEmail = req.headers.userid;
let completeData = parsedData;
if (userIdOrEmail) {
// Get actual user ID
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (user) userId = user.id;
}
try {
const account = await myTischtennisService.getAccount(userId);
if (account && account.accessToken) {
completeData = await myTischtennisUrlParserService.fetchTeamData(
parsedData,
account.cookie,
account.accessToken
);
}
} catch (error) {
// Continue with parsed data only
}
}
res.json({
success: true,
data: completeData
});
} catch (error) {
next(error);
}
}
/**
* Configure team from myTischtennis URL
* POST /api/mytischtennis/configure-team
* Body: { url: string, clubTeamId: number, createLeague?: boolean, createSeason?: boolean }
*/
async configureTeam(req, res, next) {
try {
const { url, clubTeamId, createLeague, createSeason } = req.body;
const userIdOrEmail = req.headers.userid;
if (!url || !clubTeamId) {
throw new HttpError('URL and clubTeamId are required', 400);
}
// Get actual user ID
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
throw new HttpError('User not found', 404);
}
userId = user.id;
}
// Parse URL
const parsedData = myTischtennisUrlParserService.parseUrl(url);
// Try to fetch additional data
let completeData = parsedData;
const account = await myTischtennisService.getAccount(userId);
if (account && account.accessToken) {
try {
completeData = await myTischtennisUrlParserService.fetchTeamData(
parsedData,
account.cookie,
account.accessToken
);
} catch (error) {
}
}
// Find or create season
let season = await Season.findOne({
where: { season: completeData.season }
});
if (!season && createSeason) {
season = await Season.create({
season: completeData.season
});
}
if (!season) {
throw new HttpError(`Season ${completeData.season} not found. Set createSeason=true to create it.`, 404);
}
// Find or create league
const team = await ClubTeam.findByPk(clubTeamId);
if (!team) {
throw new HttpError('Club team not found', 404);
}
let league;
// First, try to find existing league by name and season
const leagueName = completeData.leagueName || completeData.groupname;
league = await League.findOne({
where: {
name: leagueName,
seasonId: season.id,
clubId: team.clubId
}
});
if (league) {
devLog(`Found existing league: ${league.name} (ID: ${league.id})`);
// Update myTischtennis fields
await league.update({
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
} else if (team.leagueId) {
// Team has a league assigned, update it
league = await League.findByPk(team.leagueId);
if (league) {
devLog(`Updating team's existing league: ${league.name} (ID: ${league.id})`);
await league.update({
name: leagueName,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
}
} else if (createLeague) {
// Create new league
devLog(`Creating new league: ${leagueName}`);
league = await League.create({
name: leagueName,
seasonId: season.id,
clubId: team.clubId,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
} else {
throw new HttpError('League not found and team has no league assigned. Set createLeague=true to create one.', 400);
}
// Update team
await team.update({
myTischtennisTeamId: completeData.teamId,
leagueId: league.id,
seasonId: season.id
});
res.json({
success: true,
message: 'Team configured successfully',
data: {
team: {
id: team.id,
name: team.name,
myTischtennisTeamId: completeData.teamId
},
league: {
id: league.id,
name: league.name,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
},
season: {
id: season.id,
name: season.season
},
parsedData: completeData
}
});
} catch (error) {
next(error);
}
}
/**
* Manually fetch team data from myTischtennis
* POST /api/mytischtennis/fetch-team-data
* Body: { clubTeamId: number }
*/
async fetchTeamData(req, res, next) {
// Define outside of try/catch so catch has access
let account = null;
let team = null;
let myTischtennisUrl = null;
let requestStartTime = null;
try {
const { clubTeamId } = req.body;
const userIdOrEmail = req.headers.userid;
if (!clubTeamId) {
throw new HttpError('clubTeamId is required', 400);
}
if (!userIdOrEmail) {
throw new HttpError('User-ID fehlt. Bitte melden Sie sich an.', 401);
}
// Get actual user ID (userid header might be email address)
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
// It's an email, find the user
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
throw new HttpError('User not found', 404);
}
userId = user.id;
}
// Get myTischtennis session (similar to memberService.updateRatingsFromMyTischtennis)
let session;
try {
session = await myTischtennisService.getSession(userId);
} catch (sessionError) {
// Versuche automatischen Login mit gespeicherten Credentials
try {
// Check if account exists and has password
const accountCheck = await myTischtennisService.getAccount(userId);
if (!accountCheck) {
throw new Error('MyTischtennis-Account nicht gefunden');
}
if (!accountCheck.encryptedPassword) {
throw new Error('Kein Passwort gespeichert. Bitte melden Sie sich in den MyTischtennis-Einstellungen an und speichern Sie Ihr Passwort.');
}
await myTischtennisService.verifyLogin(userId);
session = await myTischtennisService.getSession(userId);
} catch (loginError) {
const errorMessage = loginError.message || 'Automatischer Login fehlgeschlagen';
throw new HttpError(`MyTischtennis-Session abgelaufen und automatischer Login fehlgeschlagen: ${errorMessage}. Bitte melden Sie sich in den MyTischtennis-Einstellungen an.`, 401);
}
}
// Get account data (for clubId, etc.)
account = await myTischtennisService.getAccount(userId);
if (!account) {
throw new HttpError('MyTischtennis-Account nicht verknüpft. Bitte verknüpfen Sie Ihren Account in den MyTischtennis-Einstellungen.', 404);
}
// Get team with league and season
team = await ClubTeam.findByPk(clubTeamId, {
include: [
{
model: League,
as: 'league',
include: [
{
model: Season,
as: 'season'
}
]
}
]
});
if (!team) {
throw new HttpError(`Team mit ID ${clubTeamId} nicht gefunden`, 404);
}
// Verbesserte Validierung mit detaillierten Fehlermeldungen
if (!team.myTischtennisTeamId) {
throw new HttpError(`Team "${team.name}" (interne ID: ${team.id}) ist nicht für myTischtennis konfiguriert: myTischtennisTeamId fehlt. Bitte konfigurieren Sie das Team zuerst über die MyTischtennis-URL.`, 400);
}
if (!team.league) {
throw new HttpError('Team ist keiner Liga zugeordnet. Bitte ordnen Sie das Team einer Liga zu.', 400);
}
if (!team.league.myTischtennisGroupId) {
throw new HttpError('Liga ist nicht für myTischtennis konfiguriert: myTischtennisGroupId fehlt. Bitte konfigurieren Sie die Liga zuerst über die MyTischtennis-URL.', 400);
}
// Validate season before proceeding
if (!team.league.season || !team.league.season.season) {
throw new HttpError('Liga ist keiner Saison zugeordnet. Bitte ordnen Sie die Liga einer Saison zu.', 400);
}
// Build the URL that will be used - do this early so we can log it even if errors occur
const seasonFull = team.league.season.season;
const seasonParts = seasonFull.split('/');
const seasonShort = seasonParts.length === 2
? `${seasonParts[0].slice(-2)}/${seasonParts[1].slice(-2)}`
: seasonFull;
const seasonStr = seasonShort.replace('/', '--');
const teamnameEncoded = encodeURIComponent(team.name.replace(/\s/g, '_'));
myTischtennisUrl = `https://www.mytischtennis.de/click-tt/${team.league.association}/${seasonStr}/ligen/${team.league.groupname}/gruppe/${team.league.myTischtennisGroupId}/mannschaft/${team.myTischtennisTeamId}/${teamnameEncoded}/spielerbilanzen/gesamt`;
// Log the request to myTischtennis BEFORE making the call
// This ensures we always see what WILL BE sent, even if the call fails
requestStartTime = Date.now();
try {
await apiLogService.logRequest({
userId: account.userId,
method: 'GET',
path: myTischtennisUrl.replace('https://www.mytischtennis.de', ''),
statusCode: null,
requestBody: JSON.stringify({
url: myTischtennisUrl,
myTischtennisTeamId: team.myTischtennisTeamId,
clubTeamId: team.id,
teamName: team.name,
leagueName: team.league.name,
association: team.league.association,
groupId: team.league.myTischtennisGroupId,
groupname: team.league.groupname,
season: seasonFull
}),
responseBody: null,
executionTime: null,
errorMessage: 'Request wird ausgeführt...',
logType: 'api_request',
schedulerJobType: 'mytischtennis_fetch'
});
} catch (logError) {
// Silent fail - logging errors shouldn't break the request
}
// Fetch data for this specific team
// Note: fetchTeamResults will also log and update with actual response
const result = await autoFetchMatchResultsService.fetchTeamResults(
{
userId: account.userId,
email: account.email,
cookie: session.cookie,
accessToken: session.accessToken,
expiresAt: session.expiresAt,
getPassword: () => null // Not needed for manual fetch
},
team
);
// Also fetch and update league table data
let tableUpdateResult = null;
try {
await autoFetchMatchResultsService.fetchAndUpdateLeagueTable(account.userId, team.league.id);
tableUpdateResult = 'League table updated successfully';
} catch (error) {
tableUpdateResult = 'League table update failed: ' + error.message;
// Don't fail the entire request if table update fails
}
// Additionally update (Q)TTR ratings for the club
let ratingsUpdate = null;
try {
// Use already resolved userId instead of authcode to avoid header dependency
const ratingsResult = await MemberService.updateRatingsFromMyTischtennisByUserId(userId, team.clubId);
ratingsUpdate = ratingsResult?.response?.message || `Ratings update status: ${ratingsResult?.status}`;
} catch (ratingsErr) {
ratingsUpdate = 'Ratings update failed: ' + (ratingsErr.message || String(ratingsErr));
}
res.json({
success: true,
message: `${result.fetchedCount} Datensätze abgerufen und verarbeitet`,
data: {
fetchedCount: result.fetchedCount,
teamName: team.name,
tableUpdate: tableUpdateResult,
ratingsUpdate
}
});
} catch (error) {
// Update log with error information if we got far enough to build the URL
if (myTischtennisUrl && account && team) {
const requestExecutionTime = requestStartTime ? (Date.now() - requestStartTime) : null;
try {
await apiLogService.logRequest({
userId: account.userId,
method: 'GET',
path: myTischtennisUrl.replace('https://www.mytischtennis.de', ''),
statusCode: 0,
requestBody: JSON.stringify({
url: myTischtennisUrl,
myTischtennisTeamId: team.myTischtennisTeamId,
clubTeamId: team.id,
teamName: team.name,
leagueName: team.league?.name,
association: team.league?.association,
groupname: team.league?.groupname,
groupId: team.league?.myTischtennisGroupId
}),
responseBody: null,
executionTime: requestExecutionTime,
errorMessage: error.message || String(error),
logType: 'api_request',
schedulerJobType: 'mytischtennis_fetch'
});
} catch (logError) {
// Silent fail - logging errors shouldn't break the request
}
}
// Normalize HTTP status code (guard against strings)
const rawCode = error && (error.statusCode != null ? error.statusCode : error.status);
const parsed = Number(rawCode);
const status = Number.isInteger(parsed) && parsed >= 100 && parsed <= 599 ? parsed : 500;
const debug = {
message: error.message || String(error),
name: error.name,
stack: (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development') ? (error.stack || null) : undefined,
team: team ? { id: team.id, name: team.name } : null,
league: team && team.league ? { id: team.league.id, name: team.league.name, association: team.league.association, groupId: team.league.myTischtennisGroupId, groupname: team.league.groupname } : null,
url: typeof myTischtennisUrl !== 'undefined' ? myTischtennisUrl : null
};
try {
if (!res.headersSent) {
// Spezieller Fall: myTischtennis-Reauth nötig → nicht 401 an FE senden, um App-Logout zu vermeiden
const isMyTischtennisAuthIssue = status === 401 && /MyTischtennis-Session abgelaufen|Automatischer Login fehlgeschlagen|Passwort gespeichert/i.test(debug.message || '');
if (isMyTischtennisAuthIssue) {
return res.status(200).json({ success: false, error: debug.message, debug, needsMyTischtennisReauth: true });
}
res.status(status).json({ success: false, error: debug.message, debug });
}
} catch (writeErr) {
// Fallback, falls Headers schon gesendet wurden
// eslint-disable-next-line no-console
console.error('[fetchTeamData] Response write failed:', writeErr);
}
}
}
/**
* Get myTischtennis URL for a team
* GET /api/mytischtennis/team-url/:teamId
*/
async getTeamUrl(req, res, next) {
try {
const { teamId } = req.params;
const team = await ClubTeam.findByPk(teamId, {
include: [
{
model: League,
as: 'league',
include: [
{
model: Season,
as: 'season'
}
]
}
]
});
if (!team) {
throw new HttpError('Team not found', 404);
}
if (!team.myTischtennisTeamId || !team.league || !team.league.myTischtennisGroupId) {
throw new HttpError('Team is not configured for myTischtennis', 400);
}
const url = myTischtennisUrlParserService.buildUrl({
association: team.league.association,
season: team.league.season?.season,
groupname: team.league.groupname,
groupId: team.league.myTischtennisGroupId,
teamId: team.myTischtennisTeamId,
teamname: team.name
});
res.json({
success: true,
url
});
} catch (error) {
next(error);
}
}
/**
* Configure league from myTischtennis table URL
* POST /api/mytischtennis/configure-league
* Body: { url: string, createSeason?: boolean }
*/
async configureLeague(req, res, next) {
try {
const { url, createSeason } = req.body;
const userIdOrEmail = req.headers.userid;
if (!url) {
throw new HttpError('URL is required', 400);
}
// Parse URL
const parsedData = myTischtennisUrlParserService.parseUrl(url);
if (parsedData.urlType !== 'table') {
throw new HttpError('URL must be a table URL (not a team URL)', 400);
}
// Find or create season
let season = await Season.findOne({
where: { season: parsedData.season }
});
if (!season && createSeason) {
season = await Season.create({
season: parsedData.season,
startDate: new Date(),
endDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 Jahr später
});
}
// Find or create league
let league = await League.findOne({
where: {
myTischtennisGroupId: parsedData.groupId,
association: parsedData.association
}
});
if (!league) {
league = await League.create({
name: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version
myTischtennisGroupId: parsedData.groupId,
association: parsedData.association,
groupname: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version
seasonId: season?.id || null
});
} else {
// Update existing league - aber nur wenn es sich wirklich geändert hat
if (league.name !== parsedData.groupnameOriginal) {
league.name = parsedData.groupnameOriginal;
league.groupname = parsedData.groupnameOriginal;
}
league.seasonId = season?.id || league.seasonId;
await league.save();
}
res.json({
success: true,
message: 'League configured successfully',
data: {
league: {
id: league.id,
name: league.name,
myTischtennisGroupId: league.myTischtennisGroupId,
association: league.association,
groupname: league.groupname
},
season: season ? {
id: season.id,
name: season.season
} : null,
parsedData
}
});
} catch (error) {
next(error);
}
}
}
export default new MyTischtennisUrlController();

View File

@@ -4,7 +4,10 @@ import { devLog } from '../utils/logger.js';
export const getParticipants = async (req, res) => {
try {
const { dateId } = req.params;
const participants = await Participant.findAll({ where: { diaryDateId: dateId } });
const participants = await Participant.findAll({
where: { diaryDateId: dateId },
attributes: ['id', 'diaryDateId', 'memberId', 'groupId', 'notes', 'createdAt', 'updatedAt']
});
res.status(200).json(participants);
} catch (error) {
devLog(error);
@@ -12,6 +15,32 @@ export const getParticipants = async (req, res) => {
}
};
export const updateParticipantGroup = async (req, res) => {
try {
const { dateId, memberId } = req.params;
const { groupId } = req.body;
const participant = await Participant.findOne({
where: {
diaryDateId: dateId,
memberId: memberId
}
});
if (!participant) {
return res.status(404).json({ error: 'Teilnehmer nicht gefunden' });
}
participant.groupId = groupId || null;
await participant.save();
res.status(200).json(participant);
} catch (error) {
devLog(error);
res.status(500).json({ error: 'Fehler beim Aktualisieren der Teilnehmer-Gruppenzuordnung' });
}
};
export const addParticipant = async (req, res) => {
try {
const { diaryDateId, memberId } = req.body;

View File

@@ -0,0 +1,167 @@
import permissionService from '../services/permissionService.js';
/**
* Get user's permissions for a club
*/
export const getUserPermissions = async (req, res) => {
try {
const { clubId } = req.params;
const userId = req.user.id;
// Validierung: clubId muss eine gültige Zahl sein
const parsedClubId = parseInt(clubId, 10);
if (isNaN(parsedClubId) || parsedClubId <= 0) {
return res.status(400).json({ error: 'Ungültige Club-ID' });
}
const permissions = await permissionService.getUserClubPermissions(userId, parsedClubId);
if (!permissions) {
return res.status(404).json({ error: 'Keine Berechtigungen gefunden' });
}
res.json(permissions);
} catch (error) {
console.error('Error getting user permissions:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Berechtigungen' });
}
};
/**
* Get all club members with their permissions
*/
export const getClubMembersWithPermissions = async (req, res) => {
try {
const { clubId } = req.params;
const userId = req.user.id;
const members = await permissionService.getClubMembersWithPermissions(
parseInt(clubId),
userId
);
res.json(members);
} catch (error) {
console.error('Error getting club members with permissions:', error);
if (error.message === 'Keine Berechtigung zum Anzeigen von Berechtigungen') {
return res.status(403).json({ error: error.message });
}
res.status(500).json({ error: 'Fehler beim Abrufen der Mitglieder' });
}
};
/**
* Update user role
*/
export const updateUserRole = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { role } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setUserRole(
parseInt(targetUserId),
parseInt(clubId),
role,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user role:', error);
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
return res.status(403).json({ error: error.message });
}
res.status(400).json({ error: error.message });
}
};
/**
* Update user custom permissions
*/
export const updateUserPermissions = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { permissions } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setCustomPermissions(
parseInt(targetUserId),
parseInt(clubId),
permissions,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user permissions:', error);
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
return res.status(403).json({ error: error.message });
}
res.status(400).json({ error: error.message });
}
};
/**
* Get available roles
*/
export const getAvailableRoles = async (req, res) => {
try {
const roles = permissionService.getAvailableRoles();
res.json(roles);
} catch (error) {
console.error('Error getting available roles:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Rollen' });
}
};
/**
* Get permission structure
*/
export const getPermissionStructure = async (req, res) => {
try {
const structure = permissionService.getPermissionStructure();
res.json(structure);
} catch (error) {
console.error('Error getting permission structure:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Berechtigungsstruktur' });
}
};
/**
* Update user status (activate/deactivate)
*/
export const updateUserStatus = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { approved } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setUserStatus(
parseInt(targetUserId),
parseInt(clubId),
approved,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user status:', error);
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
return res.status(403).json({ error: error.message });
}
res.status(400).json({ error: error.message });
}
};
export default {
getUserPermissions,
getClubMembersWithPermissions,
updateUserRole,
updateUserPermissions,
updateUserStatus,
getAvailableRoles,
getPermissionStructure
};

View File

@@ -1,5 +1,6 @@
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import TeamDocumentService from '../services/teamDocumentService.js';
import PDFParserService from '../services/pdfParserService.js';
import { getUserByToken } from '../utils/userUtils.js';
@@ -8,6 +9,11 @@ import { devLog } from '../utils/logger.js';
// Multer-Konfiguration für Datei-Uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
try {
fs.mkdirSync('uploads/temp', { recursive: true });
} catch (mkdirError) {
console.error('[multer] - Failed to ensure temp upload directory exists:', mkdirError);
}
cb(null, 'uploads/temp/');
},
filename: (req, file, cb) => {
@@ -23,15 +29,17 @@ const upload = multer({
},
fileFilter: (req, file, cb) => {
// Erlaube nur PDF, DOC, DOCX, TXT, CSV Dateien
const allowedTypes = /pdf|doc|docx|txt|csv/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
const allowedExtensions = ['.pdf', '.doc', '.docx', '.txt', '.csv'];
const allowedMimePatterns = ['pdf', 'msword', 'wordprocessingml.document', 'text/plain', 'csv', 'excel'];
if (mimetype && extname) {
const extensionValid = allowedExtensions.includes(path.extname(file.originalname).toLowerCase());
const mimetypeValid = allowedMimePatterns.some((pattern) => file.mimetype && file.mimetype.toLowerCase().includes(pattern));
if (extensionValid && mimetypeValid) {
return cb(null, true);
} else {
cb(new Error('Nur PDF, DOC, DOCX, TXT und CSV Dateien sind erlaubt!'));
}
cb(new Error('Nur PDF, DOC, DOCX, TXT und CSV Dateien sind erlaubt!'));
}
});