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:
85
backend/controllers/apiLogController.js
Normal file
85
backend/controllers/apiLogController.js
Normal 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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
}
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
232
backend/controllers/memberActivityController.js
Normal file
232
backend/controllers/memberActivityController.js
Normal 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' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
51
backend/controllers/memberTransferConfigController.js
Normal file
51
backend/controllers/memberTransferConfigController.js
Normal 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'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
601
backend/controllers/myTischtennisUrlController.js
Normal file
601
backend/controllers/myTischtennisUrlController.js
Normal 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();
|
||||
@@ -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;
|
||||
|
||||
167
backend/controllers/permissionController.js
Normal file
167
backend/controllers/permissionController.js
Normal 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
|
||||
};
|
||||
|
||||
|
||||
@@ -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!'));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user