6 Commits

Author SHA1 Message Date
Torsten Schulz (local)
dbede48d4f Entfernt Konsolenausgaben aus der MyTischtennisClient-Klasse, um die Codequalität zu verbessern und die Lesbarkeit zu erhöhen. Diese Änderungen betreffen die Methoden getUserProfile und getClubRankings und tragen zur Optimierung der Protokollierung und Performance bei. 2025-10-02 10:40:24 +02:00
Torsten Schulz (local)
6cd3c3a020 Entfernt Konsolenausgaben aus verschiedenen Controllern und Services, um die Codequalität zu verbessern und die Lesbarkeit zu erhöhen. Diese Änderungen betreffen die Controller für Clubs, Club-Teams, Mitglieder, Tagebuch-Tags, Saisons und Teams sowie die zugehörigen Services. Ziel ist es, die Protokollierung zu optimieren und die Performance zu steigern. 2025-10-02 10:34:56 +02:00
Torsten Schulz (local)
7ecbef806d Entfernt Konsolenausgaben aus mehreren Komponenten, um den Code zu bereinigen und die Lesbarkeit zu verbessern. Betroffene Dateien sind CourtDrawingRender.vue, CourtDrawingTool.vue, SeasonSelector.vue, PredefinedActivities.vue, ScheduleView.vue, TeamManagementView.vue und TournamentsView.vue. Diese Änderungen tragen zur Optimierung der Performance und zur Reduzierung von unnötigen Protokollierungen bei. 2025-10-02 10:13:03 +02:00
Torsten Schulz (local)
1c70ca97bb Fügt Unterstützung für Team-Dokumente hinzu. Aktualisiert die Backend-Modelle und -Routen, um Team-Dokumente zu verwalten, einschließlich Upload- und Parsing-Funktionen für Code- und Pin-Listen. Ergänzt die Benutzeroberfläche in TeamManagementView.vue zur Anzeige und Verwaltung von Team-Dokumenten sowie zur Integration von PDF-Parsing. Aktualisiert die Match-Modelle, um zusätzliche Felder für Spiel-Codes und PINs zu berücksichtigen. 2025-10-02 09:04:19 +02:00
Torsten Schulz (local)
a6493990d3 Erweitert die Backend- und Frontend-Funktionalität zur Unterstützung von Teams und Saisons. Fügt neue Routen für Team- und Club-Team-Management hinzu, aktualisiert die Match- und Team-Modelle zur Berücksichtigung von Saisons, und implementiert die Saison-Auswahl in der Benutzeroberfläche. Optimiert die Logik zur Abfrage von Ligen und Spielen basierend auf der ausgewählten Saison. 2025-10-01 22:47:13 +02:00
Torsten Schulz (local)
f8f4d23c4e Aktualisiert die Schaltflächen im MyTischtennisAccount.vue, um die Benutzeroberfläche zu verbessern. Ändert den Text der Schaltfläche "Verbindung testen" in "Erneut einloggen" und entfernt die Testausgabe für Login-Tests. Optimiert die Erfolgsmeldung nach erfolgreichem Login und aktualisiert die Account-Daten. Entfernt die nicht mehr benötigte Funktionalität für den Login-Flow-Test. 2025-10-01 13:52:14 +02:00
50 changed files with 4134 additions and 470 deletions

View File

