Compare commits
24 Commits
multifunct
...
e2e8ba0d54
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2e8ba0d54 | ||
|
|
63a5db1196 | ||
|
|
3ac4039097 | ||
|
|
46079fc16e | ||
|
|
e38f2c2e19 | ||
|
|
d190c2cb70 | ||
|
|
fd0b3020bf | ||
|
|
8e4edfb719 | ||
|
|
01f80ea094 | ||
|
|
8140283b7a | ||
|
|
48f0f9afb0 | ||
|
|
ad778328e8 | ||
|
|
91006bb0c5 | ||
|
|
110519c7b8 | ||
|
|
6afa5a5629 | ||
|
|
7bf7ae2bef | ||
|
|
51b620f496 | ||
|
|
f83e3986b7 | ||
|
|
bb9a030e89 | ||
|
|
a767061bd3 | ||
|
|
35031e3ae5 | ||
|
|
370df07c2a | ||
|
|
94385231d3 | ||
|
|
5357dac9e1 |
0
.cursor/commands/club-settings.md
Normal file → Executable file
0
.cursor/commands/club-settings.md
Normal file → Executable file
0
.cursor/commands/oldfkchecks.md
Normal file → Executable file
0
.cursor/commands/oldfkchecks.md
Normal file → Executable file
0
.gitea/workflows/deploy.yml
Normal file → Executable file
0
.gitea/workflows/deploy.yml
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
CHECK_SERVER.md
Normal file → Executable file
0
CHECK_SERVER.md
Normal file → Executable file
0
CODEx_MIGRATION_RULES.md
Normal file → Executable file
0
CODEx_MIGRATION_RULES.md
Normal file → Executable file
0
DEPLOYMENT_SOCKET_IO.md
Normal file → Executable file
0
DEPLOYMENT_SOCKET_IO.md
Normal file → Executable file
0
DSGVO_CHECKLIST.md
Normal file → Executable file
0
DSGVO_CHECKLIST.md
Normal file → Executable file
0
PERMISSIONS_GUIDE.md
Normal file → Executable file
0
PERMISSIONS_GUIDE.md
Normal file → Executable file
0
PERMISSIONS_MIGRATION.md
Normal file → Executable file
0
PERMISSIONS_MIGRATION.md
Normal file → Executable file
0
SERVER_NODE_UPGRADE.md
Normal file → Executable file
0
SERVER_NODE_UPGRADE.md
Normal file → Executable file
0
SITEMAP_ANLEITUNG.md
Normal file → Executable file
0
SITEMAP_ANLEITUNG.md
Normal file → Executable file
0
TrainingsTagebuch.code-workspace
Normal file → Executable file
0
TrainingsTagebuch.code-workspace
Normal file → Executable file
0
apache-http.conf.example
Normal file → Executable file
0
apache-http.conf.example
Normal file → Executable file
0
apache-https.conf.example
Normal file → Executable file
0
apache-https.conf.example
Normal file → Executable file
0
apache.conf.example
Normal file → Executable file
0
apache.conf.example
Normal file → Executable file
0
backend/CLICKTT_HTTV_README.md
Normal file → Executable file
0
backend/CLICKTT_HTTV_README.md
Normal file → Executable file
0
backend/MYTISCHTENNIS_AUTO_FETCH_README.md
Normal file → Executable file
0
backend/MYTISCHTENNIS_AUTO_FETCH_README.md
Normal file → Executable file
0
backend/MYTISCHTENNIS_URL_PARSER_README.md
Normal file → Executable file
0
backend/MYTISCHTENNIS_URL_PARSER_README.md
Normal file → Executable file
0
backend/README_CLEANUP.md
Normal file → Executable file
0
backend/README_CLEANUP.md
Normal file → Executable file
0
backend/SOCKET_IO_SSL_SETUP.md
Normal file → Executable file
0
backend/SOCKET_IO_SSL_SETUP.md
Normal file → Executable file
0
backend/checkRealIndexes.sql
Normal file → Executable file
0
backend/checkRealIndexes.sql
Normal file → Executable file
0
backend/checkTableNames.sql
Normal file → Executable file
0
backend/checkTableNames.sql
Normal file → Executable file
0
backend/cleanupKeys.sql
Normal file → Executable file
0
backend/cleanupKeys.sql
Normal file → Executable file
0
backend/cleanupKeysAggressive.sql
Normal file → Executable file
0
backend/cleanupKeysAggressive.sql
Normal file → Executable file
0
backend/cleanupKeysCorrected.sql
Normal file → Executable file
0
backend/cleanupKeysCorrected.sql
Normal file → Executable file
0
backend/cleanupKeysFinal.sql
Normal file → Executable file
0
backend/cleanupKeysFinal.sql
Normal file → Executable file
0
backend/cleanupKeysIntelligent.sql
Normal file → Executable file
0
backend/cleanupKeysIntelligent.sql
Normal file → Executable file
0
backend/cleanupKeysMinimal.sql
Normal file → Executable file
0
backend/cleanupKeysMinimal.sql
Normal file → Executable file
0
backend/cleanupKeysNode.cjs
Normal file → Executable file
0
backend/cleanupKeysNode.cjs
Normal file → Executable file
0
backend/cleanupKeysNode.js
Normal file → Executable file
0
backend/cleanupKeysNode.js
Normal file → Executable file
0
backend/cleanupKeysReal.sql
Normal file → Executable file
0
backend/cleanupKeysReal.sql
Normal file → Executable file
0
backend/cleanupKeysSimple.sql
Normal file → Executable file
0
backend/cleanupKeysSimple.sql
Normal file → Executable file
0
backend/cleanupKeysSmart.sql
Normal file → Executable file
0
backend/cleanupKeysSmart.sql
Normal file → Executable file
0
backend/clients/myTischtennisClient.js
Normal file → Executable file
0
backend/clients/myTischtennisClient.js
Normal file → Executable file
0
backend/config.js
Normal file → Executable file
0
backend/config.js
Normal file → Executable file
0
backend/constants/ERROR_CODES_USAGE.md
Normal file → Executable file
0
backend/constants/ERROR_CODES_USAGE.md
Normal file → Executable file
0
backend/constants/errorCodes.js
Normal file → Executable file
0
backend/constants/errorCodes.js
Normal file → Executable file
0
backend/controllers/accidentController.js
Normal file → Executable file
0
backend/controllers/accidentController.js
Normal file → Executable file
0
backend/controllers/activityController.js
Normal file → Executable file
0
backend/controllers/activityController.js
Normal file → Executable file
0
backend/controllers/apiLogController.js
Normal file → Executable file
0
backend/controllers/apiLogController.js
Normal file → Executable file
0
backend/controllers/authController.js
Normal file → Executable file
0
backend/controllers/authController.js
Normal file → Executable file
0
backend/controllers/billingController.js
Normal file → Executable file
0
backend/controllers/billingController.js
Normal file → Executable file
0
backend/controllers/calendarController.js
Normal file → Executable file
0
backend/controllers/calendarController.js
Normal file → Executable file
0
backend/controllers/calendarEventController.js
Normal file → Executable file
0
backend/controllers/calendarEventController.js
Normal file → Executable file
0
backend/controllers/clickTtAccountController.js
Normal file → Executable file
0
backend/controllers/clickTtAccountController.js
Normal file → Executable file
0
backend/controllers/clubAccountController.js
Normal file → Executable file
0
backend/controllers/clubAccountController.js
Normal file → Executable file
0
backend/controllers/clubArchiveController.js
Normal file → Executable file
0
backend/controllers/clubArchiveController.js
Normal file → Executable file
0
backend/controllers/clubCommunicationController.js
Normal file → Executable file
0
backend/controllers/clubCommunicationController.js
Normal file → Executable file
2
backend/controllers/clubDashboardController.js
Normal file → Executable file
2
backend/controllers/clubDashboardController.js
Normal file → Executable file
@@ -610,7 +610,7 @@ export const getClubDashboard = async (req, res) => {
|
||||
title: 'Finanzen',
|
||||
value: paidRatio === null ? 'Keine Daten' : `${paidRatio} % erledigt`,
|
||||
meta: paymentClaims.length > 0 ? `${paymentClaims.length} offene oder teilweise offene Forderungen` : 'Noch keine Beitragsforderungen erfasst',
|
||||
to: '/club-tasks',
|
||||
to: '/club-payments',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
0
backend/controllers/clubDocumentController.js
Normal file → Executable file
0
backend/controllers/clubDocumentController.js
Normal file → Executable file
0
backend/controllers/clubInvoiceController.js
Normal file → Executable file
0
backend/controllers/clubInvoiceController.js
Normal file → Executable file
0
backend/controllers/clubPaymentClaimController.js
Normal file → Executable file
0
backend/controllers/clubPaymentClaimController.js
Normal file → Executable file
0
backend/controllers/clubRequestController.js
Normal file → Executable file
0
backend/controllers/clubRequestController.js
Normal file → Executable file
0
backend/controllers/clubStatisticsController.js
Normal file → Executable file
0
backend/controllers/clubStatisticsController.js
Normal file → Executable file
0
backend/controllers/clubTaskController.js
Normal file → Executable file
0
backend/controllers/clubTaskController.js
Normal file → Executable file
0
backend/controllers/clubTeamController.js
Normal file → Executable file
0
backend/controllers/clubTeamController.js
Normal file → Executable file
0
backend/controllers/clubVenueController.js
Normal file → Executable file
0
backend/controllers/clubVenueController.js
Normal file → Executable file
0
backend/controllers/clubsController.js
Normal file → Executable file
0
backend/controllers/clubsController.js
Normal file → Executable file
0
backend/controllers/diaryController.js
Normal file → Executable file
0
backend/controllers/diaryController.js
Normal file → Executable file
0
backend/controllers/diaryDateActivityController.js
Normal file → Executable file
0
backend/controllers/diaryDateActivityController.js
Normal file → Executable file
0
backend/controllers/diaryDateTagController.js
Normal file → Executable file
0
backend/controllers/diaryDateTagController.js
Normal file → Executable file
0
backend/controllers/diaryMemberActivityController.js
Normal file → Executable file
0
backend/controllers/diaryMemberActivityController.js
Normal file → Executable file
0
backend/controllers/diaryMemberController.js
Normal file → Executable file
0
backend/controllers/diaryMemberController.js
Normal file → Executable file
0
backend/controllers/diaryNoteController.js
Normal file → Executable file
0
backend/controllers/diaryNoteController.js
Normal file → Executable file
0
backend/controllers/diaryTagController.js
Normal file → Executable file
0
backend/controllers/diaryTagController.js
Normal file → Executable file
0
backend/controllers/friendlyMatchController.js
Normal file → Executable file
0
backend/controllers/friendlyMatchController.js
Normal file → Executable file
0
backend/controllers/friendlyMatchInvitationController.js
Normal file → Executable file
0
backend/controllers/friendlyMatchInvitationController.js
Normal file → Executable file
0
backend/controllers/friendlyMatchSharedController.js
Normal file → Executable file
0
backend/controllers/friendlyMatchSharedController.js
Normal file → Executable file
0
backend/controllers/groupController.js
Normal file → Executable file
0
backend/controllers/groupController.js
Normal file → Executable file
6
backend/controllers/matchController.js
Normal file → Executable file
6
backend/controllers/matchController.js
Normal file → Executable file
@@ -4,7 +4,7 @@ import { emitScheduleMatchUpdated } from '../services/socketService.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
export const uploadCSV = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.body;
|
||||
const clubId = req.body?.clubId ?? req.query?.clubId;
|
||||
const { authcode: userToken} = req.headers;
|
||||
const filePath = req.file.path;
|
||||
const result = await MatchService.importCSV(userToken, clubId, filePath);
|
||||
@@ -16,7 +16,9 @@ export const uploadCSV = async (req, res) => {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error importing CSV:', error);
|
||||
return res.status(500).json({ error: 'Failed to import CSV data' });
|
||||
return res.status(500).json({
|
||||
error: error.message || 'Failed to import CSV data'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
0
backend/controllers/memberActivityController.js
Normal file → Executable file
0
backend/controllers/memberActivityController.js
Normal file → Executable file
281
backend/controllers/memberController.js
Normal file → Executable file
281
backend/controllers/memberController.js
Normal file → Executable file
@@ -1,7 +1,20 @@
|
||||
import { Op } from 'sequelize';
|
||||
import MemberService from "../services/memberService.js";
|
||||
import MemberTransferService from "../services/memberTransferService.js";
|
||||
import clickTtPlayerRegistrationService from "../services/clickTtPlayerRegistrationService.js";
|
||||
import { emitMemberChanged } from '../services/socketService.js';
|
||||
import Member from '../models/Member.js';
|
||||
import UserClub from '../models/UserClub.js';
|
||||
import CalendarEvent from '../models/CalendarEvent.js';
|
||||
import TrainingGroup from '../models/TrainingGroup.js';
|
||||
import ClubCommunicationRecipient from '../models/ClubCommunicationRecipient.js';
|
||||
import ClubCommunicationThread from '../models/ClubCommunicationThread.js';
|
||||
import ClubCommunicationMessage from '../models/ClubCommunicationMessage.js';
|
||||
import ClubSepaMandate from '../models/ClubSepaMandate.js';
|
||||
import MemberProfileChangeRequest from '../models/MemberProfileChangeRequest.js';
|
||||
import MemberEventResponse from '../models/MemberEventResponse.js';
|
||||
import Participant from '../models/Participant.js';
|
||||
import DiaryDate from '../models/DiaryDates.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
const getClubMembers = async(req, res) => {
|
||||
@@ -351,6 +364,261 @@ const transferMembers = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
const linkMemberUser = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const memberId = Number(req.params.memberId);
|
||||
const userId = Number(req.body?.userId);
|
||||
if (!clubId || !memberId || !userId) return res.status(400).json({ error: 'Mitglied, Verein und Benutzer sind erforderlich.' });
|
||||
const [member, clubUser, existing] = await Promise.all([
|
||||
Member.findOne({ where: { id: memberId, clubId } }),
|
||||
UserClub.findOne({ where: { clubId, userId, approved: true } }),
|
||||
Member.findOne({ where: { userId } }),
|
||||
]);
|
||||
if (!member || !clubUser) return res.status(404).json({ error: 'Mitglied oder freigegebener Vereinsbenutzer wurde nicht gefunden.' });
|
||||
if (existing && Number(existing.id) !== memberId) return res.status(409).json({ error: 'Dieses Benutzerkonto ist bereits einem Mitglied zugeordnet.' });
|
||||
await member.update({ userId });
|
||||
emitMemberChanged(clubId);
|
||||
res.json({ memberId, userId });
|
||||
} catch (error) {
|
||||
console.error('[linkMemberUser] - Error:', error);
|
||||
res.status(500).json({ error: 'Mitglied konnte nicht verknüpft werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const getApprovedLinkedMember = async (clubId, userId) => {
|
||||
const clubUser = await UserClub.findOne({ where: { clubId, userId, approved: true } });
|
||||
if (!clubUser) return { error: 'Kein Zugriff auf diesen Verein.', status: 403 };
|
||||
const member = await Member.findOne({ where: { clubId, userId, active: true } });
|
||||
if (!member) return { error: 'Für dieses Konto ist kein aktives Mitgliedsprofil verknüpft.', status: 404 };
|
||||
return { member };
|
||||
};
|
||||
|
||||
const getMemberDashboard = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const userId = Number(req.user?.id);
|
||||
const access = await getApprovedLinkedMember(clubId, userId);
|
||||
if (access.error) return res.status(access.status).json({ error: access.error });
|
||||
const { member } = access;
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const [events, trainingGroups, recipients, sepaMandate, changeRequests, trainingDates] = await Promise.all([
|
||||
CalendarEvent.findAll({
|
||||
where: { clubId, startDate: { [Op.gte]: today }, status: { [Op.notIn]: ['cancelled', 'archived'] } },
|
||||
attributes: ['id', 'title', 'startDate', 'endDate', 'eventType', 'location', 'registrationDeadline'],
|
||||
order: [['startDate', 'ASC']], limit: 5,
|
||||
}),
|
||||
TrainingGroup.findAll({ where: { clubId }, include: [{ model: Member, as: 'members', where: { id: member.id }, attributes: [], through: { attributes: [] } }, { association: 'trainingTimes', required: false }], order: [['name', 'ASC']] }),
|
||||
ClubCommunicationRecipient.findAll({ where: { clubId, memberId: member.id, deliveryStatus: 'sent' }, include: [{ model: ClubCommunicationThread, as: 'thread', attributes: ['id', 'subject', 'sentAt'] }], order: [['deliveredAt', 'DESC']], limit: 5 }),
|
||||
ClubSepaMandate.findOne({ where: { clubId, memberId: member.id, status: 'active', revokedAt: null }, attributes: ['id'] }),
|
||||
MemberProfileChangeRequest.findAll({ where: { clubId, memberId: member.id, status: 'open' }, attributes: ['id', 'requestedChanges', 'createdAt'], order: [['createdAt', 'DESC']] }),
|
||||
DiaryDate.findAll({ where: { clubId, date: { [Op.gte]: today } }, include: [{ model: Member, as: 'participants', where: { id: member.id }, required: false, through: { attributes: ['attendanceStatus'] } }], attributes: ['id', 'date', 'trainingStart', 'trainingEnd'], order: [['date', 'ASC']], limit: 5 }),
|
||||
]);
|
||||
const responses = await MemberEventResponse.findAll({ where: { clubId, memberId: member.id, eventId: events.map((event) => event.id) } });
|
||||
const responseByEvent = new Map(responses.map((response) => [response.eventId, response.status]));
|
||||
res.json({
|
||||
profile: { id: member.id, firstName: member.firstName, lastName: member.lastName, email: member.email, phone: member.phone, street: member.street, postalCode: member.postalCode, city: member.city },
|
||||
membership: { status: member.membershipStatus, type: member.membershipType, joinedOn: member.joinedOn, needsSepaMandate: member.needsSepaMandate, hasActiveSepaMandate: Boolean(sepaMandate) },
|
||||
openProfileChangeRequests: changeRequests,
|
||||
upcomingEvents: events.map((event) => ({ ...event.toJSON(), responseStatus: responseByEvent.get(event.id) || null })),
|
||||
trainingGroups: trainingGroups.map((group) => ({ id: group.id, name: group.name, trainingTimes: group.trainingTimes || [] })),
|
||||
upcomingTrainingDates: trainingDates.map((date) => ({ id: date.id, date: date.date, trainingStart: date.trainingStart, trainingEnd: date.trainingEnd, attendanceStatus: date.participants?.[0]?.Participant?.attendanceStatus || 'present' })),
|
||||
messages: recipients.map((recipient) => ({ id: recipient.thread?.id || recipient.id, subject: recipient.thread?.subject || 'Vereinsnachricht', sentAt: recipient.deliveredAt || recipient.thread?.sentAt || null })),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[getMemberDashboard] - Error:', error);
|
||||
res.status(500).json({ error: 'Persönlicher Bereich konnte nicht geladen werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const createProfileChangeRequest = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const userId = Number(req.user?.id);
|
||||
const access = await getApprovedLinkedMember(clubId, userId);
|
||||
if (access.error) return res.status(access.status).json({ error: access.error });
|
||||
const allowedFields = ['email', 'phone', 'street', 'postalCode', 'city'];
|
||||
const requestedChanges = Object.fromEntries(allowedFields
|
||||
.filter((field) => typeof req.body?.[field] === 'string' && req.body[field].trim() !== '')
|
||||
.map((field) => [field, req.body[field].trim()]));
|
||||
if (!Object.keys(requestedChanges).length) return res.status(400).json({ error: 'Bitte mindestens ein gültiges Kontaktfeld angeben.' });
|
||||
const request = await MemberProfileChangeRequest.create({ clubId, memberId: access.member.id, requestedByUserId: userId, requestedChanges });
|
||||
res.status(201).json({ id: request.id, status: request.status, requestedChanges: request.requestedChanges, createdAt: request.createdAt });
|
||||
} catch (error) {
|
||||
console.error('[createProfileChangeRequest] - Error:', error);
|
||||
res.status(500).json({ error: 'Änderungsanfrage konnte nicht eingereicht werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const respondToCalendarEvent = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const eventId = Number(req.params.eventId);
|
||||
const status = req.body?.status;
|
||||
if (!['attending', 'declined'].includes(status)) return res.status(400).json({ error: 'Ungültige Rückmeldung.' });
|
||||
const access = await getApprovedLinkedMember(clubId, Number(req.user?.id));
|
||||
if (access.error) return res.status(access.status).json({ error: access.error });
|
||||
const event = await CalendarEvent.findOne({ where: { id: eventId, clubId } });
|
||||
if (!event) return res.status(404).json({ error: 'Veranstaltung nicht gefunden.' });
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
if (event.startDate < today || (event.registrationDeadline && event.registrationDeadline < today)) return res.status(409).json({ error: 'Für diese Veranstaltung sind keine Rückmeldungen mehr möglich.' });
|
||||
const [response] = await MemberEventResponse.upsert({ clubId, eventId, memberId: access.member.id, status }, { returning: true });
|
||||
res.json({ eventId, status: response.status });
|
||||
} catch (error) {
|
||||
console.error('[respondToCalendarEvent] - Error:', error);
|
||||
res.status(500).json({ error: 'Rückmeldung konnte nicht gespeichert werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const updateOwnTrainingAttendance = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const diaryDateId = Number(req.params.diaryDateId);
|
||||
const attendanceStatus = req.body?.attendanceStatus;
|
||||
if (!['present', 'excused', 'cancelled'].includes(attendanceStatus)) return res.status(400).json({ error: 'Ungültiger Trainingsstatus.' });
|
||||
const access = await getApprovedLinkedMember(clubId, Number(req.user?.id));
|
||||
if (access.error) return res.status(access.status).json({ error: access.error });
|
||||
const diaryDate = await DiaryDate.findOne({ where: { id: diaryDateId, clubId } });
|
||||
if (!diaryDate) return res.status(404).json({ error: 'Trainingstag nicht gefunden.' });
|
||||
const [participant] = await Participant.findOrCreate({ where: { diaryDateId, memberId: access.member.id }, defaults: { attendanceStatus } });
|
||||
if (!participant.isNewRecord) await participant.update({ attendanceStatus });
|
||||
res.json({ diaryDateId, attendanceStatus: participant.attendanceStatus });
|
||||
} catch (error) {
|
||||
console.error('[updateOwnTrainingAttendance] - Error:', error);
|
||||
res.status(500).json({ error: 'Trainingsrückmeldung konnte nicht gespeichert werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const getEventResponses = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const eventId = Number(req.params.eventId);
|
||||
const responses = await MemberEventResponse.findAll({ where: { clubId, eventId }, include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }], order: [['updatedAt', 'DESC']] });
|
||||
res.json(responses.map((response) => ({ id: response.id, memberId: response.memberId, status: response.status, updatedAt: response.updatedAt, member: response.member ? { id: response.member.id, firstName: response.member.firstName, lastName: response.member.lastName } : null })));
|
||||
} catch (error) {
|
||||
console.error('[getEventResponses] - Error:', error);
|
||||
res.status(500).json({ error: 'Rückmeldungen konnten nicht geladen werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const getProfileChangeRequests = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const requests = await MemberProfileChangeRequest.findAll({
|
||||
where: { clubId, ...(req.query.status ? { status: req.query.status } : {}) },
|
||||
include: [{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] }],
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
res.json(requests.map((request) => ({ id: request.id, memberId: request.memberId, status: request.status, requestedChanges: request.requestedChanges, reviewNote: request.reviewNote, createdAt: request.createdAt, member: request.member ? { id: request.member.id, firstName: request.member.firstName, lastName: request.member.lastName } : null })));
|
||||
} catch (error) {
|
||||
console.error('[getProfileChangeRequests] - Error:', error);
|
||||
res.status(500).json({ error: 'Änderungsanfragen konnten nicht geladen werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const reviewProfileChangeRequest = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const requestId = Number(req.params.requestId);
|
||||
const status = req.body?.status;
|
||||
const reviewNote = typeof req.body?.reviewNote === 'string' ? req.body.reviewNote.trim() : null;
|
||||
if (!['approved', 'rejected'].includes(status)) return res.status(400).json({ error: 'Ungültiger Prüfstatus.' });
|
||||
const request = await MemberProfileChangeRequest.findOne({ where: { id: requestId, clubId, status: 'open' }, include: [{ model: Member, as: 'member' }] });
|
||||
if (!request || !request.member) return res.status(404).json({ error: 'Offene Änderungsanfrage nicht gefunden.' });
|
||||
if (status === 'approved') await request.member.update(request.requestedChanges);
|
||||
await request.update({ status, reviewNote, reviewedByUserId: req.user.id, reviewedAt: new Date() });
|
||||
if (status === 'approved') emitMemberChanged(clubId);
|
||||
res.json({ id: request.id, status: request.status });
|
||||
} catch (error) {
|
||||
console.error('[reviewProfileChangeRequest] - Error:', error);
|
||||
res.status(500).json({ error: 'Änderungsanfrage konnte nicht geprüft werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const getMemberInbox = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const userId = Number(req.user?.id);
|
||||
const access = await getApprovedLinkedMember(clubId, userId);
|
||||
if (access.error) return res.status(access.status).json({ error: access.error });
|
||||
const recipients = await ClubCommunicationRecipient.findAll({
|
||||
where: { clubId, memberId: access.member.id, deliveryStatus: 'sent' },
|
||||
include: [{
|
||||
model: ClubCommunicationThread,
|
||||
as: 'thread',
|
||||
required: true,
|
||||
where: { status: 'sent' },
|
||||
attributes: ['id', 'subject', 'threadType', 'replyTo', 'sentAt'],
|
||||
include: [{ model: ClubCommunicationMessage, as: 'messages', required: false, attributes: ['id', 'body', 'direction', 'messageType', 'createdByUserId', 'createdAt'] }],
|
||||
}],
|
||||
order: [['deliveredAt', 'DESC']],
|
||||
});
|
||||
const items = recipients.map((recipient) => {
|
||||
const thread = recipient.thread;
|
||||
const messages = (thread?.messages || [])
|
||||
.filter((message) => message.messageType === 'message' && (message.direction === 'outbound' || (message.direction === 'inbound' && Number(message.createdByUserId) === userId)))
|
||||
.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))
|
||||
.map((message) => ({ id: message.id, body: message.body, direction: message.direction, createdAt: message.createdAt }));
|
||||
const latestOutbound = [...messages].reverse().find((message) => message.direction === 'outbound');
|
||||
return {
|
||||
recipientId: recipient.id,
|
||||
threadId: thread.id,
|
||||
subject: thread.subject,
|
||||
threadType: thread.threadType,
|
||||
sentAt: recipient.deliveredAt || thread.sentAt,
|
||||
readAt: recipient.readAt,
|
||||
canAskQuestion: Boolean(thread.replyTo),
|
||||
preview: latestOutbound?.body || '',
|
||||
messages,
|
||||
};
|
||||
});
|
||||
res.json({ items, unreadCount: items.filter((item) => !item.readAt).length });
|
||||
} catch (error) {
|
||||
console.error('[getMemberInbox] - Error:', error);
|
||||
res.status(500).json({ error: 'Nachrichten konnten nicht geladen werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const markInboxItemRead = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const recipientId = Number(req.params.recipientId);
|
||||
const access = await getApprovedLinkedMember(clubId, Number(req.user?.id));
|
||||
if (access.error) return res.status(access.status).json({ error: access.error });
|
||||
const recipient = await ClubCommunicationRecipient.findOne({ where: { id: recipientId, clubId, memberId: access.member.id, deliveryStatus: 'sent' } });
|
||||
if (!recipient) return res.status(404).json({ error: 'Nachricht nicht gefunden.' });
|
||||
if (!recipient.readAt) await recipient.update({ readAt: new Date() });
|
||||
res.json({ recipientId, readAt: recipient.readAt });
|
||||
} catch (error) {
|
||||
console.error('[markInboxItemRead] - Error:', error);
|
||||
res.status(500).json({ error: 'Lesestatus konnte nicht gespeichert werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const askInboxQuestion = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const recipientId = Number(req.params.recipientId);
|
||||
const userId = Number(req.user?.id);
|
||||
const body = String(req.body?.body || '').trim();
|
||||
if (!body) return res.status(400).json({ error: 'Bitte formuliere deine Rückfrage.' });
|
||||
if (body.length > 4000) return res.status(400).json({ error: 'Die Rückfrage ist zu lang.' });
|
||||
const access = await getApprovedLinkedMember(clubId, userId);
|
||||
if (access.error) return res.status(access.status).json({ error: access.error });
|
||||
const recipient = await ClubCommunicationRecipient.findOne({
|
||||
where: { id: recipientId, clubId, memberId: access.member.id, deliveryStatus: 'sent' },
|
||||
include: [{ model: ClubCommunicationThread, as: 'thread', required: true, attributes: ['id', 'status', 'replyTo'] }],
|
||||
});
|
||||
if (!recipient?.thread || recipient.thread.status !== 'sent' || !recipient.thread.replyTo) return res.status(409).json({ error: 'Für diese Nachricht ist keine Rückfrage vorgesehen.' });
|
||||
const message = await ClubCommunicationMessage.create({ threadId: recipient.thread.id, clubId, body, direction: 'inbound', messageType: 'message', createdByUserId: userId });
|
||||
if (!recipient.readAt) await recipient.update({ readAt: new Date() });
|
||||
res.status(201).json({ id: message.id, body: message.body, direction: message.direction, createdAt: message.createdAt });
|
||||
} catch (error) {
|
||||
console.error('[askInboxQuestion] - Error:', error);
|
||||
res.status(500).json({ error: 'Rückfrage konnte nicht gesendet werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
getClubMembers,
|
||||
getWaitingApprovals,
|
||||
@@ -372,5 +640,16 @@ export {
|
||||
requestClickTtPlayerRegistration,
|
||||
deleteMemberImage,
|
||||
setPrimaryMemberImage,
|
||||
generateMemberGallery
|
||||
generateMemberGallery,
|
||||
linkMemberUser,
|
||||
getMemberDashboard,
|
||||
createProfileChangeRequest,
|
||||
respondToCalendarEvent,
|
||||
updateOwnTrainingAttendance,
|
||||
getEventResponses,
|
||||
getProfileChangeRequests,
|
||||
reviewProfileChangeRequest,
|
||||
getMemberInbox,
|
||||
markInboxItemRead,
|
||||
askInboxQuestion
|
||||
};
|
||||
|
||||
0
backend/controllers/memberGroupPhotoController.js
Normal file → Executable file
0
backend/controllers/memberGroupPhotoController.js
Normal file → Executable file
0
backend/controllers/memberNoteController.js
Normal file → Executable file
0
backend/controllers/memberNoteController.js
Normal file → Executable file
0
backend/controllers/memberOrderController.js
Normal file → Executable file
0
backend/controllers/memberOrderController.js
Normal file → Executable file
0
backend/controllers/memberTransferConfigController.js
Normal file → Executable file
0
backend/controllers/memberTransferConfigController.js
Normal file → Executable file
0
backend/controllers/mobileFeedbackController.js
Normal file → Executable file
0
backend/controllers/mobileFeedbackController.js
Normal file → Executable file
0
backend/controllers/myTischtennisController.js
Normal file → Executable file
0
backend/controllers/myTischtennisController.js
Normal file → Executable file
46
backend/controllers/myTischtennisUrlController.js
Normal file → Executable file
46
backend/controllers/myTischtennisUrlController.js
Normal file → Executable file
@@ -250,39 +250,37 @@ class MyTischtennisUrlController {
|
||||
throw new HttpError('Club team not found', 404);
|
||||
}
|
||||
|
||||
let league;
|
||||
|
||||
// First, try to find existing league by name and season
|
||||
const leagueName = completeData.leagueName || completeData.groupname;
|
||||
league = await League.findOne({
|
||||
where: {
|
||||
name: leagueName,
|
||||
seasonId: season.id,
|
||||
clubId: team.clubId
|
||||
let league;
|
||||
|
||||
// The league chosen for a club team is also used by the schedule filters.
|
||||
// Keep that assignment and only enrich it with the MyTischtennis metadata.
|
||||
if (team.leagueId) {
|
||||
league = await League.findOne({
|
||||
where: { id: team.leagueId, clubId: team.clubId }
|
||||
});
|
||||
if (league) {
|
||||
devLog(`Keeping team's existing league: ${league.name} (ID: ${league.id})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!league) {
|
||||
league = await League.findOne({
|
||||
where: {
|
||||
name: leagueName,
|
||||
seasonId: season.id,
|
||||
clubId: team.clubId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (league) {
|
||||
devLog(`Found existing league: ${league.name} (ID: ${league.id})`);
|
||||
// Update myTischtennis fields
|
||||
devLog(`Using existing league: ${league.name} (ID: ${league.id})`);
|
||||
await league.update({
|
||||
myTischtennisGroupId: completeData.groupId,
|
||||
association: completeData.association,
|
||||
groupname: completeData.groupname
|
||||
});
|
||||
} else if (team.leagueId) {
|
||||
// Team has a league assigned, update it
|
||||
league = await League.findByPk(team.leagueId);
|
||||
|
||||
if (league) {
|
||||
devLog(`Updating team's existing league: ${league.name} (ID: ${league.id})`);
|
||||
await league.update({
|
||||
name: leagueName,
|
||||
myTischtennisGroupId: completeData.groupId,
|
||||
association: completeData.association,
|
||||
groupname: completeData.groupname
|
||||
});
|
||||
}
|
||||
} else if (createLeague) {
|
||||
// Create new league
|
||||
devLog(`Creating new league: ${leagueName}`);
|
||||
|
||||
0
backend/controllers/officialTournamentController.js
Normal file → Executable file
0
backend/controllers/officialTournamentController.js
Normal file → Executable file
0
backend/controllers/participantController.js
Normal file → Executable file
0
backend/controllers/participantController.js
Normal file → Executable file
0
backend/controllers/permissionController.js
Normal file → Executable file
0
backend/controllers/permissionController.js
Normal file → Executable file
0
backend/controllers/predefinedActivityController.js
Normal file → Executable file
0
backend/controllers/predefinedActivityController.js
Normal file → Executable file
0
backend/controllers/predefinedActivityImageController.js
Normal file → Executable file
0
backend/controllers/predefinedActivityImageController.js
Normal file → Executable file
0
backend/controllers/seasonController.js
Normal file → Executable file
0
backend/controllers/seasonController.js
Normal file → Executable file
0
backend/controllers/sessionController.js
Normal file → Executable file
0
backend/controllers/sessionController.js
Normal file → Executable file
0
backend/controllers/teamController.js
Normal file → Executable file
0
backend/controllers/teamController.js
Normal file → Executable file
0
backend/controllers/teamDocumentController.js
Normal file → Executable file
0
backend/controllers/teamDocumentController.js
Normal file → Executable file
0
backend/controllers/tournamentController.js
Normal file → Executable file
0
backend/controllers/tournamentController.js
Normal file → Executable file
0
backend/controllers/tournamentStagesController.js
Normal file → Executable file
0
backend/controllers/tournamentStagesController.js
Normal file → Executable file
0
backend/controllers/trainingCancellationController.js
Normal file → Executable file
0
backend/controllers/trainingCancellationController.js
Normal file → Executable file
0
backend/controllers/trainingGroupController.js
Normal file → Executable file
0
backend/controllers/trainingGroupController.js
Normal file → Executable file
0
backend/controllers/trainingStatsController.js
Normal file → Executable file
0
backend/controllers/trainingStatsController.js
Normal file → Executable file
0
backend/controllers/trainingTimeController.js
Normal file → Executable file
0
backend/controllers/trainingTimeController.js
Normal file → Executable file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user