@@ -149,13 +149,11 @@ class MyTischtennisClient {
* @returns {Promise<Object>} User profile with club info
*/
async getUserProfile(cookie) {
console.log('[getUserProfile] - Calling /?_data=root with cookie:', cookie?.substring(0, 50) + '...');
const result = await this.authenticatedRequest('/?_data=root', cookie, {
method: 'GET'
});
console.log('[getUserProfile] - Result success:', result.success);
if (result.success) {
console.log('[getUserProfile] - Response structure:', {
@@ -169,8 +167,6 @@ class MyTischtennisClient {
qttr: result.data?.userProfile?.qttr
});
console.log('[getUserProfile] - Full userProfile.club:', result.data?.userProfile?.club);
console.log('[getUserProfile] - Full userProfile.organization:', result.data?.userProfile?.organization);
return {
success: true,
@@ -199,12 +195,10 @@ class MyTischtennisClient {
let currentPage = 0;
let hasMorePages = true;
console.log('[getClubRankings] - Starting to fetch rankings for club', clubId);
while (hasMorePages) {
const endpoint = `/rankings/andro-rangliste?all-players=on&clubnr=${clubId}&fednickname=${fedNickname}&results-per-page=100&page=${currentPage}&_data=routes%2F%24`;
console.log(`[getClubRankings] - Fetching page ${currentPage}...`);
const result = await this.authenticatedRequest(endpoint, cookie, {
method: 'GET'
@@ -245,7 +239,6 @@ class MyTischtennisClient {
};
}
console.log(`[getClubRankings] - Page ${currentPage}: Found ${entries.length} entries`);
// Füge Entries hinzu
allEntries.push(...entries);
@@ -255,19 +248,15 @@ class MyTischtennisClient {
// Oder wenn wir alle erwarteten Einträge haben
if (entries.length === 0) {
hasMorePages = false;
console.log('[getClubRankings] - No more entries, stopping');
} else if (rankingData.numberOfPages && currentPage >= rankingData.numberOfPages - 1) {
hasMorePages = false;
console.log(`[getClubRankings] - Reached last page (${rankingData.numberOfPages})`);
} else if (allEntries.length >= rankingData.resultLength) {
hasMorePages = false;
console.log(`[getClubRankings] - Got all entries (${allEntries.length}/${rankingData.resultLength})`);
} else {
currentPage++;
}
}
console.log(`[getClubRankings] - Total entries fetched: ${allEntries.length}`);
return {
success: true,

View File

@@ -0,0 +1,128 @@
import ClubTeamService from '../services/clubTeamService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getClubTeams = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
// Check if user has access to this club
const clubTeams = await ClubTeamService.getAllClubTeamsByClub(clubId, seasonId);
res.status(200).json(clubTeams);
} catch (error) {
console.error('[getClubTeams] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const clubTeam = await ClubTeamService.getClubTeamById(clubTeamId);
if (!clubTeam) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(clubTeam);
} catch (error) {
console.error('[getClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
if (!name) {
return res.status(400).json({ error: "missingname" });
}
const clubTeamData = {
name,
clubId: parseInt(clubId),
leagueId: leagueId ? parseInt(leagueId) : null,
seasonId: seasonId ? parseInt(seasonId) : null
};
const newClubTeam = await ClubTeamService.createClubTeam(clubTeamData);
res.status(201).json(newClubTeam);
} catch (error) {
console.error('[createClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const updateClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
const updateData = {};
if (name !== undefined) updateData.name = name;
if (leagueId !== undefined) updateData.leagueId = leagueId ? parseInt(leagueId) : null;
if (seasonId !== undefined) updateData.seasonId = seasonId ? parseInt(seasonId) : null;
const success = await ClubTeamService.updateClubTeam(clubTeamId, updateData);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
const updatedClubTeam = await ClubTeamService.getClubTeamById(clubTeamId);
res.status(200).json(updatedClubTeam);
} catch (error) {
console.error('[updateClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const success = await ClubTeamService.deleteClubTeam(clubTeamId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "Club team deleted successfully" });
} catch (error) {
console.error('[deleteClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getLeagues = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
const leagues = await ClubTeamService.getLeaguesByClub(clubId, seasonId);
res.status(200).json(leagues);
} catch (error) {
console.error('[getLeagues] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -4,11 +4,8 @@ import { devLog } from '../utils/logger.js';
export const getClubs = async (req, res) => {
try {
devLog('[getClubs] - get clubs');
const clubs = await ClubService.getAllClubs();
devLog('[getClubs] - prepare response');
res.status(200).json(clubs);
devLog('[getClubs] - done');
} catch (error) {
console.error('[getClubs] - error:', error);
res.status(500).json({ error: "internalerror" });
@@ -16,28 +13,20 @@ export const getClubs = async (req, res) => {
};
export const addClub = async (req, res) => {
devLog('[addClub] - Read out parameters');
const { authcode: token } = req.headers;
const { name: clubName } = req.body;
try {
devLog('[addClub] - find club by name');
const club = await ClubService.findClubByName(clubName);
devLog('[addClub] - get user');
const user = await getUserByToken(token);
devLog('[addClub] - check if club already exists');
if (club) {
res.status(409).json({ error: "alreadyexists" });
return;
}
devLog('[addClub] - create club');
const newClub = await ClubService.createClub(clubName);
devLog('[addClub] - add user to new club');
await ClubService.addUserToClub(user.id, newClub.id);
devLog('[addClub] - prepare response');
res.status(200).json(newClub);
devLog('[addClub] - done');
} catch (error) {
console.error('[addClub] - error:', error);
res.status(500).json({ error: "internalerror" });
@@ -45,30 +34,22 @@ export const addClub = async (req, res) => {
};
export const getClub = async (req, res) => {
devLog('[getClub] - start');
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
devLog('[getClub] - get user');
const user = await getUserByToken(token);
devLog('[getClub] - get users club');
const access = await ClubService.getUserClubAccess(user.id, clubId);
devLog('[getClub] - check access');
if (access.length === 0 || !access[0].approved) {
res.status(403).json({ error: "noaccess", status: access.length === 0 ? "notrequested" : "requested" });
return;
}
devLog('[getClub] - get club');
const club = await ClubService.findClubById(clubId);
devLog('[getClub] - check club exists');
if (!club) {
return res.status(404).json({ message: 'Club not found' });
}
devLog('[getClub] - set response');
res.status(200).json(club);
devLog('[getClub] - done');
} catch (error) {
console.error('[getClub] - error:', error);
res.status(500).json({ message: 'Server error' });
@@ -81,7 +62,6 @@ export const requestClubAccess = async (req, res) => {
try {
const user = await getUserByToken(token);
devLog('[requestClubAccess] - user:', user);
await ClubService.requestAccessToClub(user.id, clubId);
res.status(200).json({});

View File

@@ -17,7 +17,6 @@ export const createTag = async (req, res) => {
const newTag = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } });
res.status(201).json(newTag);
} catch (error) {
devLog('[createTag] - Error:', error);
res.status(500).json({ error: 'Error creating tag' });
}
};

View File

@@ -25,7 +25,8 @@ export const getLeaguesForCurrentSeason = async (req, res) => {
devLog(req.headers, req.params);
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId);
const { seasonid: seasonId } = req.query;
const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId, seasonId);
return res.status(200).json(leagues);
} catch (error) {
console.error('Error retrieving leagues:', error);
@@ -37,7 +38,8 @@ export const getMatchesForLeagues = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const matches = await MatchService.getMatchesForLeagues(userToken, clubId);
const { seasonid: seasonId } = req.query;
const matches = await MatchService.getMatchesForLeagues(userToken, clubId, seasonId);
return res.status(200).json(matches);
} catch (error) {
console.error('Error retrieving matches:', error);

View File

@@ -10,24 +10,17 @@ const getClubMembers = async(req, res) => {
}
res.status(200).json(await MemberService.getClubMembers(userToken, clubId, showAll));
} catch(error) {
devLog('[getClubMembers] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
}
const getWaitingApprovals = async(req, res) => {
try {
devLog('[getWaitingApprovals] - Start');
const { id: clubId } = req.params;
devLog('[getWaitingApprovals] - get token');
const { authcode: userToken } = req.headers;
devLog('[getWaitingApprovals] - load for waiting approvals');
const waitingApprovals = await MemberService.getApprovalRequests(userToken, clubId);
devLog('[getWaitingApprovals] - set response');
res.status(200).json(waitingApprovals);
devLog('[getWaitingApprovals] - done');
} catch(error) {
devLog('[getWaitingApprovals] - Error: ', error);
res.status(403).json({ error: error });
}
}
@@ -60,7 +53,6 @@ const uploadMemberImage = async (req, res) => {
};
const getMemberImage = async (req, res) => {
devLog('[getMemberImage]');
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
@@ -77,7 +69,6 @@ const getMemberImage = async (req, res) => {
};
const updateRatingsFromMyTischtennis = async (req, res) => {
devLog('[updateRatingsFromMyTischtennis]');
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;

View File

@@ -6,11 +6,9 @@ const getMemberNotes = async (req, res) => {
const { authcode: userToken } = req.headers;
const { memberId } = req.params;
const { clubId } = req.query;
devLog('[getMemberNotes]', userToken, memberId, clubId);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
devLog('[getMemberNotes] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -19,12 +17,10 @@ const addMemberNote = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { memberId, content, clubId } = req.body;
devLog('[addMemberNote]', userToken, memberId, content, clubId);
await MemberNoteService.addNoteToMember(userToken, clubId, memberId, content);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(201).json(notes);
} catch (error) {
devLog('[addMemberNote] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -34,13 +30,11 @@ const deleteMemberNote = async (req, res) => {
const { authcode: userToken } = req.headers;
const { noteId } = req.params;
const { clubId } = req.body;
devLog('[deleteMemberNote]', userToken, noteId, clubId);
const memberId = await MemberNoteService.getMemberIdForNote(noteId); // Member ID ermitteln
await MemberNoteService.deleteNoteForMember(userToken, clubId, noteId);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
devLog('[deleteMemberNote] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};

View File

@@ -36,7 +36,6 @@ export const uploadPredefinedActivityImage = async (req, res) => {
// Extrahiere Zeichnungsdaten aus dem Request
const drawingData = req.body.drawingData ? JSON.parse(req.body.drawingData) : null;
devLog('[uploadPredefinedActivityImage] - drawingData:', drawingData);
const imageRecord = await PredefinedActivityImage.create({
predefinedActivityId: id,

View File

@@ -0,0 +1,103 @@
import SeasonService from '../services/seasonService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getSeasons = async (req, res) => {
try {
const { authcode: token } = req.headers;
const user = await getUserByToken(token);
const seasons = await SeasonService.getAllSeasons();
res.status(200).json(seasons);
} catch (error) {
console.error('[getSeasons] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getCurrentSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const user = await getUserByToken(token);
const season = await SeasonService.getOrCreateCurrentSeason();
res.status(200).json(season);
} catch (error) {
console.error('[getCurrentSeason] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { season } = req.body;
const user = await getUserByToken(token);
if (!season) {
return res.status(400).json({ error: "missingseason" });
}
// Validiere Saison-Format (z.B. "2023/2024")
const seasonRegex = /^\d{4}\/\d{4}$/;
if (!seasonRegex.test(season)) {
return res.status(400).json({ error: "invalidseasonformat" });
}
const newSeason = await SeasonService.createSeason(season);
res.status(201).json(newSeason);
} catch (error) {
console.error('[createSeason] - Error:', error);
if (error.message === 'Season already exists') {
res.status(409).json({ error: "alreadyexists" });
} else {
res.status(500).json({ error: "internalerror" });
}
}
};
export const getSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { seasonid: seasonId } = req.params;
const user = await getUserByToken(token);
const season = await SeasonService.getSeasonById(seasonId);
if (!season) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(season);
} catch (error) {
console.error('[getSeason] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { seasonid: seasonId } = req.params;
const user = await getUserByToken(token);
const success = await SeasonService.deleteSeason(seasonId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "deleted" });
} catch (error) {
console.error('[deleteSeason] - Error:', error);
if (error.message === 'Season is used by teams' || error.message === 'Season is used by leagues') {
res.status(409).json({ error: "seasoninuse" });
} else {
res.status(500).json({ error: "internalerror" });
}
}
};

View File

@@ -0,0 +1,130 @@
import TeamService from '../services/teamService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getTeams = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
// Check if user has access to this club
const teams = await TeamService.getAllTeamsByClub(clubId, seasonId);
res.status(200).json(teams);
} catch (error) {
console.error('[getTeams] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const user = await getUserByToken(token);
const team = await TeamService.getTeamById(teamId);
if (!team) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(team);
} catch (error) {
console.error('[getTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
if (!name) {
return res.status(400).json({ error: "missingname" });
}
const teamData = {
name,
clubId: parseInt(clubId),
leagueId: leagueId ? parseInt(leagueId) : null,
seasonId: seasonId ? parseInt(seasonId) : null
};
const newTeam = await TeamService.createTeam(teamData);
res.status(201).json(newTeam);
} catch (error) {
console.error('[createTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const updateTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
const updateData = {};
if (name !== undefined) updateData.name = name;
if (leagueId !== undefined) updateData.leagueId = leagueId ? parseInt(leagueId) : null;
if (seasonId !== undefined) updateData.seasonId = seasonId ? parseInt(seasonId) : null;
const success = await TeamService.updateTeam(teamId, updateData);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
const updatedTeam = await TeamService.getTeamById(teamId);
res.status(200).json(updatedTeam);
} catch (error) {
console.error('[updateTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const user = await getUserByToken(token);
const success = await TeamService.deleteTeam(teamId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "deleted" });
} catch (error) {
console.error('[deleteTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getLeagues = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
const leagues = await TeamService.getLeaguesByClub(clubId, seasonId);
res.status(200).json(leagues);
} catch (error) {
console.error('[getLeagues] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -0,0 +1,215 @@
import multer from 'multer';
import path from 'path';
import TeamDocumentService from '../services/teamDocumentService.js';
import PDFParserService from '../services/pdfParserService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
// Multer-Konfiguration für Datei-Uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/temp/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024 // 10MB Limit
},
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);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('Nur PDF, DOC, DOCX, TXT und CSV Dateien sind erlaubt!'));
}
}
});
export const uploadMiddleware = upload.single('document');
export const uploadDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const { documentType } = req.body;
const user = await getUserByToken(token);
if (!req.file) {
return res.status(400).json({ error: "nofile" });
}
if (!documentType || !['code_list', 'pin_list'].includes(documentType)) {
return res.status(400).json({ error: "invaliddocumenttype" });
}
const document = await TeamDocumentService.uploadDocument(req.file, clubTeamId, documentType);
res.status(201).json(document);
} catch (error) {
console.error('[uploadDocument] - Error:', error);
// Lösche temporäre Datei bei Fehler
if (req.file && req.file.path) {
try {
const fs = await import('fs');
fs.unlinkSync(req.file.path);
} catch (cleanupError) {
console.error('Fehler beim Löschen der temporären Datei:', cleanupError);
}
}
if (error.message === 'Club-Team nicht gefunden') {
return res.status(404).json({ error: "clubteamnotfound" });
}
res.status(500).json({ error: "internalerror" });
}
};
export const getDocuments = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const documents = await TeamDocumentService.getDocumentsByClubTeam(clubTeamId);
res.status(200).json(documents);
} catch (error) {
console.error('[getDocuments] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(document);
} catch (error) {
console.error('[getDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const downloadDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "notfound" });
}
const filePath = await TeamDocumentService.getDocumentPath(documentId);
if (!filePath) {
return res.status(404).json({ error: "filenotfound" });
}
// Prüfe ob Datei existiert
const fs = await import('fs');
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: "filenotfound" });
}
// Setze Headers für Inline-Anzeige (PDF-Viewer)
res.setHeader('Content-Disposition', `inline; filename="${document.originalFileName}"`);
res.setHeader('Content-Type', document.mimeType);
// Sende die Datei
res.sendFile(filePath);
} catch (error) {
console.error('[downloadDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const success = await TeamDocumentService.deleteDocument(documentId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "Document deleted successfully" });
} catch (error) {
console.error('[deleteDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const parsePDF = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const { leagueid: leagueId } = req.query;
const user = await getUserByToken(token);
if (!leagueId) {
return res.status(400).json({ error: "missingleagueid" });
}
// Hole Dokument-Informationen
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "documentnotfound" });
}
// Prüfe ob es eine PDF- oder TXT-Datei ist
if (!document.mimeType.includes('pdf') && !document.mimeType.includes('text/plain')) {
return res.status(400).json({ error: "notapdfortxt" });
}
// Parse PDF
const parseResult = await PDFParserService.parsePDF(document.filePath, document.clubTeam.clubId);
// Speichere Matches in Datenbank
const saveResult = await PDFParserService.saveMatchesToDatabase(parseResult.matches, parseInt(leagueId));
res.status(200).json({
parseResult: {
matchesFound: parseResult.matches.length,
debugInfo: parseResult.debugInfo,
allLines: parseResult.allLines,
rawText: parseResult.rawText
},
saveResult: {
created: saveResult.created,
updated: saveResult.updated,
errors: saveResult.errors
}
});
} catch (error) {
console.error('[parsePDF] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -0,0 +1,44 @@
-- Migration: Add season_id to teams table
-- First, add the column as nullable
ALTER TABLE `team` ADD COLUMN `season_id` INT NULL;
-- Get or create current season
SET @current_season_id = (
SELECT id FROM `season`
WHERE season = (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
)
LIMIT 1
);
-- If no season exists, create it
INSERT IGNORE INTO `season` (season) VALUES (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
);
-- Get the season ID again (in case we just created it)
SET @current_season_id = (
SELECT id FROM `season`
WHERE season = (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
)
LIMIT 1
);
-- Update all existing teams to use the current season
UPDATE `team` SET `season_id` = @current_season_id WHERE `season_id` IS NULL;
-- Now make the column NOT NULL and add the foreign key constraint
ALTER TABLE `team` MODIFY COLUMN `season_id` INT NOT NULL;
ALTER TABLE `team` ADD CONSTRAINT `team_season_id_foreign_idx`
FOREIGN KEY (`season_id`) REFERENCES `season` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,54 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Season from './Season.js';
const ClubTeam = sequelize.define('ClubTeam', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
clubId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Club,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
leagueId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: League,
key: 'id',
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
},
seasonId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: Season,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
}, {
underscored: true,
tableName: 'club_team',
timestamps: true,
});
export default ClubTeam;

View File

@@ -3,7 +3,6 @@ import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Team from './Team.js';
import Season from './Season.js';
import Location from './Location.js';
const Match = sequelize.define('Match', {
@@ -21,14 +20,6 @@ const Match = sequelize.define('Match', {
type: DataTypes.TIME,
allowNull: true,
},
seasonId: {
type: DataTypes.INTEGER,
references: {
model: Season,
key: 'id',
},
allowNull: false,
},
locationId: {
type: DataTypes.INTEGER,
references: {
@@ -69,6 +60,21 @@ const Match = sequelize.define('Match', {
},
allowNull: false,
},
code: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Spiel-Code aus PDF-Parsing'
},
homePin: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Pin-Code für Heimteam aus PDF-Parsing'
},
guestPin: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Pin-Code für Gastteam aus PDF-Parsing'
},
}, {
underscored: true,
tableName: 'match',

View File

@@ -1,6 +1,8 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Season from './Season.js';
const Team = sequelize.define('Team', {
id: {
@@ -23,6 +25,26 @@ const Team = sequelize.define('Team', {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
leagueId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: League,
key: 'id',
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
},
seasonId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: Season,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
}, {
underscored: true,
tableName: 'team',

View File

@@ -0,0 +1,52 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import ClubTeam from './ClubTeam.js';
const TeamDocument = sequelize.define('TeamDocument', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
fileName: {
type: DataTypes.STRING,
allowNull: false,
},
originalFileName: {
type: DataTypes.STRING,
allowNull: false,
},
filePath: {
type: DataTypes.STRING,
allowNull: false,
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: false,
},
mimeType: {
type: DataTypes.STRING,
allowNull: false,
},
documentType: {
type: DataTypes.ENUM('code_list', 'pin_list'),
allowNull: false,
},
clubTeamId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: ClubTeam,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
}, {
underscored: true,
tableName: 'team_document',
timestamps: true,
});
export default TeamDocument;

View File

@@ -19,6 +19,8 @@ import DiaryDateActivity from './DiaryDateActivity.js';
import Match from './Match.js';
import League from './League.js';
import Team from './Team.js';
import ClubTeam from './ClubTeam.js';
import TeamDocument from './TeamDocument.js';
import Season from './Season.js';
import Location from './Location.js';
import Group from './Group.js';
@@ -118,8 +120,25 @@ Team.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Club.hasMany(League, { foreignKey: 'clubId', as: 'leagues' });
League.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Match.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
Season.hasMany(Match, { foreignKey: 'seasonId', as: 'matches' });
League.hasMany(Team, { foreignKey: 'leagueId', as: 'teams' });
Team.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
Season.hasMany(Team, { foreignKey: 'seasonId', as: 'teams' });
Team.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
// ClubTeam relationships
Club.hasMany(ClubTeam, { foreignKey: 'clubId', as: 'clubTeams' });
ClubTeam.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
League.hasMany(ClubTeam, { foreignKey: 'leagueId', as: 'clubTeams' });
ClubTeam.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
Season.hasMany(ClubTeam, { foreignKey: 'seasonId', as: 'clubTeams' });
ClubTeam.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
// TeamDocument relationships
ClubTeam.hasMany(TeamDocument, { foreignKey: 'clubTeamId', as: 'documents' });
TeamDocument.belongsTo(ClubTeam, { foreignKey: 'clubTeamId', as: 'clubTeam' });
Match.belongsTo(Location, { foreignKey: 'locationId', as: 'location' });
Location.hasMany(Match, { foreignKey: 'locationId', as: 'matches' });
@@ -231,6 +250,8 @@ export {
Match,
League,
Team,
ClubTeam,
TeamDocument,
Group,
GroupActivity,
Tournament,

View File

@@ -0,0 +1,32 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
getClubTeams,
getClubTeam,
createClubTeam,
updateClubTeam,
deleteClubTeam,
getLeagues
} from '../controllers/clubTeamController.js';
const router = express.Router();
// Get all club teams for a club
router.get('/club/:clubid', authenticate, getClubTeams);
// Create a new club team
router.post('/club/:clubid', authenticate, createClubTeam);
// Get leagues for a club
router.get('/leagues/:clubid', authenticate, getLeagues);
// Get a specific club team
router.get('/:clubteamid', authenticate, getClubTeam);
// Update a club team
router.put('/:clubteamid', authenticate, updateClubTeam);
// Delete a club team
router.delete('/:clubteamid', authenticate, deleteClubTeam);
export default router;

View File

@@ -0,0 +1,28 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
getSeasons,
getCurrentSeason,
createSeason,
getSeason,
deleteSeason
} from '../controllers/seasonController.js';
const router = express.Router();
// Get all seasons
router.get('/', authenticate, getSeasons);
// Get current season (creates if not exists)
router.get('/current', authenticate, getCurrentSeason);
// Get a specific season
router.get('/:seasonid', authenticate, getSeason);
// Create a new season
router.post('/', authenticate, createSeason);
// Delete a season
router.delete('/:seasonid', authenticate, deleteSeason);
export default router;

View File

@@ -0,0 +1,33 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
uploadMiddleware,
uploadDocument,
getDocuments,
getDocument,
downloadDocument,
deleteDocument,
parsePDF
} from '../controllers/teamDocumentController.js';
const router = express.Router();
// Upload eines Dokuments für ein Club-Team
router.post('/club-team/:clubteamid/upload', authenticate, uploadMiddleware, uploadDocument);
// Alle Dokumente für ein Club-Team abrufen
router.get('/club-team/:clubteamid', authenticate, getDocuments);
// Ein spezifisches Dokument abrufen
router.get('/:documentid', authenticate, getDocument);
// Ein Dokument herunterladen
router.get('/:documentid/download', authenticate, downloadDocument);
// Ein Dokument löschen
router.delete('/:documentid', authenticate, deleteDocument);
// PDF parsen und Matches extrahieren
router.post('/:documentid/parse', authenticate, parsePDF);
export default router;

View File

@@ -0,0 +1,32 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
getTeams,
getTeam,
createTeam,
updateTeam,
deleteTeam,
getLeagues
} from '../controllers/teamController.js';
const router = express.Router();
// Get all teams for a club
router.get('/club/:clubid', authenticate, getTeams);
// Get leagues for a club
router.get('/leagues/:clubid', authenticate, getLeagues);
// Get a specific team
router.get('/:teamid', authenticate, getTeam);
// Create a new team
router.post('/club/:clubid', authenticate, createTeam);
// Update a team
router.put('/:teamid', authenticate, updateTeam);
// Delete a team
router.delete('/:teamid', authenticate, deleteTeam);
export default router;

View File

@@ -6,7 +6,7 @@ import cors from 'cors';
import {
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, Group,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, TeamDocument, Group,
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis
} from './models/index.js';
@@ -34,6 +34,10 @@ import accidentRoutes from './routes/accidentRoutes.js';
import trainingStatsRoutes from './routes/trainingStatsRoutes.js';
import officialTournamentRoutes from './routes/officialTournamentRoutes.js';
import myTischtennisRoutes from './routes/myTischtennisRoutes.js';
import teamRoutes from './routes/teamRoutes.js';
import clubTeamRoutes from './routes/clubTeamRoutes.js';
import teamDocumentRoutes from './routes/teamDocumentRoutes.js';
import seasonRoutes from './routes/seasonRoutes.js';
const app = express();
const port = process.env.PORT || 3000;
@@ -79,6 +83,10 @@ app.use('/api/accident', accidentRoutes);
app.use('/api/training-stats', trainingStatsRoutes);
app.use('/api/official-tournaments', officialTournamentRoutes);
app.use('/api/mytischtennis', myTischtennisRoutes);
app.use('/api/teams', teamRoutes);
app.use('/api/club-teams', clubTeamRoutes);
app.use('/api/team-documents', teamDocumentRoutes);
app.use('/api/seasons', seasonRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));

View File

@@ -0,0 +1,180 @@
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
import SeasonService from './seasonService.js';
import { devLog } from '../utils/logger.js';
class ClubTeamService {
/**
* Holt alle ClubTeams für einen Verein, optional gefiltert nach Saison.
* Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison verwendet.
* @param {number} clubId - Die ID des Vereins.
* @param {number|null} seasonId - Optionale Saison-ID.
* @returns {Promise<Array<ClubTeam>>} Eine Liste von ClubTeams.
*/
static async getAllClubTeamsByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const clubTeams = await ClubTeam.findAll({
where: { clubId, seasonId },
order: [['name', 'ASC']]
});
// Manuelle Datenanreicherung für Liga und Saison
const enrichedClubTeams = [];
for (const clubTeam of clubTeams) {
const enrichedTeam = {
id: clubTeam.id,
name: clubTeam.name,
clubId: clubTeam.clubId,
leagueId: clubTeam.leagueId,
seasonId: clubTeam.seasonId,
createdAt: clubTeam.createdAt,
updatedAt: clubTeam.updatedAt,
league: { name: 'Unbekannt' },
season: { season: 'Unbekannt' }
};
// Lade Liga-Daten
if (clubTeam.leagueId) {
const league = await League.findByPk(clubTeam.leagueId, { attributes: ['name'] });
if (league) enrichedTeam.league = league;
}
// Lade Saison-Daten
if (clubTeam.seasonId) {
const season = await Season.findByPk(clubTeam.seasonId, { attributes: ['season'] });
if (season) enrichedTeam.season = season;
}
enrichedClubTeams.push(enrichedTeam);
}
return enrichedClubTeams;
} catch (error) {
console.error('[ClubTeamService.getAllClubTeamsByClub] - Error:', error);
throw error;
}
}
/**
* Holt ein ClubTeam anhand seiner ID
* @param {number} clubTeamId - Die ID des ClubTeams
* @returns {Promise<ClubTeam|null>} Das ClubTeam oder null, wenn nicht gefunden
*/
static async getClubTeamById(clubTeamId) {
try {
const clubTeam = await ClubTeam.findByPk(clubTeamId, {
include: [
{
model: League,
as: 'league',
attributes: ['id', 'name']
},
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
]
});
return clubTeam;
} catch (error) {
console.error('[ClubTeamService.getClubTeamById] - Error:', error);
throw error;
}
}
/**
* Erstellt ein neues ClubTeam.
* Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison zugewiesen.
* @param {object} clubTeamData - Die Daten des neuen ClubTeams (name, clubId, optional leagueId, seasonId).
* @returns {Promise<ClubTeam>} Das erstellte ClubTeam.
*/
static async createClubTeam(clubTeamData) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!clubTeamData.seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
clubTeamData.seasonId = currentSeason.id;
}
const clubTeam = await ClubTeam.create(clubTeamData);
return clubTeam;
} catch (error) {
console.error('[ClubTeamService.createClubTeam] - Error:', error);
throw error;
}
}
/**
* Aktualisiert ein bestehendes ClubTeam.
* @param {number} clubTeamId - Die ID des zu aktualisierenden ClubTeams.
* @param {object} updateData - Die zu aktualisierenden Daten.
* @returns {Promise<boolean>} True, wenn das ClubTeam aktualisiert wurde, sonst false.
*/
static async updateClubTeam(clubTeamId, updateData) {
try {
const [updatedRowsCount] = await ClubTeam.update(updateData, {
where: { id: clubTeamId }
});
return updatedRowsCount > 0;
} catch (error) {
console.error('[ClubTeamService.updateClubTeam] - Error:', error);
throw error;
}
}
/**
* Löscht ein ClubTeam.
* @param {number} clubTeamId - Die ID des zu löschenden ClubTeams.
* @returns {Promise<boolean>} True, wenn das ClubTeam gelöscht wurde, sonst false.
*/
static async deleteClubTeam(clubTeamId) {
try {
const deletedRows = await ClubTeam.destroy({
where: { id: clubTeamId }
});
return deletedRows > 0;
} catch (error) {
console.error('[ClubTeamService.deleteClubTeam] - Error:', error);
throw error;
}
}
/**
* Holt alle Ligen für einen Verein, optional gefiltert nach Saison.
* Wenn keine Saison-ID angegeben ist, wird die aktuelle Saison verwendet.
* @param {number} clubId - Die ID des Vereins.
* @param {number|null} seasonId - Optionale Saison-ID.
* @returns {Promise<Array<League>>} Eine Liste von Ligen.
*/
static async getLeaguesByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const leagues = await League.findAll({
where: { clubId, seasonId },
attributes: ['id', 'name', 'seasonId'],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
console.error('[ClubTeamService.getLeaguesByClub] - Error:', error);
throw error;
}
}
}
export default ClubTeamService;

View File

@@ -10,9 +10,7 @@ import { devLog } from '../utils/logger.js';
class DiaryDateActivityService {
async createActivity(userToken, clubId, data) {
devLog('[DiaryDateActivityService::createActivity] - check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryDateActivityService::createActivity] - add: ', data);
const { activity, ...restData } = data;
// Versuche, die PredefinedActivity robust zu finden:
// 1) per übergebener ID
@@ -60,23 +58,18 @@ class DiaryDateActivityService {
});
const newOrderId = maxOrderId !== null ? maxOrderId + 1 : 1;
restData.orderId = newOrderId;
devLog('[DiaryDateActivityService::createActivity] - create diary date activity');
return await DiaryDateActivity.create(restData);
}
async updateActivity(userToken, clubId, id, data) {
devLog('[DiaryDateActivityService::updateActivity] - check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryDateActivityService::updateActivity] - load activity', id);
const activity = await DiaryDateActivity.findByPk(id);
if (!activity) {
devLog('[DiaryDateActivityService::updateActivity] - activity not found');
throw new Error('Activity not found');
}
// Wenn customActivityName gesendet wird, müssen wir die PredefinedActivity behandeln
if (data.customActivityName) {
devLog('[DiaryDateActivityService::updateActivity] - handling customActivityName:', data.customActivityName);
// Suche nach einer existierenden PredefinedActivity mit diesem Namen
let predefinedActivity = await PredefinedActivity.findOne({
@@ -85,7 +78,6 @@ class DiaryDateActivityService {
if (!predefinedActivity) {
// Erstelle eine neue PredefinedActivity
devLog('[DiaryDateActivityService::updateActivity] - creating new PredefinedActivity');
predefinedActivity = await PredefinedActivity.create({
name: data.customActivityName,
description: data.description || '',
@@ -100,7 +92,6 @@ class DiaryDateActivityService {
delete data.customActivityName;
}
devLog('[DiaryDateActivityService::updateActivity] - update activity', clubId, id, data, JSON.stringify(data));
return await activity.update(data);
}
@@ -114,22 +105,14 @@ class DiaryDateActivityService {
}
async updateActivityOrder(userToken, clubId, id, newOrderId) {
devLog(`[DiaryDateActivityService::updateActivityOrder] - Start update for activity id: ${id}`);
devLog(`[DiaryDateActivityService::updateActivityOrder] - User token: ${userToken}, Club id: ${clubId}, New order id: ${newOrderId}`);
devLog('[DiaryDateActivityService::updateActivityOrder] - Checking user access');
await checkAccess(userToken, clubId);
devLog('[DiaryDateActivityService::updateActivityOrder] - User access confirmed');
devLog(`[DiaryDateActivityService::updateActivityOrder] - Finding activity with id: ${id}`);
const activity = await DiaryDateActivity.findByPk(id);
if (!activity) {
console.error('[DiaryDateActivityService::updateActivityOrder] - Activity not found, throwing error');
throw new Error('Activity not found');
}
devLog('[DiaryDateActivityService::updateActivityOrder] - Activity found:', activity);
const currentOrderId = activity.orderId;
devLog(`[DiaryDateActivityService::updateActivityOrder] - Current order id: ${currentOrderId}`);
if (newOrderId < currentOrderId) {
devLog(`[DiaryDateActivityService::updateActivityOrder] - Shifting items down. Moving activities with orderId between ${newOrderId} and ${currentOrderId - 1}`);
await DiaryDateActivity.increment(
{ orderId: 1 },
{
@@ -139,9 +122,7 @@ class DiaryDateActivityService {
},
}
);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Items shifted down`);
} else if (newOrderId > currentOrderId) {
devLog(`[DiaryDateActivityService::updateActivityOrder] - Shifting items up. Moving activities with orderId between ${currentOrderId + 1} and ${newOrderId}`);
await DiaryDateActivity.decrement(
{ orderId: 1 },
{
@@ -151,16 +132,10 @@ class DiaryDateActivityService {
},
}
);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Items shifted up`);
} else {
devLog('[DiaryDateActivityService::updateActivityOrder] - New order id is the same as the current order id. No shift required.');
}
devLog(`[DiaryDateActivityService::updateActivityOrder] - Setting new order id for activity id: ${id}`);
activity.orderId = newOrderId;
devLog('[DiaryDateActivityService::updateActivityOrder] - Saving activity with new order id');
const savedActivity = await activity.save();
devLog('[DiaryDateActivityService::updateActivityOrder] - Activity saved:', savedActivity);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Finished update for activity id: ${id}`);
return savedActivity;
}
@@ -257,9 +232,7 @@ class DiaryDateActivityService {
}
async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity) {
devLog('[DiaryDateActivityService::addGroupActivity] Check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryDateActivityService::addGroupActivity] Check diary date');
const diaryDateActivity = await DiaryDateActivity.findOne({
where: {
diaryDateId,
@@ -272,19 +245,16 @@ class DiaryDateActivityService {
console.error('[DiaryDateActivityService::addGroupActivity] Activity not found');
throw new Error('Activity not found');
}
devLog('[DiaryDateActivityService::addGroupActivity] Check group');
const group = await Group.findByPk(groupId);
if (!group || group.diaryDateId !== diaryDateActivity.diaryDateId) {
console.error('[DiaryDateActivityService::addGroupActivity] Group and date don\'t fit');
throw new Error('Group isn\'t related to date');
}
devLog('[DiaryDateActivityService::addGroupActivity] Get predefined activity');
const [predefinedActivity, created] = await PredefinedActivity.findOrCreate({
where: {
name: activity
}
});
devLog('[DiaryDateActivityService::addGroupActivity] Add group activity');
devLog(predefinedActivity);
const activityData = {
diaryDateActivity: diaryDateActivity.id,

View File

@@ -10,14 +10,11 @@ import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
class DiaryService {
async getDatesForClub(userToken, clubId) {
devLog('[DiaryService::getDatesForClub] - Check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryService::getDatesForClub] - Validate club existence');
const club = await Club.findByPk(clubId);
if (!club) {
throw new HttpError('Club not found', 404);
}
devLog('[DiaryService::getDatesForClub] - Load diary dates');
const dates = await DiaryDate.findAll({
where: { clubId },
include: [
@@ -30,14 +27,11 @@ class DiaryService {
}
async createDateForClub(userToken, clubId, date, trainingStart, trainingEnd) {
devLog('[DiaryService::createDateForClub] - Check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryService::createDateForClub] - Validate club existence');
const club = await Club.findByPk(clubId);
if (!club) {
throw new HttpError('Club not found', 404);
}
devLog('[DiaryService::createDateForClub] - Validate date');
const parsedDate = new Date(date);
if (isNaN(parsedDate.getTime())) {
throw new HttpError('Invalid date format', 400);
@@ -45,7 +39,6 @@ class DiaryService {
if (trainingStart && trainingEnd && trainingStart >= trainingEnd) {
throw new HttpError('Training start time must be before training end time', 400);
}
devLog('[DiaryService::createDateForClub] - Create new diary date');
const newDate = await DiaryDate.create({
date: parsedDate,
clubId,
@@ -57,9 +50,7 @@ class DiaryService {
}
async updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd) {
devLog('[DiaryService::updateTrainingTimes] - Check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryService::updateTrainingTimes] - Validate date');
const diaryDate = await DiaryDate.findOne({ where: { clubId, id: dateId } });
if (!diaryDate) {
throw new HttpError('Diary entry not found', 404);
@@ -67,7 +58,6 @@ class DiaryService {
if (trainingStart && trainingEnd && trainingStart >= trainingEnd) {
throw new HttpError('Training start time must be before training end time', 400);
}
devLog('[DiaryService::updateTrainingTimes] - Update training times');
diaryDate.trainingStart = trainingStart || null;
diaryDate.trainingEnd = trainingEnd || null;
await diaryDate.save();
@@ -75,14 +65,12 @@ class DiaryService {
}
async addNoteToDate(userToken, diaryDateId, content) {
devLog('[DiaryService::addNoteToDate] - Add note');
await checkAccess(userToken, diaryDateId);
await DiaryNote.create({ diaryDateId, content });
return await DiaryNote.findAll({ where: { diaryDateId }, order: [['createdAt', 'DESC']] });
}
async deleteNoteFromDate(userToken, noteId) {
devLog('[DiaryService::deleteNoteFromDate] - Delete note');
const note = await DiaryNote.findByPk(noteId);
if (!note) {
throw new HttpError('Note not found', 404);
@@ -93,7 +81,6 @@ class DiaryService {
}
async addTagToDate(userToken, diaryDateId, tagName) {
devLog('[DiaryService::addTagToDate] - Add tag');
await checkAccess(userToken, diaryDateId);
let tag = await DiaryTag.findOne({ where: { name: tagName } });
if (!tag) {
@@ -106,29 +93,24 @@ class DiaryService {
async addTagToDiaryDate(userToken, clubId, diaryDateId, tagId) {
checkAccess(userToken, clubId);
devLog(`[DiaryService::addTagToDiaryDate] - diaryDateId: ${diaryDateId}, tagId: ${tagId}`);
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (!diaryDate) {
throw new HttpError('DiaryDate not found', 404);
}
devLog('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
const existingEntry = await DiaryDateTag.findOne({
where: { diaryDateId, tagId }
});
if (existingEntry) {
return;
}
devLog('[DiaryService::addTagToDiaryDate] - Tag not found, creating new entry');
const tag = await DiaryTag.findByPk(tagId);
if (!tag) {
throw new HttpError('Tag not found', 404);
}
devLog('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
await DiaryDateTag.create({
diaryDateId,
tagId
});
devLog('[DiaryService::addTagToDiaryDate] - Get tags');
const tags = await DiaryDateTag.findAll({ where: {
diaryDateId: diaryDateId },
include: {
@@ -141,7 +123,6 @@ class DiaryService {
}
async getDiaryNotesForDateAndMember(diaryDateId, memberId) {
devLog('[DiaryService::getDiaryNotesForDateAndMember] - Fetching notes');
return await DiaryNote.findAll({
where: { diaryDateId, memberId },
order: [['createdAt', 'DESC']]
@@ -154,19 +135,15 @@ class DiaryService {
}
async removeDateForClub(userToken, clubId, dateId) {
devLog('[DiaryService::removeDateForClub] - Check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryService::removeDateForClub] - Validate date');
const diaryDate = await DiaryDate.findOne({ where: { id: dateId, clubId } });
if (!diaryDate) {
throw new HttpError('Diary entry not found', 404);
}
devLog('[DiaryService::removeDateForClub] - Check for activities');
const activityCount = await DiaryDateActivity.count({ where: { diaryDateId: dateId } });
if (activityCount > 0) {
throw new HttpError('Cannot delete date with activities', 409);
}
devLog('[DiaryService::removeDateForClub] - Delete diary date');
await diaryDate.destroy();
return { ok: true };
}

View File

@@ -0,0 +1,94 @@
import League from '../models/League.js';
import Season from '../models/Season.js';
import SeasonService from './seasonService.js';
import { devLog } from '../utils/logger.js';
class LeagueService {
static async getAllLeaguesByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const leagues = await League.findAll({
where: { clubId, seasonId },
include: [
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
console.error('[LeagueService.getAllLeaguesByClub] - Error:', error);
throw error;
}
}
static async getLeagueById(leagueId) {
try {
const league = await League.findByPk(leagueId, {
include: [
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
]
});
return league;
} catch (error) {
console.error('[LeagueService.getLeagueById] - Error:', error);
throw error;
}
}
static async createLeague(leagueData) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!leagueData.seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
leagueData.seasonId = currentSeason.id;
}
const league = await League.create(leagueData);
return league;
} catch (error) {
console.error('[LeagueService.createLeague] - Error:', error);
throw error;
}
}
static async updateLeague(leagueId, updateData) {
try {
const [updatedRowsCount] = await League.update(updateData, {
where: { id: leagueId }
});
return updatedRowsCount > 0;
} catch (error) {
console.error('[LeagueService.updateLeague] - Error:', error);
throw error;
}
}
static async deleteLeague(leagueId) {
try {
const deletedRowsCount = await League.destroy({
where: { id: leagueId }
});
return deletedRowsCount > 0;
} catch (error) {
console.error('[LeagueService.deleteLeague] - Error:', error);
throw error;
}
}
}
export default LeagueService;

View File

@@ -7,6 +7,7 @@ import Season from '../models/Season.js';
import Location from '../models/Location.js';
import League from '../models/League.js';
import Team from '../models/Team.js';
import SeasonService from './seasonService.js';
import { checkAccess } from '../utils/userUtils.js';
import { Op } from 'sequelize';
@@ -22,8 +23,7 @@ class MatchService {
seasonStartYear = currentYear - 1;
}
const seasonEndYear = seasonStartYear + 1;
const seasonEndYearString = seasonEndYear.toString().slice(-2);
return `${seasonStartYear}/${seasonEndYearString}`;
return `${seasonStartYear}/${seasonEndYear}`;
}
async importCSV(userToken, clubId, filePath) {
@@ -58,7 +58,6 @@ class MatchService {
},
});
matches.push({
seasonId: season.id,
date: parsedDate,
time: row['Termin'].split(' ')[1],
homeTeamId: homeTeamId,
@@ -72,7 +71,14 @@ class MatchService {
if (seasonString) {
season = await Season.findOne({ where: { season: seasonString } });
if (season) {
await Match.destroy({ where: { clubId, seasonId: season.id } });
// Lösche alle Matches für Ligen dieser Saison
const leagues = await League.findAll({
where: { seasonId: season.id, clubId }
});
const leagueIds = leagues.map(league => league.id);
if (leagueIds.length > 0) {
await Match.destroy({ where: { clubId, leagueId: leagueIds } });
}
}
}
const result = await Match.bulkCreate(matches);
@@ -99,33 +105,28 @@ class MatchService {
}
async getLeaguesForCurrentSeason(userToken, clubId) {
async getLeaguesForCurrentSeason(userToken, clubId, seasonId = null) {
await checkAccess(userToken, clubId);
const seasonString = this.generateSeasonString();
const season = await Season.findOne({
where: {
season: {
[Op.like]: `%${seasonString}%`
}
// Verwende SeasonService für korrekte Saison-Verwaltung
let season;
if (!seasonId) {
season = await SeasonService.getOrCreateCurrentSeason();
} else {
season = await SeasonService.getSeasonById(seasonId);
if (!season) {
throw new Error('Season not found');
}
});
if (!season) {
await Season.create({ season: seasonString });
throw new Error('Season not found');
}
try {
const leagues = await League.findAll({
include: [{
model: Match,
as: 'leagueMatches',
where: {
seasonId: season.id,
clubId: clubId
},
attributes: [],
}],
where: {
clubId: clubId,
seasonId: season.id
},
attributes: ['id', 'name'],
group: ['League.id'],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
@@ -134,48 +135,69 @@ class MatchService {
}
}
async getMatchesForLeagues(userToken, clubId) {
async getMatchesForLeagues(userToken, clubId, seasonId = null) {
await checkAccess(userToken, clubId);
const seasonString = this.generateSeasonString();
const season = await Season.findOne({
where: {
season: {
[Op.like]: `%${seasonString}%`
}
// Wenn keine Saison angegeben, verwende die aktuelle
let season;
if (!seasonId) {
season = await SeasonService.getOrCreateCurrentSeason();
} else {
season = await SeasonService.getSeasonById(seasonId);
if (!season) {
throw new Error('Season not found');
}
});
if (!season) {
throw new Error('Season not found');
}
const matches = await Match.findAll({
where: {
seasonId: season.id,
clubId: clubId,
},
include: [
{
model: League,
as: 'leagueDetails',
attributes: ['name'],
},
{
model: Team,
as: 'homeTeam', // Assuming your associations are set correctly
attributes: ['name'],
},
{
model: Team,
as: 'guestTeam',
attributes: ['name'],
},
{
model: Location,
as: 'location',
attributes: ['name', 'address', 'city', 'zip'],
}
]
}
});
return matches;
// Filtere Matches nach Liga-Saison und lade Daten manuell
const enrichedMatches = [];
for (const match of matches) {
// Lade Liga-Daten
const league = await League.findByPk(match.leagueId, { attributes: ['name', 'seasonId'] });
if (!league || league.seasonId !== season.id) {
continue; // Skip matches from other seasons
}
const enrichedMatch = {
id: match.id,
date: match.date,
time: match.time,
homeTeamId: match.homeTeamId,
guestTeamId: match.guestTeamId,
locationId: match.locationId,
leagueId: match.leagueId,
code: match.code,
homePin: match.homePin,
guestPin: match.guestPin,
homeTeam: { name: 'Unbekannt' },
guestTeam: { name: 'Unbekannt' },
location: { name: 'Unbekannt', address: '', city: '', zip: '' },
leagueDetails: { name: league.name }
};
if (match.homeTeamId) {
const homeTeam = await Team.findByPk(match.homeTeamId, { attributes: ['name'] });
if (homeTeam) enrichedMatch.homeTeam = homeTeam;
}
if (match.guestTeamId) {
const guestTeam = await Team.findByPk(match.guestTeamId, { attributes: ['name'] });
if (guestTeam) enrichedMatch.guestTeam = guestTeam;
}
if (match.locationId) {
const location = await Location.findByPk(match.locationId, {
attributes: ['name', 'address', 'city', 'zip']
});
if (location) enrichedMatch.location = location;
}
enrichedMatches.push(enrichedMatch);
}
return enrichedMatches;
}
async getMatchesForLeague(userToken, clubId, leagueId) {
@@ -193,34 +215,53 @@ class MatchService {
}
const matches = await Match.findAll({
where: {
seasonId: season.id,
clubId: clubId,
leagueId: leagueId
},
include: [
{
model: League,
as: 'leagueDetails',
attributes: ['name'],
},
{
model: Team,
as: 'homeTeam',
attributes: ['name'],
},
{
model: Team,
as: 'guestTeam',
attributes: ['name'],
},
{
model: Location,
as: 'location',
attributes: ['name', 'address', 'city', 'zip'],
}
]
}
});
return matches;
// Lade Team- und Location-Daten manuell
const enrichedMatches = [];
for (const match of matches) {
const enrichedMatch = {
id: match.id,
date: match.date,
time: match.time,
homeTeamId: match.homeTeamId,
guestTeamId: match.guestTeamId,
locationId: match.locationId,
leagueId: match.leagueId,
code: match.code,
homePin: match.homePin,
guestPin: match.guestPin,
homeTeam: { name: 'Unbekannt' },
guestTeam: { name: 'Unbekannt' },
location: { name: 'Unbekannt', address: '', city: '', zip: '' },
leagueDetails: { name: 'Unbekannt' }
};
if (match.homeTeamId) {
const homeTeam = await Team.findByPk(match.homeTeamId, { attributes: ['name'] });
if (homeTeam) enrichedMatch.homeTeam = homeTeam;
}
if (match.guestTeamId) {
const guestTeam = await Team.findByPk(match.guestTeamId, { attributes: ['name'] });
if (guestTeam) enrichedMatch.guestTeam = guestTeam;
}
if (match.locationId) {
const location = await Location.findByPk(match.locationId, {
attributes: ['name', 'address', 'city', 'zip']
});
if (location) enrichedMatch.location = location;
}
if (match.leagueId) {
const league = await League.findByPk(match.leagueId, { attributes: ['name'] });
if (league) enrichedMatch.leagueDetails = league;
}
enrichedMatches.push(enrichedMatch);
}
return enrichedMatches;
}
}

View File

@@ -9,11 +9,8 @@ import sharp from 'sharp';
import { devLog } from '../utils/logger.js';
class MemberService {
async getApprovalRequests(userToken, clubId) {
devLog('[MemberService::getApprovalRequest] - Check user access');
await checkAccess(userToken, clubId);
devLog('[MemberService::getApprovalRequest] - Load user');
const user = await getUserByToken(userToken);
devLog('[MemberService::getApprovalRequest] - Load userclub');
return await UserClub.findAll({
where: {
clubId: clubId,
@@ -24,9 +21,7 @@ class MemberService {
}
async getClubMembers(userToken, clubId, showAll) {
devLog('[getClubMembers] - Check access');
await checkAccess(userToken, clubId);
devLog('[getClubMembers] - Find members');
const where = {
clubId: clubId
};
@@ -45,7 +40,6 @@ class MemberService {
});
})
.then(membersWithImageStatus => {
devLog('[getClubMembers] - return members');
return membersWithImageStatus;
})
.catch(error => {
@@ -57,15 +51,11 @@ class MemberService {
async setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate, phone, email, active = true, testMembership = false,
picsInInternetAllowed = false, gender = 'unknown', ttr = null, qttr = null) {
try {
devLog('[setClubMembers] - Check access');
await checkAccess(userToken, clubId);
devLog('[setClubMembers] - set default member');
let member = null;
devLog('[setClubMembers] - load member if possible');
if (memberId) {
member = await Member.findOne({ where: { id: memberId } });
}
devLog('[setClubMembers] - set member');
if (member) {
member.firstName = firstName;
member.lastName = lastName;
@@ -99,7 +89,6 @@ class MemberService {
qttr: qttr,
});
}
devLog('[setClubMembers] - return response');
return {
status: 200,
response: { result: "success" },
@@ -153,34 +142,18 @@ class MemberService {
}
async updateRatingsFromMyTischtennis(userToken, clubId) {
devLog('[updateRatingsFromMyTischtennis] - Check access');
await checkAccess(userToken, clubId);
const user = await getUserByToken(userToken);
devLog('[updateRatingsFromMyTischtennis] - User:', user.id);
const myTischtennisService = (await import('./myTischtennisService.js')).default;
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
try {
// 1. myTischtennis-Session abrufen
devLog('[updateRatingsFromMyTischtennis] - Get session for user', user.id);
const session = await myTischtennisService.getSession(user.id);
devLog('[updateRatingsFromMyTischtennis] - Session retrieved:', {
hasAccessToken: !!session.accessToken,
hasCookie: !!session.cookie,
expiresAt: session.expiresAt
});
const account = await myTischtennisService.getAccount(user.id);
devLog('[updateRatingsFromMyTischtennis] - Account data:', {
id: account?.id,
email: account?.email,
clubId: account?.clubId,
clubName: account?.clubName,
fedNickname: account?.fedNickname,
hasSession: !!(account?.accessToken)
});
if (!account) {
console.error('[updateRatingsFromMyTischtennis] - No account found!');
@@ -217,24 +190,12 @@ class MemberService {
}
// 2. Rangliste vom Verein abrufen
devLog('[updateRatingsFromMyTischtennis] - Get club rankings', {
clubId: account.clubId,
fedNickname: account.fedNickname,
hasCookie: !!session.cookie
});
const rankings = await myTischtennisClient.getClubRankings(
session.cookie,
account.clubId,
account.fedNickname
);
devLog('[updateRatingsFromMyTischtennis] - Rankings result:', {
success: rankings.success,
entriesCount: rankings.entries?.length || 0,
error: rankings.error
});
if (!rankings.success) {
return {
status: 500,
@@ -252,9 +213,7 @@ class MemberService {
}
// 3. Alle Mitglieder des Clubs laden
devLog('[updateRatingsFromMyTischtennis] - Load club members for clubId:', clubId);
const members = await Member.findAll({ where: { clubId } });
devLog('[updateRatingsFromMyTischtennis] - Found members:', members.length);
let updated = 0;
const errors = [];
@@ -285,7 +244,6 @@ class MemberService {
oldTtr: oldTtr,
newTtr: rankingEntry.fedRank
});
devLog(`[updateRatingsFromMyTischtennis] - Updated ${firstName} ${lastName}: TTR ${oldTtr}${rankingEntry.fedRank}`);
} catch (error) {
console.error(`[updateRatingsFromMyTischtennis] - Error updating ${firstName} ${lastName}:`, error);
errors.push({
@@ -295,11 +253,9 @@ class MemberService {
}
} else {
notFound.push(`${firstName} ${lastName}`);
devLog(`[updateRatingsFromMyTischtennis] - Not found in rankings: ${firstName} ${lastName}`);
}
}
devLog('[updateRatingsFromMyTischtennis] - Update complete');
devLog(`Updated: ${updated}, Not found: ${notFound.length}, Errors: ${errors.length}`);
let message = `${updated} Mitglied(er) aktualisiert.`;

View File

@@ -68,20 +68,12 @@ class MyTischtennisService {
account.userData = loginResult.user;
// Hole Club-ID und Federation
devLog('[myTischtennisService] - Getting user profile...');
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
devLog('[myTischtennisService] - Profile result:', {
success: profileResult.success,
clubId: profileResult.clubId,
clubName: profileResult.clubName,
fedNickname: profileResult.fedNickname
});
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
devLog('[myTischtennisService] - Updated account with club data');
} else {
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
}
@@ -177,20 +169,12 @@ class MyTischtennisService {
account.userData = loginResult.user;
// Hole Club-ID und Federation
devLog('[myTischtennisService] - Getting user profile...');
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
devLog('[myTischtennisService] - Profile result:', {
success: profileResult.success,
clubId: profileResult.clubId,
clubName: profileResult.clubName,
fedNickname: profileResult.fedNickname
});
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
devLog('[myTischtennisService] - Updated account with club data');
} else {
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
}

View File

@@ -0,0 +1,638 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pdfParse = require('pdf-parse/lib/pdf-parse.js');
import { Op } from 'sequelize';
import Match from '../models/Match.js';
import Team from '../models/Team.js';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Location from '../models/Location.js';
import { devLog } from '../utils/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class PDFParserService {
/**
* Parst eine PDF-Datei und extrahiert Spiel-Daten
* @param {string} filePath - Pfad zur PDF-Datei
* @param {number} clubId - ID des Vereins
* @returns {Promise<Object>} Geparste Spiel-Daten
*/
static async parsePDF(filePath, clubId) {
try {
if (!fs.existsSync(filePath)) {
throw new Error('PDF-Datei nicht gefunden');
}
// Bestimme Dateityp basierend auf Dateiendung
const fileExtension = path.extname(filePath).toLowerCase();
let fileContent;
if (fileExtension === '.pdf') {
// Echte PDF-Parsing
const pdfBuffer = fs.readFileSync(filePath);
const pdfData = await pdfParse(pdfBuffer);
fileContent = pdfData.text;
} else {
// Fallback für TXT-Dateien (für Tests)
fileContent = fs.readFileSync(filePath, 'utf8');
}
// Parse den Text nach Spiel-Daten
const parsedData = this.extractMatchData(fileContent, clubId);
return parsedData;
} catch (error) {
console.error('[PDFParserService.parsePDF] - Error:', error);
throw error;
}
}
/**
* Extrahiert Spiel-Daten aus dem PDF-Text
* @param {string} text - Der extrahierte Text aus der PDF
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Daten mit Matches und Metadaten
*/
static extractMatchData(text, clubId) {
const matches = [];
const errors = [];
const metadata = {
totalLines: 0,
parsedMatches: 0,
errors: 0
};
try {
// Teile Text in Zeilen auf
const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
metadata.totalLines = lines.length;
// Verschiedene Parsing-Strategien je nach PDF-Format
const strategies = [
{ name: 'Standard Format', fn: this.parseStandardFormat },
{ name: 'Table Format', fn: this.parseTableFormat },
{ name: 'List Format', fn: this.parseListFormat }
];
for (const strategy of strategies) {
try {
const result = strategy.fn(lines, clubId);
if (result.matches.length > 0) {
matches.push(...result.matches);
metadata.parsedMatches += result.matches.length;
break; // Erste erfolgreiche Strategie verwenden
}
} catch (strategyError) {
errors.push(`Strategy ${strategy.name} failed: ${strategyError.message}`);
}
}
metadata.errors = errors.length;
return {
matches,
errors,
metadata,
rawText: text.substring(0, 1000), // Erste 1000 Zeichen für Debugging
allLines: lines, // Alle Zeilen für Debugging
debugInfo: {
totalTextLength: text.length,
totalLines: lines.length,
firstFewLines: lines.slice(0, 10),
lastFewLines: lines.slice(-5)
}
};
} catch (error) {
console.error('[PDFParserService.extractMatchData] - Error:', error);
throw error;
}
}
/**
* Standard-Format Parser (Datum, Zeit, Heimteam, Gastteam, Code, Pins)
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseStandardFormat(lines, clubId) {
const matches = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Suche nach Datum-Pattern (dd.mm.yyyy oder dd/mm/yyyy)
const dateMatch = line.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
// Debug: Zeige die gesamte Zeile mit sichtbaren Whitespaces
const debugLine = line.replace(/\s/g, (match) => {
if (match === ' ') return '·'; // Mittelpunkt für normales Leerzeichen
if (match === '\t') return '→'; // Pfeil für Tab
if (match === '\n') return '↵'; // Enter-Zeichen
if (match === '\r') return '⏎'; // Carriage Return
return `[${match.charCodeAt(0)}]`; // Zeichencode für andere Whitespaces
});
try {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
// Suche nach Zeit-Pattern direkt nach dem Datum (hh:mm) - Format: Wt.dd.mm.yyyyhh:MM
const timeMatch = line.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})(\d{1,2}):(\d{2})/);
let time = null;
if (timeMatch) {
time = `${timeMatch[4].padStart(2, '0')}:${timeMatch[5]}`;
}
// Entferne Datum und Zeit vom Anfang der Zeile
const cleanLine = line.replace(/^[A-Za-z]{2}\.(\d{1,2})[./](\d{1,2})[./](\d{4})(\d{1,2}):(\d{2})\s*/, '');
// Entferne Nummerierung am Anfang (z.B. "(1)")
const cleanLine2 = cleanLine.replace(/^\(\d+\)/, '');
// Entferne alle Inhalte in Klammern (z.B. "(J11)")
const cleanLine3 = cleanLine2.replace(/\([^)]*\)/g, '');
// Suche nach Code (12 Zeichen) oder PIN (4 Ziffern) am Ende
const codeMatch = cleanLine3.match(/([A-Z0-9]{12})$/);
const pinMatch = cleanLine3.match(/(\d{4})$/);
let code = null;
let homePin = null;
let guestPin = null;
let teamsPart = cleanLine3;
if (codeMatch) {
// Code gefunden (12 Zeichen)
code = codeMatch[1];
teamsPart = cleanLine3.substring(0, cleanLine3.length - code.length).trim();
} else if (pinMatch) {
// PIN gefunden (4 Ziffern)
const pin = pinMatch[1];
teamsPart = cleanLine3.substring(0, cleanLine3.length - pin.length).trim();
// PIN gehört zu dem Team, das direkt vor der PIN steht
// Analysiere die Position der PIN in der ursprünglichen Zeile
const pinIndex = cleanLine3.lastIndexOf(pin);
const teamsPartIndex = cleanLine3.indexOf(teamsPart);
// Wenn PIN direkt nach dem Teams-Part steht, gehört sie zur Heimmannschaft
// Wenn PIN zwischen den Teams steht, gehört sie zur Gastmannschaft
if (pinIndex === teamsPartIndex + teamsPart.length) {
// PIN steht direkt nach den Teams -> Heimmannschaft
homePin = pin;
} else {
// PIN steht zwischen den Teams -> Gastmannschaft
guestPin = pin;
}
}
if (code || pinMatch) {
// Debug: Zeige Whitespaces als lesbare Zeichen
const debugTeamsPart = teamsPart.replace(/\s/g, (match) => {
if (match === ' ') return '·'; // Mittelpunkt für normales Leerzeichen
if (match === '\t') return '→'; // Pfeil für Tab
return `[${match.charCodeAt(0)}]`; // Zeichencode für andere Whitespaces
});
// Neue Strategie: Teile die Zeile durch mehrere Leerzeichen (wie in der Tabelle)
// Die Struktur ist: Heimmannschaft Gastmannschaft Code
const parts = teamsPart.split(/\s{2,}/); // Mindestens 2 Leerzeichen als Trenner
let homeTeamName = '';
let guestTeamName = '';
if (parts.length >= 2) {
homeTeamName = parts[0].trim();
guestTeamName = parts[1].trim();
// Entferne noch verbleibende Klammern aus den Team-Namen
homeTeamName = homeTeamName.replace(/\([^)]*\)/g, '').trim();
guestTeamName = guestTeamName.replace(/\([^)]*\)/g, '').trim();
// Erkenne römische Ziffern am Ende der Team-Namen
// Römische Ziffern: I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, etc.
const romanNumeralPattern = /\s+(I{1,3}|IV|V|VI{0,3}|IX|X|XI{0,2})$/;
// Prüfe Heimteam auf römische Ziffern
const homeRomanMatch = homeTeamName.match(romanNumeralPattern);
if (homeRomanMatch) {
const romanNumeral = homeRomanMatch[1];
const baseName = homeTeamName.replace(romanNumeralPattern, '').trim();
homeTeamName = `${baseName} ${romanNumeral}`;
}
// Prüfe Gastteam auf römische Ziffern
const guestRomanMatch = guestTeamName.match(romanNumeralPattern);
if (guestRomanMatch) {
const romanNumeral = guestRomanMatch[1];
const baseName = guestTeamName.replace(romanNumeralPattern, '').trim();
guestTeamName = `${baseName} ${romanNumeral}`;
}
} else {
// Fallback: Versuche mit einzelnen Leerzeichen zu trennen
// Strategie 1: Suche nach "Harheimer TC" als Heimteam
if (teamsPart.includes('Harheimer TC')) {
const harheimerIndex = teamsPart.indexOf('Harheimer TC');
homeTeamName = 'Harheimer TC';
guestTeamName = teamsPart.substring(harheimerIndex + 'Harheimer TC'.length).trim();
// Entferne Klammern aus Gastteam
guestTeamName = guestTeamName.replace(/\([^)]*\)/g, '').trim();
} else {
// Strategie 2: Suche nach Großbuchstaben am Anfang des zweiten Teams
const teamSplitMatch = teamsPart.match(/^([A-Za-z0-9\s\-\.]+?)\s+([A-Z][A-Za-z0-9\s\-\.]+)$/);
if (teamSplitMatch) {
homeTeamName = teamSplitMatch[1].trim();
guestTeamName = teamSplitMatch[2].trim();
} else {
continue;
}
}
}
if (homeTeamName && guestTeamName) {
let debugInfo;
if (code) {
debugInfo = `code: "${code}"`;
} else if (homePin && guestPin) {
debugInfo = `homePin: "${homePin}", guestPin: "${guestPin}"`;
} else if (homePin) {
debugInfo = `homePin: "${homePin}"`;
} else if (guestPin) {
debugInfo = `guestPin: "${guestPin}"`;
}
matches.push({
date: date,
time: time,
homeTeamName: homeTeamName,
guestTeamName: guestTeamName,
code: code,
homePin: homePin,
guestPin: guestPin,
clubId: clubId,
rawLine: line
});
}
} else {
}
} catch (parseError) {
}
}
}
return { matches };
}
/**
* Tabellen-Format Parser
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseTableFormat(lines, clubId) {
const matches = [];
// Suche nach Tabellen-Header
let headerIndex = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i].toLowerCase().includes('datum') &&
lines[i].toLowerCase().includes('zeit') &&
lines[i].toLowerCase().includes('heim') &&
lines[i].toLowerCase().includes('gast')) {
headerIndex = i;
break;
}
}
if (headerIndex >= 0) {
// Parse Tabellen-Zeilen
for (let i = headerIndex + 1; i < lines.length; i++) {
const line = lines[i];
const columns = line.split(/\s{2,}|\t/); // Split bei mehreren Leerzeichen oder Tabs
if (columns.length >= 4) {
try {
const dateStr = columns[0];
const timeStr = columns[1];
const homeTeam = columns[2];
const guestTeam = columns[3];
const code = columns[4] || null;
const homePin = columns[5] || null;
const guestPin = columns[6] || null;
// Parse Datum
const dateMatch = dateStr.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
matches.push({
date: date,
time: timeStr || null,
homeTeamName: homeTeam.trim(),
guestTeamName: guestTeam.trim(),
code: code ? code.trim() : null,
homePin: homePin ? homePin.trim() : null,
guestPin: guestPin ? guestPin.trim() : null,
clubId: clubId,
rawLine: line
});
}
} catch (parseError) {
}
}
}
}
return { matches };
}
/**
* Listen-Format Parser
* @param {Array} lines - Textzeilen
* @param {number} clubId - ID des Vereins
* @returns {Object} Geparste Matches
*/
static parseListFormat(lines, clubId) {
const matches = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Suche nach Nummerierten Listen (1., 2., etc.)
const listMatch = line.match(/^\d+\.\s*(.+)/);
if (listMatch) {
const content = listMatch[1];
// Versuche verschiedene Formate zu parsen
const patterns = [
/(\d{1,2}[./]\d{1,2}[./]\d{4})\s+(\d{1,2}:\d{2})?\s+(.+?)\s+vs?\s+(.+?)(?:\s+code[:\s]*([A-Za-z0-9]+))?(?:\s+home[:\s]*pin[:\s]*([A-Za-z0-9]+))?(?:\s+guest[:\s]*pin[:\s]*([A-Za-z0-9]+))?/i,
/(\d{1,2}[./]\d{1,2}[./]\d{4})\s+(\d{1,2}:\d{2})?\s+(.+?)\s+-\s+(.+?)(?:\s+code[:\s]*([A-Za-z0-9]+))?(?:\s+heim[:\s]*pin[:\s]*([A-Za-z0-9]+))?(?:\s+gast[:\s]*pin[:\s]*([A-Za-z0-9]+))?/i
];
for (const pattern of patterns) {
const match = content.match(pattern);
if (match) {
try {
const [, dateStr, timeStr, homeTeam, guestTeam, code, homePin, guestPin] = match;
// Parse Datum
const dateMatch = dateStr.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
if (dateMatch) {
const [, day, month, year] = dateMatch;
const date = new Date(`${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`);
matches.push({
date: date,
time: timeStr || null,
homeTeamName: homeTeam.trim(),
guestTeamName: guestTeam.trim(),
code: code ? code.trim() : null,
homePin: homePin ? homePin.trim() : null,
guestPin: guestPin ? guestPin.trim() : null,
clubId: clubId,
rawLine: line
});
break; // Erste erfolgreiche Pattern verwenden
}
} catch (parseError) {
}
}
}
}
}
return { matches };
}
/**
* Speichert geparste Matches in der Datenbank
* @param {Array} matches - Array von Match-Objekten
* @param {number} leagueId - ID der Liga
* @returns {Promise<Object>} Ergebnis der Speicherung
*/
static async saveMatchesToDatabase(matches, leagueId) {
try {
const results = {
created: 0,
updated: 0,
errors: []
};
for (const matchData of matches) {
try {
let debugInfo;
if (matchData.code) {
debugInfo = `Code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
debugInfo = `HomePin: ${matchData.homePin}, GuestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
debugInfo = `HomePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
debugInfo = `GuestPin: ${matchData.guestPin}`;
}
// Lade alle Matches für das Datum und die Liga
// Konvertiere das Datum zu einem Datum ohne Zeit für den Vergleich
const dateOnly = new Date(matchData.date.getFullYear(), matchData.date.getMonth(), matchData.date.getDate());
const nextDay = new Date(dateOnly);
nextDay.setDate(nextDay.getDate() + 1);
const existingMatches = await Match.findAll({
where: {
date: {
[Op.gte]: dateOnly, // Größer oder gleich dem Datum
[Op.lt]: nextDay // Kleiner als der nächste Tag
},
leagueId: leagueId,
...(matchData.time && { time: matchData.time }) // Füge Zeit hinzu wenn vorhanden
},
include: [
{
model: Team,
as: 'homeTeam',
attributes: ['id', 'name']
},
{
model: Team,
as: 'guestTeam',
attributes: ['id', 'name']
}
]
});
const timeFilter = matchData.time ? ` and time ${matchData.time}` : '';
// Debug: Zeige alle gefundenen Matches und lade Teams manuell
for (let i = 0; i < existingMatches.length; i++) {
const match = existingMatches[i];
// Lade Teams manuell
const homeTeam = await Team.findByPk(match.homeTeamId);
const guestTeam = await Team.findByPk(match.guestTeamId);
// Füge die Teams zum Match-Objekt hinzu
match.homeTeam = homeTeam;
match.guestTeam = guestTeam;
}
// Suche nach dem passenden Match basierend auf Gastmannschaft
const matchingMatch = existingMatches.find(match => {
if (!match.guestTeam) return false;
const guestTeamName = match.guestTeam.name.toLowerCase();
const searchGuestName = matchData.guestTeamName.toLowerCase();
// Exakte Übereinstimmung oder Teilstring-Match
return guestTeamName === searchGuestName ||
guestTeamName.includes(searchGuestName) ||
searchGuestName.includes(guestTeamName);
});
if (matchingMatch) {
// Update das bestehende Match mit Code und Pins
// Erstelle Update-Objekt nur mit vorhandenen Feldern
const updateData = {};
if (matchData.code) {
updateData.code = matchData.code;
}
if (matchData.homePin) {
updateData.homePin = matchData.homePin;
}
if (matchData.guestPin) {
updateData.guestPin = matchData.guestPin;
}
await matchingMatch.update(updateData);
results.updated++;
let updateInfo;
if (matchData.code) {
updateInfo = `code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
updateInfo = `homePin: ${matchData.homePin}, guestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
updateInfo = `homePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
updateInfo = `guestPin: ${matchData.guestPin}`;
}
// Lade das aktualisierte Match neu, um die aktuellen Werte zu zeigen
await matchingMatch.reload();
const currentValues = [];
if (matchingMatch.code) currentValues.push(`code: ${matchingMatch.code}`);
if (matchingMatch.homePin) currentValues.push(`homePin: ${matchingMatch.homePin}`);
if (matchingMatch.guestPin) currentValues.push(`guestPin: ${matchingMatch.guestPin}`);
} else {
// Fallback: Versuche Teams direkt zu finden
const homeTeam = await Team.findOne({
where: {
name: matchData.homeTeamName,
clubId: matchData.clubId
}
});
const guestTeam = await Team.findOne({
where: {
name: matchData.guestTeamName,
clubId: matchData.clubId
}
});
// Debug: Zeige alle verfügbaren Teams für diesen Club
if (!homeTeam || !guestTeam) {
const allTeams = await Team.findAll({
where: { clubId: matchData.clubId },
attributes: ['id', 'name']
});
// Versuche Fuzzy-Matching für Team-Namen
const homeTeamFuzzy = allTeams.find(t =>
t.name.toLowerCase().includes(matchData.homeTeamName.toLowerCase()) ||
matchData.homeTeamName.toLowerCase().includes(t.name.toLowerCase())
);
const guestTeamFuzzy = allTeams.find(t =>
t.name.toLowerCase().includes(matchData.guestTeamName.toLowerCase()) ||
matchData.guestTeamName.toLowerCase().includes(t.name.toLowerCase())
);
if (homeTeamFuzzy) {
}
if (guestTeamFuzzy) {
}
}
if (!homeTeam || !guestTeam) {
let errorInfo;
if (matchData.code) {
errorInfo = `Code: ${matchData.code}`;
} else if (matchData.homePin && matchData.guestPin) {
errorInfo = `HomePin: ${matchData.homePin}, GuestPin: ${matchData.guestPin}`;
} else if (matchData.homePin) {
errorInfo = `HomePin: ${matchData.homePin}`;
} else if (matchData.guestPin) {
errorInfo = `GuestPin: ${matchData.guestPin}`;
}
results.errors.push(`Teams nicht gefunden: "${matchData.homeTeamName}" oder "${matchData.guestTeamName}" (Datum: ${matchData.date.toISOString().split('T')[0]}, Zeit: ${matchData.time}, ${errorInfo})`);
continue;
}
// Erstelle neues Match (Fallback)
await Match.create({
date: matchData.date,
time: matchData.time,
homeTeamId: homeTeam.id,
guestTeamId: guestTeam.id,
leagueId: leagueId,
clubId: matchData.clubId,
code: matchData.code,
homePin: matchData.homePin,
guestPin: matchData.guestPin,
locationId: 1 // Default Location, kann später angepasst werden
});
results.created++;
}
} catch (matchError) {
console.error('[PDFParserService.saveMatchesToDatabase] - Error:', matchError);
results.errors.push(`Fehler beim Speichern von Match: ${matchData.rawLine} - ${matchError.message}`);
}
}
return results;
} catch (error) {
console.error('[PDFParserService.saveMatchesToDatabase] - Error:', error);
throw error;
}
}
}
export default PDFParserService;

View File

@@ -8,7 +8,6 @@ import { Op } from 'sequelize';
import { devLog } from '../utils/logger.js';
class PredefinedActivityService {
async createPredefinedActivity(data) {
devLog('[PredefinedActivityService::createPredefinedActivity] - Creating predefined activity');
return await PredefinedActivity.create({
name: data.name,
code: data.code,
@@ -21,10 +20,8 @@ class PredefinedActivityService {
}
async updatePredefinedActivity(id, data) {
devLog(`[PredefinedActivityService::updatePredefinedActivity] - Updating predefined activity with id: ${id}`);
const activity = await PredefinedActivity.findByPk(id);
if (!activity) {
devLog('[PredefinedActivityService::updatePredefinedActivity] - Activity not found');
throw new Error('Predefined activity not found');
}
return await activity.update({
@@ -39,7 +36,6 @@ class PredefinedActivityService {
}
async getAllPredefinedActivities() {
devLog('[PredefinedActivityService::getAllPredefinedActivities] - Fetching all predefined activities');
return await PredefinedActivity.findAll({
order: [
[sequelize.literal('code IS NULL'), 'ASC'], // Non-null codes first
@@ -50,10 +46,8 @@ class PredefinedActivityService {
}
async getPredefinedActivityById(id) {
devLog(`[PredefinedActivityService::getPredefinedActivityById] - Fetching predefined activity with id: ${id}`);
const activity = await PredefinedActivity.findByPk(id);
if (!activity) {
devLog('[PredefinedActivityService::getPredefinedActivityById] - Activity not found');
throw new Error('Predefined activity not found');
}
return activity;
@@ -81,7 +75,6 @@ class PredefinedActivityService {
}
async mergeActivities(sourceId, targetId) {
devLog(`[PredefinedActivityService::mergeActivities] - Merge ${sourceId} -> ${targetId}`);
if (!sourceId || !targetId) throw new Error('sourceId and targetId are required');
if (Number(sourceId) === Number(targetId)) throw new Error('sourceId and targetId must differ');
@@ -121,7 +114,6 @@ class PredefinedActivityService {
}
async deduplicateActivities() {
devLog('[PredefinedActivityService::deduplicateActivities] - Start');
const all = await PredefinedActivity.findAll();
const nameToActivities = new Map();
for (const activity of all) {
@@ -143,7 +135,6 @@ class PredefinedActivityService {
mergedCount++;
}
}
devLog('[PredefinedActivityService::deduplicateActivities] - Done', { mergedCount, groupCount });
return { mergedCount, groupCount };
}
}

View File

@@ -0,0 +1,149 @@
import Season from '../models/Season.js';
import { devLog } from '../utils/logger.js';
class SeasonService {
/**
* Ermittelt die aktuelle Saison basierend auf dem aktuellen Datum
* @returns {string} Saison im Format "2023/2024"
*/
static getCurrentSeasonString() {
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1; // getMonth() ist 0-basiert
// Ab 1. Juli: neue Saison beginnt
if (currentMonth >= 7) {
return `${currentYear}/${currentYear + 1}`;
} else {
return `${currentYear - 1}/${currentYear}`;
}
}
/**
* Holt oder erstellt die aktuelle Saison
* @returns {Promise<Season>} Die aktuelle Saison
*/
static async getOrCreateCurrentSeason() {
try {
const currentSeasonString = this.getCurrentSeasonString();
// Versuche die aktuelle Saison zu finden
let season = await Season.findOne({
where: { season: currentSeasonString }
});
// Falls nicht vorhanden, erstelle sie
if (!season) {
season = await Season.create({
season: currentSeasonString
});
}
return season;
} catch (error) {
console.error('[SeasonService.getOrCreateCurrentSeason] - Error:', error);
throw error;
}
}
/**
* Holt alle verfügbaren Saisons
* @returns {Promise<Array<Season>>} Alle Saisons sortiert nach Name
*/
static async getAllSeasons() {
try {
const seasons = await Season.findAll({
order: [['season', 'DESC']] // Neueste zuerst
});
return seasons;
} catch (error) {
console.error('[SeasonService.getAllSeasons] - Error:', error);
throw error;
}
}
/**
* Erstellt eine neue Saison
* @param {string} seasonString - Saison im Format "2023/2024"
* @returns {Promise<Season>} Die erstellte Saison
*/
static async createSeason(seasonString) {
try {
// Prüfe ob Saison bereits existiert
const existingSeason = await Season.findOne({
where: { season: seasonString }
});
if (existingSeason) {
throw new Error('Season already exists');
}
const season = await Season.create({
season: seasonString
});
return season;
} catch (error) {
console.error('[SeasonService.createSeason] - Error:', error);
throw error;
}
}
/**
* Holt eine Saison nach ID
* @param {number} seasonId - Die Saison-ID
* @returns {Promise<Season|null>} Die Saison oder null
*/
static async getSeasonById(seasonId) {
try {
const season = await Season.findByPk(seasonId);
return season;
} catch (error) {
console.error('[SeasonService.getSeasonById] - Error:', error);
throw error;
}
}
/**
* Löscht eine Saison (nur wenn keine Teams/Ligen damit verknüpft sind)
* @param {number} seasonId - Die Saison-ID
* @returns {Promise<boolean>} True wenn gelöscht, false wenn nicht möglich
*/
static async deleteSeason(seasonId) {
try {
// Prüfe ob Saison verwendet wird
const season = await Season.findByPk(seasonId, {
include: [
{ association: 'teams' },
{ association: 'leagues' }
]
});
if (!season) {
return false;
}
// Prüfe ob Saison verwendet wird
if (season.teams && season.teams.length > 0) {
throw new Error('Season is used by teams');
}
if (season.leagues && season.leagues.length > 0) {
throw new Error('Season is used by leagues');
}
await Season.destroy({
where: { id: seasonId }
});
return true;
} catch (error) {
console.error('[SeasonService.deleteSeason] - Error:', error);
throw error;
}
}
}
export default SeasonService;

View File

@@ -0,0 +1,188 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import TeamDocument from '../models/TeamDocument.js';
import ClubTeam from '../models/ClubTeam.js';
import { devLog } from '../utils/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class TeamDocumentService {
/**
* Speichert ein hochgeladenes Dokument für ein Club-Team
* @param {Object} file - Das hochgeladene File-Objekt (von multer)
* @param {number} clubTeamId - Die ID des Club-Teams
* @param {string} documentType - Der Typ des Dokuments ('code_list' oder 'pin_list')
* @returns {Promise<TeamDocument>} Das erstellte TeamDocument
*/
static async uploadDocument(file, clubTeamId, documentType) {
try {
// Prüfe ob das Club-Team existiert
const clubTeam = await ClubTeam.findByPk(clubTeamId);
if (!clubTeam) {
throw new Error('Club-Team nicht gefunden');
}
// Generiere einen eindeutigen Dateinamen
const fileExtension = path.extname(file.originalname);
const uniqueFileName = `${clubTeamId}_${documentType}_${Date.now()}${fileExtension}`;
// Zielverzeichnis für Team-Dokumente
const uploadDir = path.join(__dirname, '..', 'uploads', 'team-documents');
// Erstelle Upload-Verzeichnis falls es nicht existiert
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const filePath = path.join(uploadDir, uniqueFileName);
// Verschiebe die Datei vom temporären Verzeichnis zum finalen Speicherort
fs.renameSync(file.path, filePath);
// Lösche alte Dokumente des gleichen Typs für dieses Team
await this.deleteDocumentsByType(clubTeamId, documentType);
// Erstelle Datenbankeintrag
const teamDocument = await TeamDocument.create({
fileName: uniqueFileName,
originalFileName: file.originalname,
filePath: filePath,
fileSize: file.size,
mimeType: file.mimetype,
documentType: documentType,
clubTeamId: clubTeamId
});
return teamDocument;
} catch (error) {
console.error('[TeamDocumentService.uploadDocument] - Error:', error);
throw error;
}
}
/**
* Holt alle Dokumente für ein Club-Team
* @param {number} clubTeamId - Die ID des Club-Teams
* @returns {Promise<Array<TeamDocument>>} Liste der Dokumente
*/
static async getDocumentsByClubTeam(clubTeamId) {
try {
const documents = await TeamDocument.findAll({
where: { clubTeamId },
order: [['createdAt', 'DESC']]
});
return documents;
} catch (error) {
console.error('[TeamDocumentService.getDocumentsByClubTeam] - Error:', error);
throw error;
}
}
/**
* Holt ein spezifisches Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<TeamDocument|null>} Das Dokument oder null
*/
static async getDocumentById(documentId) {
try {
const document = await TeamDocument.findByPk(documentId, {
include: [{
model: ClubTeam,
as: 'clubTeam',
attributes: ['id', 'name', 'clubId']
}]
});
return document;
} catch (error) {
console.error('[TeamDocumentService.getDocumentById] - Error:', error);
throw error;
}
}
/**
* Löscht ein Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<boolean>} True wenn gelöscht, sonst false
*/
static async deleteDocument(documentId) {
try {
const document = await TeamDocument.findByPk(documentId);
if (!document) {
return false;
}
// Lösche die physische Datei
if (fs.existsSync(document.filePath)) {
fs.unlinkSync(document.filePath);
}
// Lösche den Datenbankeintrag
const deletedRows = await TeamDocument.destroy({
where: { id: documentId }
});
return deletedRows > 0;
} catch (error) {
console.error('[TeamDocumentService.deleteDocument] - Error:', error);
throw error;
}
}
/**
* Löscht alle Dokumente eines bestimmten Typs für ein Club-Team
* @param {number} clubTeamId - Die ID des Club-Teams
* @param {string} documentType - Der Typ des Dokuments
* @returns {Promise<number>} Anzahl der gelöschten Dokumente
*/
static async deleteDocumentsByType(clubTeamId, documentType) {
try {
const documents = await TeamDocument.findAll({
where: { clubTeamId, documentType }
});
let deletedCount = 0;
for (const document of documents) {
// Lösche die physische Datei
if (fs.existsSync(document.filePath)) {
fs.unlinkSync(document.filePath);
}
deletedCount++;
}
// Lösche die Datenbankeinträge
const deletedRows = await TeamDocument.destroy({
where: { clubTeamId, documentType }
});
return deletedRows;
} catch (error) {
console.error('[TeamDocumentService.deleteDocumentsByType] - Error:', error);
throw error;
}
}
/**
* Holt den Dateipfad für ein Dokument
* @param {number} documentId - Die ID des Dokuments
* @returns {Promise<string|null>} Der Dateipfad oder null
*/
static async getDocumentPath(documentId) {
try {
const document = await TeamDocument.findByPk(documentId);
return document ? document.filePath : null;
} catch (error) {
console.error('[TeamDocumentService.getDocumentPath] - Error:', error);
throw error;
}
}
}
export default TeamDocumentService;

View File

@@ -0,0 +1,132 @@
import Team from '../models/Team.js';
import League from '../models/League.js';
import Club from '../models/Club.js';
import Season from '../models/Season.js';
import SeasonService from './seasonService.js';
import { devLog } from '../utils/logger.js';
class TeamService {
static async getAllTeamsByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const teams = await Team.findAll({
where: { clubId, seasonId },
include: [
{
model: League,
as: 'league',
attributes: ['id', 'name']
},
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
],
order: [['name', 'ASC']]
});
return teams;
} catch (error) {
console.error('[TeamService.getAllTeamsByClub] - Error:', error);
throw error;
}
}
static async getTeamById(teamId) {
try {
const team = await Team.findByPk(teamId, {
include: [
{
model: League,
as: 'league',
attributes: ['id', 'name']
},
{
model: Club,
as: 'club',
attributes: ['id', 'name']
},
{
model: Season,
as: 'season',
attributes: ['id', 'season']
}
]
});
return team;
} catch (error) {
console.error('[TeamService.getTeamById] - Error:', error);
throw error;
}
}
static async createTeam(teamData) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!teamData.seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
teamData.seasonId = currentSeason.id;
}
const team = await Team.create(teamData);
return team;
} catch (error) {
console.error('[TeamService.createTeam] - Error:', error);
throw error;
}
}
static async updateTeam(teamId, updateData) {
try {
const [updatedRowsCount] = await Team.update(updateData, {
where: { id: teamId }
});
return updatedRowsCount > 0;
} catch (error) {
console.error('[TeamService.updateTeam] - Error:', error);
throw error;
}
}
static async deleteTeam(teamId) {
try {
const deletedRowsCount = await Team.destroy({
where: { id: teamId }
});
return deletedRowsCount > 0;
} catch (error) {
console.error('[TeamService.deleteTeam] - Error:', error);
throw error;
}
}
static async getLeaguesByClub(clubId, seasonId = null) {
try {
// Wenn keine Saison angegeben, verwende die aktuelle
if (!seasonId) {
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
seasonId = currentSeason.id;
}
const leagues = await League.findAll({
where: { clubId, seasonId },
attributes: ['id', 'name', 'seasonId'],
order: [['name', 'ASC']]
});
return leagues;
} catch (error) {
console.error('[TeamService.getLeaguesByClub] - Error:', error);
throw error;
}
}
}
export default TeamService;

View File

@@ -203,11 +203,8 @@ class TournamentService {
}
// 4) RoundRobin anlegen wie gehabt - NUR innerhalb jeder Gruppe
devLog(`[fillGroups] Erstelle Matches für ${groups.length} Gruppen`);
for (const g of groups) {
devLog(`[fillGroups] Verarbeite Gruppe ${g.id}`);
const gm = await TournamentMember.findAll({ where: { groupId: g.id } });
devLog(`[fillGroups] Gruppe ${g.id} hat ${gm.length} Teilnehmer:`, gm.map(m => ({ id: m.id, name: m.member?.firstName + ' ' + m.member?.lastName })));
if (gm.length < 2) {
console.warn(`Gruppe ${g.id} hat nur ${gm.length} Teilnehmer - keine Matches erstellt`);
@@ -215,10 +212,8 @@ class TournamentService {
}
const rounds = this.generateRoundRobinSchedule(gm);
devLog(`[fillGroups] Gruppe ${g.id} hat ${rounds.length} Runden`);
for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
devLog(`[fillGroups] Runde ${roundIndex + 1} für Gruppe ${g.id}:`, rounds[roundIndex]);
for (const [p1Id, p2Id] of rounds[roundIndex]) {
// Prüfe, ob beide Spieler zur gleichen Gruppe gehören
const p1 = gm.find(p => p.id === p1Id);
@@ -232,7 +227,6 @@ class TournamentService {
player2Id: p2Id,
groupRound: roundIndex + 1
});
devLog(`[fillGroups] Match erstellt: ${match.id} - Spieler ${p1Id} vs ${p2Id} in Gruppe ${g.id}`);
} else {
console.warn(`Spieler gehören nicht zur gleichen Gruppe: ${p1Id} (${p1?.groupId}) vs ${p2Id} (${p2?.groupId}) in Gruppe ${g.id}`);
}

View File

@@ -0,0 +1,7 @@
Spielplan 2025/2026 - Test Liga
1. 15.01.2025 19:00 Team Alpha vs Team Beta code: ABC123 home pin: PIN001 guest pin: PIN002
2. 22.01.2025 20:00 Team Gamma gegen Team Delta code: DEF456 heim pin: PIN003 gast pin: PIN004
3. 29.01.2025 18:30 Team Epsilon - Team Zeta code: GHI789 home pin: PIN005 guest pin: PIN006
4. 05.02.2025 19:30 Team Alpha vs Team Gamma code: JKL012 home pin: PIN007 guest pin: PIN008
5. 12.02.2025 20:00 Team Beta gegen Team Delta code: MNO345 heim pin: PIN009 gast pin: PIN010

View File

@@ -42,7 +42,6 @@ export const getUserByToken = async (token) => {
export const hasUserClubAccess = async (userId, clubId) => {
try {
devLog('[hasUserClubAccess]');
const userClub = await UserClub.findOne({
where: {
user_id: userId,
@@ -62,11 +61,9 @@ export const checkAccess = async (userToken, clubId) => {
const user = await getUserByToken(userToken);
const hasAccess = await hasUserClubAccess(user.id, clubId);
if (!hasAccess) {
devLog('[checkAccess] - no club access');
throw new HttpError('noaccess', 403);
}
} catch (error) {
devLog('[checkAccess] - error:', error);
throw error;
}
};
@@ -76,7 +73,6 @@ export const checkGlobalAccess = async (userToken) => {
const user = await getUserByToken(userToken);
return user; // Einfach den User zurückgeben, da globale Zugriffe nur Authentifizierung benötigen
} catch (error) {
devLog('[checkGlobalAccess] - error:', error);
throw error;
}
};

View File

@@ -65,6 +65,10 @@
<span class="nav-icon"></span>
Vordefinierte Aktivitäten
</a>
<a href="/team-management" class="nav-link">
<span class="nav-icon">👥</span>
Team-Verwaltung
</a>
</div>
</nav>
@@ -170,7 +174,7 @@ export default {
},
loadClub() {
this.setCurrentClub(this.currentClub);
this.setCurrentClub(this.selectedClub);
this.$router.push('/training-stats');
},

View File

@@ -110,7 +110,6 @@ export default {
this.config.canvas.width = this.width || 600;
this.config.canvas.height = this.height || 400;
this.$nextTick(() => {
console.log('CourtDrawingRender: mounted with drawingData =', this.drawingData);
this.init();
this.redraw();
});
@@ -120,11 +119,9 @@ export default {
this.canvas = this.$refs.canvas;
if (this.canvas) {
this.ctx = this.canvas.getContext('2d');
console.log('CourtDrawingRender: canvas/context initialized');
}
},
redraw() {
console.log('CourtDrawingRender: redraw called with data =', this.drawingData);
if (!this.ctx) return;
const { width, height } = this.config.canvas;
// clear and background

View File

@@ -344,7 +344,6 @@ export default {
this.loadDrawingFromMetadata();
} else if (oldVal && !newVal) {
// drawingData wurde auf null gesetzt - reset alle Werte und zeichne leeres Canvas
console.log('CourtDrawingTool: drawingData set to null, resetting all values');
this.resetAllValues();
this.clearCanvas();
this.drawCourt(true); // forceRedraw = true
@@ -354,7 +353,6 @@ export default {
}
},
mounted() {
console.log('CourtDrawingTool: mounted');
this.$nextTick(() => {
this.initCanvas();
this.drawCourt();
@@ -365,12 +363,9 @@ export default {
},
methods: {
initCanvas() {
console.log('CourtDrawingTool: initCanvas called');
this.canvas = this.$refs.drawingCanvas;
console.log('CourtDrawingTool: canvas =', this.canvas);
if (this.canvas) {
this.ctx = this.canvas.getContext('2d');
console.log('CourtDrawingTool: ctx =', this.ctx);
this.ctx.lineCap = this.config.pen.cap;
this.ctx.lineJoin = this.config.pen.join;
} else {
@@ -379,7 +374,6 @@ export default {
},
drawCourt(forceRedraw = false) {
console.log('CourtDrawingTool: drawCourt called, forceRedraw:', forceRedraw);
const ctx = this.ctx;
const canvas = this.canvas;
const config = this.config;
@@ -389,8 +383,6 @@ export default {
return;
}
console.log('CourtDrawingTool: Drawing court...');
console.log('Canvas dimensions:', canvas.width, 'x', canvas.height);
// Hintergrund immer zeichnen wenn forceRedraw=true, sonst nur wenn Canvas leer ist
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
@@ -400,9 +392,7 @@ export default {
// Hintergrund
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
console.log('Background drawn');
} else {
console.log('Canvas not empty, skipping background');
}
// Tischtennis-Tisch
@@ -411,8 +401,6 @@ export default {
const tableX = (canvas.width - tableWidth) / 2;
const tableY = (canvas.height - tableHeight) / 2;
console.log('Table dimensions:', tableWidth, 'x', tableHeight);
console.log('Table position:', tableX, ',', tableY);
// Tischtennis-Tisch Hintergrund
ctx.fillStyle = config.table.color;
@@ -1074,9 +1062,6 @@ export default {
},
testDraw() {
console.log('CourtDrawingTool: testDraw called');
console.log('Canvas:', this.canvas);
console.log('Context:', this.ctx);
if (!this.canvas || !this.ctx) {
console.error('Canvas or context not available, trying to reinitialize...');
@@ -1084,7 +1069,6 @@ export default {
}
if (this.canvas && this.ctx) {
console.log('Drawing simple test...');
// Einfacher Test: Roter Kreis
this.ctx.fillStyle = 'red';
@@ -1092,18 +1076,15 @@ export default {
this.ctx.arc(300, 200, 50, 0, 2 * Math.PI);
this.ctx.fill();
console.log('Red circle drawn');
} else {
console.error('Still no canvas or context available');
}
},
async saveDrawing() {
console.log('CourtDrawingTool: saveDrawing called');
async saveDrawing() {
try {
const dataURL = this.canvas.toDataURL('image/png');
console.log('CourtDrawingTool: dataURL created, length:', dataURL.length);
this.$emit('input', dataURL);
@@ -1121,7 +1102,6 @@ export default {
timestamp: new Date().toISOString()
};
console.log('CourtDrawingTool: drawingData created:', drawingData);
// Immer Metadaten nach oben geben
this.$emit('update-drawing-data', drawingData);
@@ -1129,18 +1109,12 @@ export default {
// Konvertiere DataURL zu Blob für Upload
const response = await fetch(dataURL);
const blob = await response.blob();
console.log('CourtDrawingTool: blob created, size:', blob.size);
// Erstelle File-Objekt
const file = new File([blob], `exercise-${Date.now()}.png`, { type: 'image/png' });
console.log('CourtDrawingTool: file created:', file);
console.log('CourtDrawingTool: file type:', file.type);
console.log('CourtDrawingTool: file size:', file.size);
// Emittiere das File und die Zeichnungsdaten für Upload
console.log('CourtDrawingTool: emitting upload-image event');
this.$emit('upload-image', file, drawingData);
console.log('CourtDrawingTool: upload-image event emitted');
} else {
// Kein Bild-Upload mehr: gebe lediglich die Zeichnungsdaten an den Parent weiter,
// damit Felder (Kürzel/Name/Beschreibung) gefüllt werden können
@@ -1173,7 +1147,6 @@ export default {
},
resetAllValues() {
console.log('CourtDrawingTool: Resetting all values to initial state');
this.selectedStartPosition = null;
this.selectedCirclePosition = null;
this.strokeType = null;
@@ -1189,7 +1162,6 @@ export default {
loadDrawingFromMetadata() {
if (this.drawingData) {
console.log('CourtDrawingTool: Loading drawing from metadata:', this.drawingData);
// Lade alle Zeichnungsdaten
this.selectedStartPosition = this.drawingData.selectedStartPosition || null;
@@ -1213,7 +1185,7 @@ export default {
this.selectedCirclePosition = 'bottom';
}
console.log('CourtDrawingTool: Loaded values:', {
this.$emit('drawing-data', {
selectedStartPosition: this.selectedStartPosition,
selectedCirclePosition: this.selectedCirclePosition,
strokeType: this.strokeType,
@@ -1229,7 +1201,6 @@ export default {
this.drawCourt();
});
console.log('CourtDrawingTool: Drawing loaded from metadata');
}
},

View File

@@ -0,0 +1,299 @@
<template>
<div class="season-selector">
<label>
<span>Saison:</span>
<div class="season-input-group">
<select v-model="selectedSeasonId" @change="onSeasonChange" class="season-select" :disabled="loading">
<option value="">{{ loading ? 'Lade...' : 'Saison wählen...' }}</option>
<option v-for="season in seasons" :key="season.id" :value="season.id">
{{ season.season }}
</option>
</select>
<button @click="showNewSeasonForm = !showNewSeasonForm" class="btn-add-season" title="Neue Saison hinzufügen">
{{ showNewSeasonForm ? '✕' : '+' }}
</button>
</div>
</label>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="showNewSeasonForm" class="new-season-form">
<label>
<span>Neue Saison:</span>
<input
type="text"
v-model="newSeasonString"
placeholder="z.B. 2023/2024"
@keyup.enter="createSeason"
class="season-input"
>
</label>
<div class="form-actions">
<button @click="createSeason" :disabled="!isValidSeasonFormat" class="btn-create">
Erstellen
</button>
<button @click="cancelNewSeason" class="btn-cancel">
Abbrechen
</button>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, watch } from 'vue';
import { useStore } from 'vuex';
import apiClient from '../apiClient.js';
export default {
name: 'SeasonSelector',
props: {
modelValue: {
type: [String, Number],
default: null
},
showCurrentSeason: {
type: Boolean,
default: true
}
},
emits: ['update:modelValue', 'season-change'],
setup(props, { emit }) {
const store = useStore();
// Reactive data
const seasons = ref([]);
const selectedSeasonId = ref(props.modelValue);
const showNewSeasonForm = ref(false);
const newSeasonString = ref('');
const loading = ref(false);
const error = ref(null);
// Computed
const isValidSeasonFormat = computed(() => {
const seasonRegex = /^\d{4}\/\d{4}$/;
return seasonRegex.test(newSeasonString.value);
});
// Methods
const loadSeasons = async () => {
loading.value = true;
error.value = null;
try {
const response = await apiClient.get('/seasons');
seasons.value = response.data;
// Wenn showCurrentSeason true ist und keine Saison ausgewählt, wähle die aktuelle
if (props.showCurrentSeason && !selectedSeasonId.value && seasons.value.length > 0) {
// Die erste Saison ist die neueste (sortiert nach DESC)
selectedSeasonId.value = seasons.value[0].id;
emit('update:modelValue', selectedSeasonId.value);
emit('season-change', seasons.value[0]);
}
} catch (err) {
console.error('Fehler beim Laden der Saisons:', err);
error.value = 'Fehler beim Laden der Saisons';
} finally {
loading.value = false;
}
};
const onSeasonChange = () => {
const selectedSeason = seasons.value.find(s => s.id == selectedSeasonId.value);
emit('update:modelValue', selectedSeasonId.value);
emit('season-change', selectedSeason);
};
const createSeason = async () => {
if (!isValidSeasonFormat.value) return;
try {
const response = await apiClient.post('/seasons', {
season: newSeasonString.value
});
const newSeason = response.data;
seasons.value.unshift(newSeason); // Am Anfang einfügen (neueste zuerst)
selectedSeasonId.value = newSeason.id;
emit('update:modelValue', selectedSeasonId.value);
emit('season-change', newSeason);
// Formular zurücksetzen
newSeasonString.value = '';
showNewSeasonForm.value = false;
} catch (error) {
console.error('Fehler beim Erstellen der Saison:', error);
if (error.response?.data?.error === 'alreadyexists') {
alert('Diese Saison existiert bereits!');
} else {
alert('Fehler beim Erstellen der Saison');
}
}
};
const cancelNewSeason = () => {
newSeasonString.value = '';
showNewSeasonForm.value = false;
};
// Watch for prop changes
watch(() => props.modelValue, (newValue) => {
selectedSeasonId.value = newValue;
});
// Lifecycle
onMounted(() => {
loadSeasons();
});
return {
seasons,
selectedSeasonId,
showNewSeasonForm,
newSeasonString,
loading,
error,
isValidSeasonFormat,
onSeasonChange,
createSeason,
cancelNewSeason
};
}
};
</script>
<style scoped>
.season-selector {
margin-bottom: 1rem;
}
.season-selector label {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.season-selector label span {
font-weight: 600;
color: var(--text-color);
}
.season-input-group {
display: flex;
gap: 0.5rem;
align-items: center;
}
.season-select {
flex: 1;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-small);
font-size: 1rem;
background: white;
}
.season-select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.btn-add-season {
background: var(--primary-color);
color: white;
border: none;
border-radius: var(--border-radius-small);
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: bold;
transition: var(--transition);
}
.btn-add-season:hover {
background: var(--primary-dark);
}
.new-season-form {
background: var(--background-light);
padding: 1rem;
border-radius: var(--border-radius);
border: 1px solid var(--border-color);
margin-top: 0.5rem;
}
.new-season-form label {
margin-bottom: 1rem;
}
.season-input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-small);
font-size: 1rem;
}
.season-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px var(--primary-light);
}
.form-actions {
display: flex;
gap: 0.5rem;
}
.btn-create {
background: var(--primary-color);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: var(--border-radius-small);
cursor: pointer;
font-weight: 600;
transition: var(--transition);
}
.btn-create:hover:not(:disabled) {
background: var(--primary-dark);
}
.btn-create:disabled {
background: var(--text-muted);
cursor: not-allowed;
}
.btn-cancel {
background: var(--background-light);
color: var(--text-color);
border: 1px solid var(--border-color);
padding: 0.5rem 1rem;
border-radius: var(--border-radius-small);
cursor: pointer;
font-weight: 600;
transition: var(--transition);
}
.btn-cancel:hover {
background: var(--border-color);
}
.error-message {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.5rem;
padding: 0.5rem;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: var(--border-radius-small);
}
</style>

View File

@@ -14,6 +14,7 @@ import TrainingStatsView from './views/TrainingStatsView.vue';
import PredefinedActivities from './views/PredefinedActivities.vue';
import OfficialTournaments from './views/OfficialTournaments.vue';
import MyTischtennisAccount from './views/MyTischtennisAccount.vue';
import TeamManagementView from './views/TeamManagementView.vue';
import Impressum from './views/Impressum.vue';
import Datenschutz from './views/Datenschutz.vue';
@@ -33,6 +34,7 @@ const routes = [
{ path: '/predefined-activities', component: PredefinedActivities },
{ path: '/official-tournaments', component: OfficialTournaments },
{ path: '/mytischtennis-account', component: MyTischtennisAccount },
{ path: '/team-management', component: TeamManagementView },
{ path: '/impressum', component: Impressum },
{ path: '/datenschutz', component: Datenschutz },
];

View File

@@ -36,17 +36,10 @@
<div class="button-group">
<button class="btn-primary" @click="openEditDialog">Account bearbeiten</button>
<button class="btn-secondary" @click="testConnection">Verbindung testen</button>
<button class="btn-secondary" @click="testLoginFlow">Test: Login-Flow</button>
<button class="btn-secondary" @click="testConnection">Erneut einloggen</button>
<button class="btn-danger" @click="deleteAccount">Account trennen</button>
</div>
</div>
<!-- Test-Ausgabe -->
<div v-if="testResult" class="test-result" :class="testResult.type">
<h3>Test-Ergebnis:</h3>
<pre>{{ testResult.data }}</pre>
</div>
</div>
<div v-else class="no-account">
@@ -89,8 +82,7 @@ export default {
return {
loading: true,
account: null,
showDialog: false,
testResult: null
showDialog: false
};
},
mounted() {
@@ -131,16 +123,15 @@ export default {
},
async testConnection() {
this.testResult = null;
try {
await apiClient.post('/mytischtennis/verify');
this.$store.dispatch('showMessage', {
text: 'Verbindung erfolgreich! Login funktioniert.',
text: 'Login erfolgreich! Verbindungsdaten aktualisiert.',
type: 'success'
});
await this.loadAccount(); // Aktualisiere lastLoginSuccess
await this.loadAccount(); // Aktualisiere Account-Daten inkl. clubId, fedNickname
} catch (error) {
const message = error.response?.data?.message || 'Verbindung fehlgeschlagen';
const message = error.response?.data?.message || 'Login fehlgeschlagen';
if (error.response?.status === 400 && message.includes('Kein Passwort gespeichert')) {
// Passwort-Dialog öffnen
@@ -154,71 +145,6 @@ export default {
}
},
async testLoginFlow() {
this.testResult = null;
try {
// 1. Verify Login
console.log('Testing login...');
const verifyResponse = await apiClient.post('/mytischtennis/verify');
console.log('Login successful:', verifyResponse.data);
// 2. Get Session
console.log('Fetching session...');
const sessionResponse = await apiClient.get('/mytischtennis/session');
console.log('Session data:', sessionResponse.data);
// 3. Check Status
console.log('Checking status...');
const statusResponse = await apiClient.get('/mytischtennis/status');
console.log('Status:', statusResponse.data);
this.testResult = {
type: 'success',
data: {
message: 'Alle Tests erfolgreich!',
login: {
accessToken: verifyResponse.data.accessToken ? '✓ vorhanden' : '✗ fehlt',
expiresAt: verifyResponse.data.expiresAt,
clubId: verifyResponse.data.clubId || '✗ nicht gefunden',
clubName: verifyResponse.data.clubName || '✗ nicht gefunden'
},
session: {
accessToken: sessionResponse.data.session?.accessToken ? '✓ vorhanden' : '✗ fehlt',
refreshToken: sessionResponse.data.session?.refreshToken ? '✓ vorhanden' : '✗ fehlt',
cookie: sessionResponse.data.session?.cookie ? '✓ vorhanden' : '✗ fehlt',
userData: sessionResponse.data.session?.userData ? '✓ vorhanden' : '✗ fehlt',
expiresAt: sessionResponse.data.session?.expiresAt
},
status: statusResponse.data
}
};
this.$store.dispatch('showMessage', {
text: 'Test erfolgreich! Details siehe unten.',
type: 'success'
});
} catch (error) {
console.error('Test failed:', error);
this.testResult = {
type: 'error',
data: {
message: 'Test fehlgeschlagen',
error: error.response?.data?.message || error.message,
status: error.response?.status,
details: error.response?.data
}
};
this.$store.dispatch('showMessage', {
text: `Test fehlgeschlagen: ${error.response?.data?.message || error.message}`,
type: 'error'
});
}
},
async deleteAccount() {
if (!confirm('Möchten Sie die Verknüpfung zum myTischtennis-Account wirklich trennen?')) {
return;
@@ -384,40 +310,5 @@ h1 {
.btn-danger:hover {
background-color: #c82333;
}
.test-result {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-top: 1rem;
}
.test-result.success {
border-left: 4px solid #28a745;
}
.test-result.error {
border-left: 4px solid #dc3545;
}
.test-result h3 {
margin-top: 0;
margin-bottom: 1rem;
color: #333;
}
.test-result pre {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 1rem;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
</style>

View File

@@ -179,7 +179,6 @@ export default {
const r = await apiClient.get(`/predefined-activities/${this.editModel.id}`);
const { images } = r.data;
this.images = images || [];
console.log('Images reloaded:', this.images);
} catch (error) {
console.error('Error reloading images:', error);
}
@@ -218,7 +217,6 @@ export default {
async save() {
if (!this.editModel) return;
console.log('Save: selectedFile =', this.selectedFile);
if (this.editModel.id) {
const { id, ...payload } = this.editModel;
@@ -234,10 +232,8 @@ export default {
// Nach dem Speichern (sowohl CREATE als auch UPDATE): Bild hochladen falls vorhanden
if (this.selectedFile) {
console.log('Uploading image after save...');
await this.uploadImage();
} else {
console.log('No selectedFile to upload');
}
await this.reload();
@@ -250,16 +246,9 @@ export default {
},
async uploadImage() {
if (!this.editModel || !this.editModel.id || !this.selectedFile) {
console.log('Upload skipped: editModel=', this.editModel, 'selectedFile=', this.selectedFile);
return;
}
console.log('Starting image upload...');
console.log('editModel:', this.editModel);
console.log('selectedActivity:', this.selectedActivity);
console.log('Activity ID (editModel.id):', this.editModel.id);
console.log('Activity ID (selectedActivity.id):', this.selectedActivity?.id);
console.log('File:', this.selectedFile);
const fd = new FormData();
fd.append('image', this.selectedFile);
@@ -267,20 +256,17 @@ export default {
// Füge Zeichnungsdaten hinzu, falls vorhanden
if (this.selectedDrawingData) {
fd.append('drawingData', JSON.stringify(this.selectedDrawingData));
console.log('Added drawingData to FormData:', this.selectedDrawingData);
}
// Verwende PUT für Updates, POST für neue Activities
const isUpdate = this.selectedActivity && this.selectedActivity.id === this.editModel.id;
const method = isUpdate ? 'put' : 'post';
console.log('Using method:', method);
try {
const response = await apiClient[method](`/predefined-activities/${this.editModel.id}/image`, fd, {
headers: { 'Content-Type': 'multipart/form-data' }
});
console.log('Upload successful:', response);
// Nach Upload Details neu laden
await this.select(this.editModel);
@@ -334,24 +320,15 @@ export default {
},
async onDrawingImageUpload(file, drawingData) {
console.log('onDrawingImageUpload called with file:', file);
console.log('onDrawingImageUpload called with drawingData:', drawingData);
console.log('File type:', file?.type);
console.log('File size:', file?.size);
console.log('File name:', file?.name);
// Setze das File und die Zeichnungsdaten für den Upload
this.selectedFile = file;
this.selectedDrawingData = drawingData;
console.log('selectedFile set to:', this.selectedFile);
console.log('selectedDrawingData set to:', this.selectedDrawingData);
// Upload wird erst beim Speichern durchgeführt
console.log('File and drawing data ready for upload when saving');
},
async onImageUploaded() {
console.log('Image uploaded successfully, refreshing image list...');
// Bildliste aktualisieren
if (this.editModel && this.editModel.id) {
await this.select(this.editModel);

View File

@@ -1,6 +1,13 @@
<template>
<div>
<h2>Spielpläne</h2>
<SeasonSelector
v-model="selectedSeasonId"
@season-change="onSeasonChange"
:show-current-season="true"
/>
<button @click="openImportModal">Spielplanimport</button>
<div v-if="hoveredMatch && hoveredMatch.location" class="hover-info">
<p><strong>{{ hoveredMatch.location.name || 'N/A' }}</strong></p>
@@ -12,8 +19,9 @@
<li class="special-link" @click="loadAllMatches">Gesamtspielplan</li>
<li class="special-link" @click="loadAdultMatches">Spielplan Erwachsene</li>
<li class="divider"></li>
<li v-for="league in leagues" :key="league" @click="loadMatchesForLeague(league.id, league.name)">{{
<li v-for="league in leagues" :key="league.id" @click="loadMatchesForLeague(league.id, league.name)">{{
league.name }}</li>
<li v-if="leagues.length === 0" class="no-leagues">Keine Ligen für diese Saison gefunden</li>
</ul>
<div class="flex-item">
<button @click="generatePDF">Download PDF</button>
@@ -27,6 +35,9 @@
<th>Heimmannschaft</th>
<th>Gastmannschaft</th>
<th v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">Altersklasse</th>
<th>Code</th>
<th>Heim-PIN</th>
<th>Gast-PIN</th>
</tr>
</thead>
<tbody>
@@ -37,6 +48,18 @@
<td v-html="highlightClubName(match.homeTeam?.name || 'N/A')"></td>
<td v-html="highlightClubName(match.guestTeam?.name || 'N/A')"></td>
<td v-if="selectedLeague === 'Gesamtspielplan' || selectedLeague === 'Spielplan Erwachsene'">{{ match.leagueDetails?.name || 'N/A' }}</td>
<td class="code-cell">
<span v-if="match.code" class="code-value clickable" @click="copyToClipboard(match.code, 'Code')" :title="'Code kopieren: ' + match.code">{{ match.code }}</span>
<span v-else class="no-data">-</span>
</td>
<td class="pin-cell">
<span v-if="match.homePin" class="pin-value clickable" @click="copyToClipboard(match.homePin, 'Heim-PIN')" :title="'Heim-PIN kopieren: ' + match.homePin">{{ match.homePin }}</span>
<span v-else class="no-data">-</span>
</td>
<td class="pin-cell">
<span v-if="match.guestPin" class="pin-value clickable" @click="copyToClipboard(match.guestPin, 'Gast-PIN')" :title="'Gast-PIN kopieren: ' + match.guestPin">{{ match.guestPin }}</span>
<span v-else class="no-data">-</span>
</td>
</tr>
</tbody>
</table>
@@ -65,9 +88,13 @@
import { mapGetters } from 'vuex';
import apiClient from '../apiClient.js';
import PDFGenerator from '../components/PDFGenerator.js';
import SeasonSelector from '../components/SeasonSelector.vue';
export default {
name: 'ScheduleView',
components: {
SeasonSelector
},
computed: {
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName']),
},
@@ -79,6 +106,8 @@ export default {
matches: [],
selectedLeague: '',
hoveredMatch: null,
selectedSeasonId: null,
currentSeason: null,
};
},
methods: {
@@ -174,12 +203,21 @@ export default {
async loadLeagues() {
try {
const clubId = this.currentClub;
const response = await apiClient.get(`/matches/leagues/current/${clubId}`);
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/current/${clubId}${seasonParam}`);
this.leagues = this.sortLeagues(response.data);
} catch (error) {
console.error('ScheduleView: Error loading leagues:', error);
alert('Fehler beim Laden der Ligen');
}
},
onSeasonChange(season) {
this.currentSeason = season;
this.loadLeagues();
// Leere die aktuellen Matches, da sie für eine andere Saison sind
this.matches = [];
this.selectedLeague = '';
},
async loadMatchesForLeague(leagueId, leagueName) {
this.selectedLeague = leagueName;
try {
@@ -193,7 +231,8 @@ export default {
async loadAllMatches() {
this.selectedLeague = 'Gesamtspielplan';
try {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches`);
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches${seasonParam}`);
this.matches = response.data;
} catch (error) {
alert('Fehler beim Laden des Gesamtspielplans');
@@ -203,7 +242,8 @@ export default {
async loadAdultMatches() {
this.selectedLeague = 'Spielplan Erwachsene';
try {
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches`);
const seasonParam = this.selectedSeasonId ? `?seasonid=${this.selectedSeasonId}` : '';
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches${seasonParam}`);
// Filtere nur Erwachsenenligen (keine Jugendligen)
const allMatches = response.data;
this.matches = allMatches.filter(match => {
@@ -301,9 +341,35 @@ export default {
return ''; // Keine besondere Farbe
},
async copyToClipboard(text, type) {
try {
await navigator.clipboard.writeText(text);
// Zeige eine kurze Bestätigung
const originalText = event.target.textContent;
event.target.textContent = '✓';
event.target.style.color = '#4CAF50';
setTimeout(() => {
event.target.textContent = originalText;
event.target.style.color = '';
}, 1000);
} catch (err) {
console.error('Fehler beim Kopieren:', err);
// Fallback für ältere Browser
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
},
},
async created() {
await this.loadLeagues();
// Ligen werden geladen, sobald eine Saison ausgewählt ist
// Die SeasonSelector-Komponente wird automatisch die aktuelle Saison auswählen
// und dann onSeasonChange aufrufen, was loadLeagues() triggert
}
};
</script>
@@ -428,6 +494,12 @@ li {
transition: all 0.3s ease;
}
.no-leagues {
color: #666;
font-style: italic;
padding: 0.5rem;
}
.divider {
height: 1px;
background-color: #ddd;
@@ -436,6 +508,61 @@ li {
color: transparent !important;
}
/* Code und PIN Styles */
.code-cell, .pin-cell {
text-align: center;
font-family: 'Courier New', monospace;
font-weight: bold;
min-width: 80px;
}
.code-value {
background: #e3f2fd;
color: #1976d2;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9rem;
display: inline-block;
border: 1px solid #bbdefb;
}
.code-value.clickable {
cursor: pointer;
transition: all 0.2s ease;
}
.code-value.clickable:hover {
background: #bbdefb;
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.pin-value {
background: #fff3e0;
color: #f57c00;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.9rem;
display: inline-block;
border: 1px solid #ffcc02;
}
.pin-value.clickable {
cursor: pointer;
transition: all 0.2s ease;
}
.pin-value.clickable:hover {
background: #ffcc02;
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.no-data {
color: #999;
font-style: italic;
}
.match-today {
background-color: #fff3cd !important; /* Gelb für heute */
}

File diff suppressed because it is too large Load Diff

View File

@@ -586,7 +586,6 @@ export default {
try {
const d = await apiClient.get(`/tournament/${this.currentClub}`);
this.dates = d.data;
console.log('Loaded tournaments:', this.dates);
// Prüfe, ob es einen Trainingstag heute gibt
await this.checkTrainingToday();
@@ -601,7 +600,6 @@ export default {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD Format
const response = await apiClient.get(`/diary/${this.currentClub}`);
console.log('Training check response:', response.data);
// Die API gibt alle Trainingstage zurück, filtere nach heute
const trainingData = response.data;
@@ -610,7 +608,6 @@ export default {
} else {
this.hasTrainingToday = false;
}
console.log('Training today:', this.hasTrainingToday);
} catch (error) {
console.error('Fehler beim Prüfen des Trainingstags:', error);
this.hasTrainingToday = false;
@@ -625,7 +622,6 @@ export default {
date: this.newDate
});
console.log('Tournament created, response:', r.data);
// Speichere die ID des neuen Turniers
const newTournamentId = r.data.id;
@@ -982,7 +978,6 @@ export default {
},
highlightMatch(player1Id, player2Id, groupId) {
console.log('highlightMatch called:', { player1Id, player2Id, groupId });
// Finde das entsprechende Match (auch unbeendete)
const match = this.matches.find(m =>
@@ -992,17 +987,14 @@ export default {
(m.player1.id === player2Id && m.player2.id === player1Id))
);
console.log('Found match:', match);
if (!match) {
console.log('No match found');
return;
}
// Setze Highlight-Klasse
this.$nextTick(() => {
const matchElement = document.querySelector(`tr[data-match-id="${match.id}"]`);
console.log('Match element:', matchElement);
if (matchElement) {
// Entferne vorherige Highlights
@@ -1019,7 +1011,6 @@ export default {
block: 'center'
});
} else {
console.log('Match element not found in DOM');
}
});
},
@@ -1083,23 +1074,18 @@ export default {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD Format
const response = await apiClient.get(`/diary/${this.currentClub}`);
console.log('Training response:', response.data);
console.log('Looking for date:', today);
// Die API gibt alle Trainingstage zurück, filtere nach heute
const trainingData = response.data;
if (Array.isArray(trainingData)) {
console.log('Available training dates:', trainingData.map(t => t.date));
// Finde den Trainingstag für heute
const todayTraining = trainingData.find(training => training.date === today);
console.log('Today training found:', todayTraining);
if (todayTraining) {
// Lade die Teilnehmer für diesen Trainingstag über die Participant-API
const participantsResponse = await apiClient.get(`/participants/${todayTraining.id}`);
console.log('Participants response:', participantsResponse.data);
const participants = participantsResponse.data;