Compare commits
35 Commits
111b37b287
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
616bade9a5 | ||
|
|
ef8d99eaf4 | ||
|
|
240cbf7d14 | ||
|
|
b404c289bb | ||
|
|
ee526dc3f2 | ||
|
|
01ca924e3e | ||
|
|
e2e8ba0d54 | ||
|
|
63a5db1196 | ||
|
|
3ac4039097 | ||
|
|
46079fc16e | ||
|
|
e38f2c2e19 | ||
|
|
d190c2cb70 | ||
|
|
fd0b3020bf | ||
|
|
8e4edfb719 | ||
|
|
01f80ea094 | ||
|
|
8140283b7a | ||
|
|
48f0f9afb0 | ||
|
|
ad778328e8 | ||
|
|
91006bb0c5 | ||
|
|
110519c7b8 | ||
|
|
6afa5a5629 | ||
|
|
7bf7ae2bef | ||
|
|
51b620f496 | ||
|
|
f83e3986b7 | ||
|
|
bb9a030e89 | ||
|
|
a767061bd3 | ||
|
|
35031e3ae5 | ||
|
|
370df07c2a | ||
|
|
94385231d3 | ||
|
|
5357dac9e1 | ||
|
|
ac97332e6f | ||
|
|
90e6c2f9f6 | ||
|
|
dd91c07694 | ||
|
|
302ed20a9b | ||
|
|
542fae089c |
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
15
backend/controllers/calendarEventController.js
Normal file → Executable file
15
backend/controllers/calendarEventController.js
Normal file → Executable file
@@ -19,7 +19,7 @@ export const createClubCalendarEvent = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const event = await calendarEventService.createClubEvent(userToken, clubId, req.body);
|
||||
const event = await calendarEventService.createClubEvent(userToken, clubId, req.body, req.user?.id || null);
|
||||
res.status(201).json(event);
|
||||
} catch (error) {
|
||||
console.error('[createClubCalendarEvent] - Error:', error);
|
||||
@@ -28,6 +28,19 @@ export const createClubCalendarEvent = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClubCalendarEvent = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, eventId } = req.params;
|
||||
const event = await calendarEventService.updateClubEvent(userToken, clubId, eventId, req.body, req.user?.id || null);
|
||||
res.status(200).json(event);
|
||||
} catch (error) {
|
||||
console.error('[updateClubCalendarEvent] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Speichern des Kalender-Events');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteClubCalendarEvent = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
|
||||
0
backend/controllers/clickTtAccountController.js
Normal file → Executable file
0
backend/controllers/clickTtAccountController.js
Normal file → Executable file
93
backend/controllers/clubAccountController.js
Executable file
93
backend/controllers/clubAccountController.js
Executable file
@@ -0,0 +1,93 @@
|
||||
import clubAccountService from '../services/clubAccountService.js';
|
||||
|
||||
class ClubAccountController {
|
||||
async listClubAccounts(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const payload = await clubAccountService.listClubAccounts(Number(clubId));
|
||||
res.json(payload);
|
||||
} catch (error) {
|
||||
console.error('[listClubAccounts] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Konten konnten nicht geladen werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async createClubAccount(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const account = await clubAccountService.createClubAccount(Number(clubId), req.body || {});
|
||||
res.status(201).json({ account });
|
||||
} catch (error) {
|
||||
console.error('[createClubAccount] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Konto konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateClubAccount(req, res) {
|
||||
try {
|
||||
const { clubId, accountId } = req.params;
|
||||
const account = await clubAccountService.updateClubAccount(Number(clubId), Number(accountId), req.body || {});
|
||||
res.json({ account });
|
||||
} catch (error) {
|
||||
console.error('[updateClubAccount] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Konto konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateClubAccountStatus(req, res) {
|
||||
try {
|
||||
const { clubId, accountId } = req.params;
|
||||
const account = await clubAccountService.updateClubAccountStatus(Number(clubId), Number(accountId), String(req.body?.status || ''));
|
||||
res.json({ account });
|
||||
} catch (error) {
|
||||
console.error('[updateClubAccountStatus] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Kontostatus konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteClubAccount(req, res) {
|
||||
try {
|
||||
const { clubId, accountId } = req.params;
|
||||
await clubAccountService.deleteClubAccount(Number(clubId), Number(accountId));
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[deleteClubAccount] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Konto konnte nicht gelöscht werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async createTransaction(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const transaction = await clubAccountService.createTransaction(Number(clubId), req.user?.id || null, req.body || {});
|
||||
res.status(201).json({ transaction });
|
||||
} catch (error) {
|
||||
console.error('[createClubAccountTransaction] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Kontenbewegung konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateTransaction(req, res) {
|
||||
try {
|
||||
const { clubId, transactionId } = req.params;
|
||||
const transaction = await clubAccountService.updateTransaction(Number(clubId), Number(transactionId), req.body || {});
|
||||
res.json({ transaction });
|
||||
} catch (error) {
|
||||
console.error('[updateClubAccountTransaction] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Kontenbewegung konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteTransaction(req, res) {
|
||||
try {
|
||||
const { clubId, transactionId } = req.params;
|
||||
await clubAccountService.deleteTransaction(Number(clubId), Number(transactionId));
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[deleteClubAccountTransaction] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Kontenbewegung konnte nicht gelöscht werden.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ClubAccountController();
|
||||
19
backend/controllers/clubArchiveController.js
Executable file
19
backend/controllers/clubArchiveController.js
Executable file
@@ -0,0 +1,19 @@
|
||||
import clubArchiveService from '../services/clubArchiveService.js';
|
||||
|
||||
class ClubArchiveController {
|
||||
async getClubArchive(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const archive = await clubArchiveService.getClubArchive(clubId);
|
||||
res.json(archive);
|
||||
} catch (error) {
|
||||
console.error('[getClubArchive] - Error:', error);
|
||||
if (error?.status) {
|
||||
return res.status(error.status).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: 'Fehler beim Laden des Vereinsarchivs' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ClubArchiveController();
|
||||
132
backend/controllers/clubCommunicationController.js
Executable file
132
backend/controllers/clubCommunicationController.js
Executable file
@@ -0,0 +1,132 @@
|
||||
import clubCommunicationService from '../services/clubCommunicationService.js';
|
||||
|
||||
function handleError(res, scope, error) {
|
||||
console.error(`[${scope}] - Error:`, error);
|
||||
res.status(error?.status || 500).json({
|
||||
error: error?.message || 'internalerror',
|
||||
});
|
||||
}
|
||||
|
||||
const clubCommunicationController = {
|
||||
async list(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const payload = await clubCommunicationService.listClubCommunication(Number(clubId));
|
||||
res.json(payload);
|
||||
} catch (error) {
|
||||
handleError(res, 'listClubCommunication', error);
|
||||
}
|
||||
},
|
||||
|
||||
async createThread(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const thread = await clubCommunicationService.createThread(Number(clubId), req.user?.id || null, req.body || {});
|
||||
res.status(201).json({ thread });
|
||||
} catch (error) {
|
||||
handleError(res, 'createClubCommunicationThread', error);
|
||||
}
|
||||
},
|
||||
|
||||
async updateThread(req, res) {
|
||||
try {
|
||||
const { clubId, threadId } = req.params;
|
||||
const thread = await clubCommunicationService.updateThread(Number(clubId), Number(threadId), req.body || {});
|
||||
res.json({ thread });
|
||||
} catch (error) {
|
||||
handleError(res, 'updateClubCommunicationThread', error);
|
||||
}
|
||||
},
|
||||
|
||||
async deleteThread(req, res) {
|
||||
try {
|
||||
const { clubId, threadId } = req.params;
|
||||
await clubCommunicationService.deleteThread(Number(clubId), Number(threadId));
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
handleError(res, 'deleteClubCommunicationThread', error);
|
||||
}
|
||||
},
|
||||
|
||||
async addMessage(req, res) {
|
||||
try {
|
||||
const { clubId, threadId } = req.params;
|
||||
const message = await clubCommunicationService.addMessage(Number(clubId), Number(threadId), req.user?.id || null, req.body || {});
|
||||
res.status(201).json({ message });
|
||||
} catch (error) {
|
||||
handleError(res, 'addClubCommunicationMessage', error);
|
||||
}
|
||||
},
|
||||
|
||||
async sendThread(req, res) {
|
||||
try {
|
||||
const { clubId, threadId } = req.params;
|
||||
const thread = await clubCommunicationService.sendThread(Number(clubId), Number(threadId), req.user?.id || null);
|
||||
res.json({ thread });
|
||||
} catch (error) {
|
||||
handleError(res, 'sendClubCommunicationThread', error);
|
||||
}
|
||||
},
|
||||
|
||||
async createGroup(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const group = await clubCommunicationService.createGroup(Number(clubId), req.body || {});
|
||||
res.status(201).json({ group });
|
||||
} catch (error) {
|
||||
handleError(res, 'createClubDistributionGroup', error);
|
||||
}
|
||||
},
|
||||
|
||||
async updateGroup(req, res) {
|
||||
try {
|
||||
const { clubId, groupId } = req.params;
|
||||
const group = await clubCommunicationService.updateGroup(Number(clubId), Number(groupId), req.body || {});
|
||||
res.json({ group });
|
||||
} catch (error) {
|
||||
handleError(res, 'updateClubDistributionGroup', error);
|
||||
}
|
||||
},
|
||||
|
||||
async deleteGroup(req, res) {
|
||||
try {
|
||||
const { clubId, groupId } = req.params;
|
||||
await clubCommunicationService.deleteGroup(Number(clubId), Number(groupId));
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
handleError(res, 'deleteClubDistributionGroup', error);
|
||||
}
|
||||
},
|
||||
|
||||
async createTemplate(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const template = await clubCommunicationService.createTemplate(Number(clubId), req.body || {});
|
||||
res.status(201).json({ template });
|
||||
} catch (error) {
|
||||
handleError(res, 'createClubCommunicationTemplate', error);
|
||||
}
|
||||
},
|
||||
|
||||
async updateTemplate(req, res) {
|
||||
try {
|
||||
const { clubId, templateId } = req.params;
|
||||
const template = await clubCommunicationService.updateTemplate(Number(clubId), Number(templateId), req.body || {});
|
||||
res.json({ template });
|
||||
} catch (error) {
|
||||
handleError(res, 'updateClubCommunicationTemplate', error);
|
||||
}
|
||||
},
|
||||
|
||||
async deleteTemplate(req, res) {
|
||||
try {
|
||||
const { clubId, templateId } = req.params;
|
||||
await clubCommunicationService.deleteTemplate(Number(clubId), Number(templateId));
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
handleError(res, 'deleteClubCommunicationTemplate', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default clubCommunicationController;
|
||||
671
backend/controllers/clubDashboardController.js
Executable file
671
backend/controllers/clubDashboardController.js
Executable file
@@ -0,0 +1,671 @@
|
||||
import { Op } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import {
|
||||
CalendarEvent,
|
||||
ClubCommunicationThread,
|
||||
ClubInvoice,
|
||||
ClubPaymentClaim,
|
||||
ClubRequest,
|
||||
ClubSepaMandate,
|
||||
ClubTask,
|
||||
Match,
|
||||
Member,
|
||||
TrainingGroup,
|
||||
} from '../models/index.js';
|
||||
import clubArchiveService from '../services/clubArchiveService.js';
|
||||
import { hasClubPaymentClaimPaidAmountCentsColumn } from '../services/clubPaymentClaimCompatibility.js';
|
||||
import { getSafeErrorMessage } from '../utils/errorUtils.js';
|
||||
|
||||
function formatRequestWorkflowStage(stage) {
|
||||
return {
|
||||
contact_replied: 'Kontakt beantwortet',
|
||||
trial_training_scheduled: 'Probetraining terminiert',
|
||||
trial_training_feedback_recorded: 'Probetraining nachbereitet',
|
||||
membership_reviewed: 'Mitgliedsanfrage geprüft',
|
||||
admission_prepared: 'Aufnahme vorbereitet',
|
||||
member_record_created: 'Mitglied angelegt',
|
||||
sepa_pending: 'SEPA ausstehend',
|
||||
onboarding_completed: 'Onboarding abgeschlossen',
|
||||
sponsoring_contacted: 'Sponsoring kontaktiert',
|
||||
sponsoring_offer_prepared: 'Sponsoringangebot vorbereitet',
|
||||
sponsoring_followed_up: 'Sponsoring nachgefasst',
|
||||
}[stage] || stage;
|
||||
}
|
||||
|
||||
function formatTaskType(taskType) {
|
||||
return {
|
||||
request_contact_reply: 'Kontaktanfrage beantworten',
|
||||
request_schedule_trial_training: 'Probetraining organisieren',
|
||||
request_trial_training_follow_up: 'Probetraining nachbereiten',
|
||||
request_membership_review: 'Mitgliedsanfrage prüfen',
|
||||
membership_prepare_admission: 'Aufnahme vorbereiten',
|
||||
membership_create_member_record: 'Mitglied anlegen',
|
||||
membership_collect_sepa_mandate: 'SEPA organisieren',
|
||||
membership_assign_fee: 'Beitrag zuordnen',
|
||||
request_sponsoring_reply: 'Sponsoring nachfassen',
|
||||
sponsoring_prepare_offer: 'Sponsoringangebot vorbereiten',
|
||||
sponsoring_follow_up: 'Sponsoring nachfassen',
|
||||
document_review_required: 'Dokument prüfen',
|
||||
sponsor_contract_renewal: 'Sponsoring verlängern',
|
||||
member_missing_email: 'E-Mail ergänzen',
|
||||
member_missing_birthdate: 'Geburtsdatum ergänzen',
|
||||
member_missing_sepa_mandate: 'SEPA-Mandat einholen',
|
||||
payment_claim_due_soon: 'Fällige Zahlung vorbereiten',
|
||||
payment_claim_overdue: 'Überfällige Zahlung nachfassen',
|
||||
payment_claim_reminder: 'Mahnstufe prüfen',
|
||||
calendar_event_prepare: 'Termin vorbereiten',
|
||||
calendar_event_deadline_check: 'Terminfrist prüfen',
|
||||
}[taskType] || taskType || 'Freie Aufgabe';
|
||||
}
|
||||
|
||||
function formatCommunicationStatus(status) {
|
||||
return {
|
||||
draft: 'Entwurf',
|
||||
scheduled: 'Geplant',
|
||||
sent: 'Versendet',
|
||||
archived: 'Archiviert',
|
||||
}[status] || status || 'Status';
|
||||
}
|
||||
|
||||
function formatInvoiceStatus(status) {
|
||||
return {
|
||||
draft: 'Entwurf',
|
||||
issued: 'Gestellt',
|
||||
partially_paid: 'Teilbezahlt',
|
||||
paid: 'Bezahlt',
|
||||
cancelled: 'Storniert',
|
||||
archived: 'Archiviert',
|
||||
}[status] || status || 'Status';
|
||||
}
|
||||
|
||||
function formatEventDateRange(event) {
|
||||
if (!event?.startDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formatter = new Intl.DateTimeFormat('de-DE', { dateStyle: 'medium' });
|
||||
const start = formatter.format(new Date(event.startDate));
|
||||
const end = event.endDate ? formatter.format(new Date(event.endDate)) : start;
|
||||
return start === end ? start : `${start} bis ${end}`;
|
||||
}
|
||||
|
||||
function formatDate(value, options = { dateStyle: 'medium' }) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat('de-DE', options).format(new Date(value));
|
||||
}
|
||||
|
||||
function formatTime(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return String(value).slice(0, 5);
|
||||
}
|
||||
|
||||
function formatWeekday(weekday) {
|
||||
return ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'][Number(weekday)] || 'Unbekannt';
|
||||
}
|
||||
|
||||
function formatConfiguredTrainingLabel(entry) {
|
||||
const start = formatTime(entry.startTime);
|
||||
const end = formatTime(entry.endTime);
|
||||
const timeRange = start && end ? `${start} bis ${end} Uhr` : start ? `${start} Uhr` : null;
|
||||
|
||||
return [entry.groupName, formatWeekday(entry.weekday), timeRange].filter(Boolean).join(' · ');
|
||||
}
|
||||
|
||||
function formatMatchLabel(match) {
|
||||
const date = formatDate(match.date);
|
||||
const time = formatTime(match.time);
|
||||
const homeTeam = match.homeTeam?.name || 'Heimteam';
|
||||
const guestTeam = match.guestTeam?.name || 'Gastteam';
|
||||
const league = match.leagueDetails?.name || null;
|
||||
|
||||
return [
|
||||
`${homeTeam} gegen ${guestTeam}`,
|
||||
date,
|
||||
time ? `${time} Uhr` : null,
|
||||
league,
|
||||
].filter(Boolean).join(' · ');
|
||||
}
|
||||
|
||||
function createDashboardItem(label, to, extra = {}) {
|
||||
return { label, to, ...extra };
|
||||
}
|
||||
|
||||
function buildMemberRoute(memberId, scope = 'active', extraQuery = {}) {
|
||||
return {
|
||||
path: '/members',
|
||||
query: {
|
||||
scope,
|
||||
memberId: String(memberId),
|
||||
mode: 'edit',
|
||||
...extraQuery,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildRequestRoute(requestId, status = '') {
|
||||
const query = { requestId: String(requestId) };
|
||||
if (status) {
|
||||
query.status = status;
|
||||
}
|
||||
return {
|
||||
path: '/club-requests',
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
function buildTaskRoute(taskId, status = '') {
|
||||
const query = { taskId: String(taskId) };
|
||||
if (status) {
|
||||
query.status = status;
|
||||
}
|
||||
return {
|
||||
path: '/club-tasks',
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadAvailableTables() {
|
||||
const tables = await sequelize.getQueryInterface().showAllTables();
|
||||
return new Set(
|
||||
tables
|
||||
.map((table) => (typeof table === 'string' ? table : Object.values(table || {})[0]))
|
||||
.filter(Boolean)
|
||||
.map((table) => String(table).toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
async function loadOptionalTableData(availableTables, tableName, loader, fallbackValue = []) {
|
||||
if (!availableTables.has(String(tableName).toLowerCase())) {
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
return loader();
|
||||
}
|
||||
|
||||
function countMissingMemberFields(members) {
|
||||
const missing = {
|
||||
email: 0,
|
||||
birthDate: 0,
|
||||
};
|
||||
|
||||
for (const member of members) {
|
||||
if (!String(member.email || '').trim()) {
|
||||
missing.email += 1;
|
||||
}
|
||||
if (!String(member.birthDate || '').trim()) {
|
||||
missing.birthDate += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
function toNextOccurrenceDate(weekday, startTime) {
|
||||
const now = new Date();
|
||||
const result = new Date(now);
|
||||
const targetWeekday = Number(weekday);
|
||||
const daysUntilWeekday = (targetWeekday - result.getDay() + 7) % 7;
|
||||
result.setDate(result.getDate() + daysUntilWeekday);
|
||||
|
||||
const [hours = '0', minutes = '0'] = String(startTime || '00:00').split(':');
|
||||
result.setHours(Number(hours), Number(minutes), 0, 0);
|
||||
|
||||
if (result < now) {
|
||||
result.setDate(result.getDate() + 7);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildUpcomingTrainingSlots(groups, limit = 5) {
|
||||
return groups
|
||||
.flatMap((group) => (Array.isArray(group.trainingTimes) ? group.trainingTimes.map((time) => ({
|
||||
id: time.id,
|
||||
weekday: time.weekday,
|
||||
startTime: time.startTime,
|
||||
endTime: time.endTime,
|
||||
sortOrder: time.sortOrder,
|
||||
groupName: group.name,
|
||||
nextOccurrence: toNextOccurrenceDate(time.weekday, time.startTime),
|
||||
})) : []))
|
||||
.sort((left, right) => {
|
||||
const timeDiff = left.nextOccurrence.getTime() - right.nextOccurrence.getTime();
|
||||
if (timeDiff !== 0) return timeDiff;
|
||||
return String(left.groupName || '').localeCompare(String(right.groupName || ''));
|
||||
})
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
export const getClubDashboard = async (req, res) => {
|
||||
try {
|
||||
const clubId = Number(req.params.clubId);
|
||||
const currentUserId = Number(req.user?.id) || null;
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const todayIso = today.toISOString().slice(0, 10);
|
||||
const availableTables = await loadAvailableTables();
|
||||
const hasPaidAmountCentsColumn = await hasClubPaymentClaimPaidAmountCentsColumn();
|
||||
|
||||
const [
|
||||
requests,
|
||||
tasks,
|
||||
members,
|
||||
mandates,
|
||||
paymentClaims,
|
||||
upcomingEvents,
|
||||
trainingGroups,
|
||||
upcomingMatches,
|
||||
communicationThreads,
|
||||
invoices,
|
||||
archive,
|
||||
] = await Promise.all([
|
||||
loadOptionalTableData(availableTables, 'club_requests', () => ClubRequest.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
status: { [Op.notIn]: ['archived'] },
|
||||
},
|
||||
order: [['receivedAt', 'DESC']],
|
||||
})),
|
||||
loadOptionalTableData(availableTables, 'club_tasks', () => ClubTask.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
status: { [Op.notIn]: ['archived'] },
|
||||
},
|
||||
order: [['dueAt', 'ASC'], ['updatedAt', 'DESC']],
|
||||
})),
|
||||
Member.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
active: true,
|
||||
},
|
||||
order: [['createdAt', 'DESC']],
|
||||
}),
|
||||
loadOptionalTableData(availableTables, 'club_sepa_mandates', () => ClubSepaMandate.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
status: 'active',
|
||||
revokedAt: null,
|
||||
memberId: { [Op.ne]: null },
|
||||
},
|
||||
attributes: ['memberId'],
|
||||
})),
|
||||
hasPaidAmountCentsColumn
|
||||
? loadOptionalTableData(availableTables, 'club_payment_claims', () => ClubPaymentClaim.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
status: { [Op.in]: ['open', 'partially_paid'] },
|
||||
archivedAt: null,
|
||||
},
|
||||
order: [['dueOn', 'ASC']],
|
||||
}))
|
||||
: Promise.resolve([]),
|
||||
loadOptionalTableData(availableTables, 'calendar_events', () => CalendarEvent.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
endDate: { [Op.gte]: todayIso },
|
||||
},
|
||||
order: [['startDate', 'ASC']],
|
||||
limit: 5,
|
||||
})),
|
||||
loadOptionalTableData(availableTables, 'training_group', () => TrainingGroup.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
association: 'trainingTimes',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
order: [['isPreset', 'DESC'], ['sortOrder', 'ASC'], ['name', 'ASC']],
|
||||
})),
|
||||
loadOptionalTableData(availableTables, 'match', () => Match.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
date: { [Op.gte]: today },
|
||||
},
|
||||
include: [
|
||||
{ association: 'homeTeam', attributes: ['id', 'name'] },
|
||||
{ association: 'guestTeam', attributes: ['id', 'name'] },
|
||||
{ association: 'leagueDetails', attributes: ['id', 'name'] },
|
||||
],
|
||||
order: [['date', 'ASC'], ['time', 'ASC']],
|
||||
limit: 5,
|
||||
})),
|
||||
loadOptionalTableData(availableTables, 'club_communication_threads', () => ClubCommunicationThread.findAll({
|
||||
where: { clubId },
|
||||
attributes: ['id', 'subject', 'status', 'threadType', 'scheduledAt', 'sentAt', 'updatedAt'],
|
||||
order: [['updatedAt', 'DESC'], ['createdAt', 'DESC']],
|
||||
limit: 8,
|
||||
})),
|
||||
loadOptionalTableData(availableTables, 'club_invoices', () => ClubInvoice.findAll({
|
||||
where: {
|
||||
clubId,
|
||||
archivedAt: null,
|
||||
},
|
||||
attributes: ['id', 'invoiceNumber', 'invoiceDirection', 'invoiceType', 'status', 'dueOn', 'grossAmountCents', 'currencyCode', 'updatedAt'],
|
||||
order: [['updatedAt', 'DESC'], ['dueOn', 'ASC']],
|
||||
limit: 8,
|
||||
})),
|
||||
clubArchiveService.getClubArchive(clubId),
|
||||
]);
|
||||
|
||||
const visibleDashboardTasks = tasks.filter((task) => !task.assignedUserId || Number(task.assignedUserId) === currentUserId);
|
||||
const membersById = new Map(members.map((member) => [Number(member.id), member]));
|
||||
const paymentClaimsById = new Map(paymentClaims.map((claim) => [Number(claim.id), claim]));
|
||||
const missingFields = countMissingMemberFields(members);
|
||||
const openTasks = visibleDashboardTasks.filter((task) => task.status === 'open');
|
||||
const inProgressTasks = visibleDashboardTasks.filter((task) => task.status === 'in_progress');
|
||||
const automatedTasks = visibleDashboardTasks.filter((task) => Boolean(task.automationKey));
|
||||
const automatedOpenTasks = automatedTasks.filter((task) => ['open', 'in_progress', 'waiting'].includes(task.status));
|
||||
const overdueTaskCount = visibleDashboardTasks.filter((task) => {
|
||||
if (!task.dueAt || ['done', 'cancelled', 'archived'].includes(task.status)) {
|
||||
return false;
|
||||
}
|
||||
return new Date(task.dueAt) < today;
|
||||
}).length;
|
||||
const memberIdsWithMandate = new Set(mandates.map((mandate) => Number(mandate.memberId)).filter(Boolean));
|
||||
const missingMandateCount = members.filter((member) => !memberIdsWithMandate.has(Number(member.id))).length;
|
||||
const openRequestCount = requests.filter((request) => request.status === 'open').length;
|
||||
const inProgressRequestCount = requests.filter((request) => request.status === 'in_progress').length;
|
||||
const trialTrainingCount = requests.filter((request) => request.requestType === 'trial_training' && request.status !== 'archived').length;
|
||||
const workflowStageCounts = requests.reduce((accumulator, request) => {
|
||||
if (!request.workflowStage) return accumulator;
|
||||
accumulator[request.workflowStage] = (accumulator[request.workflowStage] || 0) + 1;
|
||||
return accumulator;
|
||||
}, {});
|
||||
const onboardingCount =
|
||||
(workflowStageCounts.membership_reviewed || 0) +
|
||||
(workflowStageCounts.admission_prepared || 0) +
|
||||
(workflowStageCounts.member_record_created || 0) +
|
||||
(workflowStageCounts.sepa_pending || 0);
|
||||
const duePaymentCount = paymentClaims.filter((claim) => claim.status === 'open').length;
|
||||
const reminderCount = paymentClaims.filter((claim) => Number(claim.reminderLevel || 0) > 0).length;
|
||||
const openInvoices = invoices.filter((invoice) => ['draft', 'issued', 'partially_paid'].includes(invoice.status));
|
||||
const outgoingOpenInvoices = openInvoices.filter((invoice) => invoice.invoiceDirection === 'outgoing');
|
||||
const incomingOpenInvoices = openInvoices.filter((invoice) => invoice.invoiceDirection === 'incoming');
|
||||
const communicationDraftCount = communicationThreads.filter((thread) => thread.status === 'draft').length;
|
||||
const communicationScheduledCount = communicationThreads.filter((thread) => thread.status === 'scheduled').length;
|
||||
const communicationSentCount = communicationThreads.filter((thread) => thread.status === 'sent').length;
|
||||
const recentMembers = members.slice(0, 4);
|
||||
const upcomingTrainings = buildUpcomingTrainingSlots(trainingGroups);
|
||||
const paidRatio = paymentClaims.length === 0
|
||||
? null
|
||||
: Math.max(
|
||||
0,
|
||||
Math.round(
|
||||
((paymentClaims.length - duePaymentCount) / paymentClaims.length) * 100
|
||||
)
|
||||
);
|
||||
|
||||
function taskDetailTarget(task) {
|
||||
if (task.relatedEntityType === 'member' && task.relatedEntityId) {
|
||||
return buildMemberRoute(task.relatedEntityId, 'active');
|
||||
}
|
||||
|
||||
if (task.relatedEntityType === 'club_request' && task.relatedEntityId) {
|
||||
const request = requests.find((entry) => Number(entry.id) === Number(task.relatedEntityId));
|
||||
return buildRequestRoute(task.relatedEntityId, request?.status || '');
|
||||
}
|
||||
|
||||
if (task.relatedEntityType === 'club_payment_claim' && task.relatedEntityId) {
|
||||
const claim = paymentClaimsById.get(Number(task.relatedEntityId));
|
||||
if (claim?.memberId) {
|
||||
return buildMemberRoute(claim.memberId, 'active');
|
||||
}
|
||||
}
|
||||
|
||||
return buildTaskRoute(task.id, task.status || '');
|
||||
}
|
||||
|
||||
function taskDashboardItem(task, label) {
|
||||
return createDashboardItem(label, taskDetailTarget(task), {
|
||||
isAssignedToCurrentUser: Boolean(task.assignedUserId) && Number(task.assignedUserId) === currentUserId,
|
||||
});
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: 'action-needed',
|
||||
title: 'Handlungsbedarf',
|
||||
cards: [
|
||||
{
|
||||
title: 'Neue Anfragen',
|
||||
value: `${openRequestCount + inProgressRequestCount}`,
|
||||
meta: trialTrainingCount > 0 ? `${trialTrainingCount} Probetrainings` : null,
|
||||
to: '/club-requests',
|
||||
accent: 'amber',
|
||||
items: [
|
||||
createDashboardItem(`${openRequestCount} offen`, '/club-requests'),
|
||||
createDashboardItem(`${inProgressRequestCount} in Bearbeitung`, '/club-requests'),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Anfrage-Workflows',
|
||||
value: `${onboardingCount}`,
|
||||
meta: onboardingCount > 0 ? 'im Aufnahme- und Onboardingprozess' : 'Keine aktiven Onboarding-Fälle',
|
||||
to: '/club-requests',
|
||||
accent: 'green',
|
||||
items: [
|
||||
createDashboardItem(`${workflowStageCounts.trial_training_scheduled || 0} Probetrainings terminiert`, '/club-requests'),
|
||||
createDashboardItem(`${workflowStageCounts.membership_reviewed || 0} Mitgliedsanfragen geprüft`, '/club-requests'),
|
||||
createDashboardItem(`${workflowStageCounts.sepa_pending || 0} Fälle mit ausstehendem SEPA`, '/club-requests'),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Offene Zahlungen',
|
||||
value: `${paymentClaims.length}`,
|
||||
meta: [
|
||||
duePaymentCount > 0 ? `${duePaymentCount} fällig offen` : null,
|
||||
reminderCount > 0 ? `${reminderCount} mit Mahnstufe` : null,
|
||||
].filter(Boolean).join(' · ') || 'Keine Mahnungen aktiv',
|
||||
to: '/club-payments',
|
||||
accent: 'red',
|
||||
items: paymentClaims.slice(0, 3).map((claim) => {
|
||||
const amount = `${(Number(claim.amountCents) / 100).toFixed(2)} ${claim.currencyCode || 'EUR'}`;
|
||||
return createDashboardItem(
|
||||
`${amount} · ${claim.dueOn ? `fällig ${claim.dueOn}` : 'ohne Fälligkeit'}`,
|
||||
claim.memberId ? buildMemberRoute(claim.memberId, 'active') : '/club-payments'
|
||||
);
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'Fehlende Daten',
|
||||
value: `${missingFields.email + missingFields.birthDate + missingMandateCount}`,
|
||||
to: '/members',
|
||||
accent: 'blue',
|
||||
items: [
|
||||
createDashboardItem(`${missingFields.email} Mitglieder ohne E-Mail`, { path: '/members', query: { scope: 'dataIncomplete' } }),
|
||||
createDashboardItem(`${missingFields.birthDate} Mitglieder ohne Geburtsdatum`, { path: '/members', query: { scope: 'dataIncomplete' } }),
|
||||
createDashboardItem(`${missingMandateCount} Mitglieder ohne SEPA-Mandat`, { path: '/members', query: { scope: 'dataIncomplete' } }),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Offene Aufgaben',
|
||||
value: `${openTasks.length + inProgressTasks.length}`,
|
||||
meta: overdueTaskCount > 0 ? `${overdueTaskCount} überfällig` : 'Keine überfälligen Aufgaben',
|
||||
to: '/club-tasks',
|
||||
accent: overdueTaskCount > 0 ? 'red' : 'green',
|
||||
items: [
|
||||
createDashboardItem(`${automatedOpenTasks.length} automatisch erzeugte Schritte`, '/club-tasks'),
|
||||
...visibleDashboardTasks.slice(0, 3).map((task) => taskDashboardItem(task, task.title)),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'operations-room',
|
||||
title: 'Kommunikation, Finanzen und Archiv',
|
||||
cards: [
|
||||
{
|
||||
title: 'Kommunikation',
|
||||
value: `${communicationThreads.length}`,
|
||||
meta: `${communicationDraftCount} Entwürfe, ${communicationScheduledCount} geplant, ${communicationSentCount} versendet`,
|
||||
to: '/club-communication',
|
||||
accent: 'blue',
|
||||
items: communicationThreads.slice(0, 3).map((thread) => createDashboardItem(
|
||||
`${thread.subject || 'Ohne Betreff'} · ${formatCommunicationStatus(thread.status)}`,
|
||||
'/club-communication'
|
||||
)),
|
||||
},
|
||||
{
|
||||
title: 'Rechnungen',
|
||||
value: `${openInvoices.length}`,
|
||||
meta: `${outgoingOpenInvoices.length} ausgehend, ${incomingOpenInvoices.length} eingehend`,
|
||||
to: '/club-invoices',
|
||||
accent: 'amber',
|
||||
items: openInvoices.slice(0, 3).map((invoice) => createDashboardItem(
|
||||
`${invoice.invoiceNumber || `Rechnung #${invoice.id}`} · ${formatInvoiceStatus(invoice.status)}`,
|
||||
'/club-invoices'
|
||||
)),
|
||||
},
|
||||
{
|
||||
title: 'Archiv',
|
||||
value: `${archive.summary.archivedRequests + archive.summary.archivedTasks + archive.summary.archivedClaims}`,
|
||||
meta: `${archive.summary.inactiveMembers} inaktive Mitglieder`,
|
||||
to: '/club-archive',
|
||||
accent: 'neutral',
|
||||
items: [
|
||||
createDashboardItem(`${archive.summary.archivedRequests} archivierte Anfragen`, '/club-archive'),
|
||||
createDashboardItem(`${archive.summary.archivedTasks} archivierte Aufgaben`, '/club-archive'),
|
||||
createDashboardItem(`${archive.summary.archivedClaims} archivierte Beitragsfälle`, '/club-archive'),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'appointments',
|
||||
title: 'Aktuelle Termine',
|
||||
cards: [
|
||||
{
|
||||
title: 'Nächste Trainings',
|
||||
value: `${upcomingTrainings.length}`,
|
||||
to: {
|
||||
path: '/club-settings',
|
||||
query: { tab: 'training-times' },
|
||||
},
|
||||
items: upcomingTrainings.map((training) => createDashboardItem(
|
||||
formatConfiguredTrainingLabel(training),
|
||||
{
|
||||
path: '/club-settings',
|
||||
query: { tab: 'training-times' },
|
||||
}
|
||||
)),
|
||||
},
|
||||
{
|
||||
title: 'Nächste Spiele',
|
||||
value: `${upcomingMatches.length}`,
|
||||
to: '/schedule',
|
||||
items: upcomingMatches.map((match) => createDashboardItem(formatMatchLabel(match), '/schedule')),
|
||||
},
|
||||
{
|
||||
title: 'Kalendertermine',
|
||||
value: `${upcomingEvents.length}`,
|
||||
to: '/calendar',
|
||||
items: upcomingEvents.map((event) => createDashboardItem(`${event.title} · ${formatEventDateRange(event)}`, '/calendar')),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'club-status',
|
||||
title: 'Vereinsstatus',
|
||||
cards: [
|
||||
{
|
||||
title: 'Mitglieder',
|
||||
value: `${members.length} aktiv`,
|
||||
meta: members.length > 0 ? `${members.filter((member) => {
|
||||
const createdAt = new Date(member.createdAt);
|
||||
return createdAt.getFullYear() === today.getFullYear();
|
||||
}).length} dieses Jahr angelegt` : null,
|
||||
to: '/members',
|
||||
items: recentMembers.map((member) => {
|
||||
const name = [member.firstName, member.lastName].filter(Boolean).join(' ').trim() || member.email || `Mitglied ${member.id}`;
|
||||
return createDashboardItem(name, buildMemberRoute(member.id, 'active'));
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'Anfragen',
|
||||
value: `${requests.length}`,
|
||||
meta: `${openRequestCount} offen, ${inProgressRequestCount} in Bearbeitung`,
|
||||
to: '/club-requests',
|
||||
},
|
||||
{
|
||||
title: 'Workflow-Fortschritt',
|
||||
value: `${workflowStageCounts.onboarding_completed || 0}`,
|
||||
meta: 'Onboardings abgeschlossen',
|
||||
to: '/club-requests',
|
||||
items: [
|
||||
createDashboardItem(`${workflowStageCounts.admission_prepared || 0} Aufnahmen vorbereitet`, '/club-requests'),
|
||||
createDashboardItem(`${workflowStageCounts.member_record_created || 0} Mitglieder angelegt`, '/club-requests'),
|
||||
createDashboardItem(`${workflowStageCounts.sepa_pending || 0} warten auf SEPA`, '/club-requests'),
|
||||
],
|
||||
},
|
||||
{
|
||||
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-payments',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'recent-activity',
|
||||
title: 'Letzte Aktivitäten',
|
||||
cards: [
|
||||
{
|
||||
title: 'Zuletzt eingegangen',
|
||||
to: '/club-requests',
|
||||
items: requests.slice(0, 4).map((request) => {
|
||||
const name = [request.firstName, request.lastName].filter(Boolean).join(' ').trim() || request.email || 'Unbekannt';
|
||||
const workflow = request.workflowStage ? ` · ${formatRequestWorkflowStage(request.workflowStage)}` : '';
|
||||
return createDashboardItem(
|
||||
`${name} · ${request.subject || request.requestType}${workflow}`,
|
||||
buildRequestRoute(request.id, request.status || '')
|
||||
);
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: 'Aktuelle Aufgaben',
|
||||
to: '/club-tasks',
|
||||
items: visibleDashboardTasks.slice(0, 4).map((task) => taskDashboardItem(task, `${formatTaskType(task.taskType)} · ${task.status}`)),
|
||||
},
|
||||
{
|
||||
title: 'Automatik zuletzt aktiv',
|
||||
to: '/club-tasks',
|
||||
items: automatedTasks.slice(0, 4).map((task) => {
|
||||
const sourceLabel = task.automationSource === 'club_requests'
|
||||
? 'Anfrage'
|
||||
: task.automationSource === 'club_payment_claims'
|
||||
? 'Zahlung'
|
||||
: task.automationSource === 'club_documents'
|
||||
? 'Dokument'
|
||||
: task.automationSource === 'club_invoice_parties'
|
||||
? 'Sponsor'
|
||||
: task.automationSource === 'club_invoices'
|
||||
? 'Rechnung'
|
||||
: task.automationSource === 'club_communication'
|
||||
? 'Kommunikation'
|
||||
: task.automationSource === 'calendar_events'
|
||||
? 'Termin'
|
||||
: 'Workflow';
|
||||
return taskDashboardItem(task, `${formatTaskType(task.taskType)} · ${sourceLabel}`);
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
res.status(200).json({ sections });
|
||||
} catch (error) {
|
||||
console.error('[getClubDashboard] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Dashboard konnte nicht geladen werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
111
backend/controllers/clubDocumentController.js
Executable file
111
backend/controllers/clubDocumentController.js
Executable file
@@ -0,0 +1,111 @@
|
||||
import multer from 'multer';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import clubDocumentService from '../services/clubDocumentService.js';
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
fs.mkdirSync('uploads/temp', { recursive: true });
|
||||
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,
|
||||
limits: {
|
||||
fileSize: 20 * 1024 * 1024,
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
const allowedExtensions = ['.pdf', '.doc', '.docx', '.txt', '.csv', '.odt', '.xlsx'];
|
||||
const allowedMimePatterns = ['pdf', 'msword', 'wordprocessingml.document', 'text/plain', 'csv', 'excel', 'opendocument.text'];
|
||||
const extensionValid = allowedExtensions.includes(path.extname(file.originalname).toLowerCase());
|
||||
const mimetypeValid = allowedMimePatterns.some((pattern) => file.mimetype && file.mimetype.toLowerCase().includes(pattern));
|
||||
if (extensionValid && mimetypeValid) {
|
||||
return cb(null, true);
|
||||
}
|
||||
cb(new Error('Nur PDF, DOC, DOCX, TXT, CSV, ODT und XLSX Dateien sind erlaubt!'));
|
||||
},
|
||||
});
|
||||
|
||||
export const uploadMiddleware = upload.single('document');
|
||||
|
||||
function cleanupTempFile(file) {
|
||||
if (file?.path && fs.existsSync(file.path)) {
|
||||
fs.unlinkSync(file.path);
|
||||
}
|
||||
}
|
||||
|
||||
export const listClubDocuments = async (req, res) => {
|
||||
try {
|
||||
const documents = await clubDocumentService.listClubDocuments(Number(req.params.clubId), req.query || {});
|
||||
res.status(200).json({ documents });
|
||||
} catch (error) {
|
||||
res.status(error.status || 500).json({ error: error.message || 'internalerror' });
|
||||
}
|
||||
};
|
||||
|
||||
export const createClubDocument = async (req, res) => {
|
||||
try {
|
||||
const document = await clubDocumentService.createClubDocument(Number(req.params.clubId), req.user?.id || null, req.body || {}, req.file || null);
|
||||
res.status(201).json({ document });
|
||||
} catch (error) {
|
||||
cleanupTempFile(req.file);
|
||||
res.status(error.status || 500).json({ error: error.message || 'internalerror' });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClubDocument = async (req, res) => {
|
||||
try {
|
||||
const document = await clubDocumentService.updateClubDocument(Number(req.params.clubId), Number(req.params.documentId), req.user?.id || null, req.body || {}, req.file || null);
|
||||
res.status(200).json({ document });
|
||||
} catch (error) {
|
||||
cleanupTempFile(req.file);
|
||||
res.status(error.status || 500).json({ error: error.message || 'internalerror' });
|
||||
}
|
||||
};
|
||||
|
||||
export const archiveClubDocument = async (req, res) => {
|
||||
try {
|
||||
const document = await clubDocumentService.archiveClubDocument(Number(req.params.clubId), Number(req.params.documentId));
|
||||
res.status(200).json({ document });
|
||||
} catch (error) {
|
||||
res.status(error.status || 500).json({ error: error.message || 'internalerror' });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteClubDocument = async (req, res) => {
|
||||
try {
|
||||
await clubDocumentService.deleteClubDocument(Number(req.params.clubId), Number(req.params.documentId));
|
||||
res.status(204).end();
|
||||
} catch (error) {
|
||||
res.status(error.status || 500).json({ error: error.message || 'internalerror' });
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadClubDocument = async (req, res) => {
|
||||
try {
|
||||
const versionNo = req.query.versionNo ? Number(req.query.versionNo) : null;
|
||||
const { document, version } = await clubDocumentService.getDocumentDownload(Number(req.params.clubId), Number(req.params.documentId), versionNo);
|
||||
if (!fs.existsSync(version.storagePath)) {
|
||||
return res.status(404).json({ error: 'filenotfound' });
|
||||
}
|
||||
res.setHeader('Content-Disposition', `inline; filename="${document.title || version.fileName}"`);
|
||||
res.setHeader('Content-Type', version.mimeType || 'application/octet-stream');
|
||||
res.sendFile(version.storagePath);
|
||||
} catch (error) {
|
||||
res.status(error.status || 500).json({ error: error.message || 'internalerror' });
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
listClubDocuments,
|
||||
createClubDocument,
|
||||
updateClubDocument,
|
||||
archiveClubDocument,
|
||||
deleteClubDocument,
|
||||
downloadClubDocument,
|
||||
};
|
||||
93
backend/controllers/clubInvoiceController.js
Executable file
93
backend/controllers/clubInvoiceController.js
Executable file
@@ -0,0 +1,93 @@
|
||||
import clubInvoiceService from '../services/clubInvoiceService.js';
|
||||
|
||||
class ClubInvoiceController {
|
||||
async listClubInvoices(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const payload = await clubInvoiceService.listClubInvoices(Number(clubId));
|
||||
res.json(payload);
|
||||
} catch (error) {
|
||||
console.error('[listClubInvoices] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Rechnungen konnten nicht geladen werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async createInvoiceParty(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const party = await clubInvoiceService.createInvoiceParty(Number(clubId), req.body || {});
|
||||
res.status(201).json({ party });
|
||||
} catch (error) {
|
||||
console.error('[createInvoiceParty] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Rechnungspartei konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateInvoiceParty(req, res) {
|
||||
try {
|
||||
const { clubId, partyId } = req.params;
|
||||
const party = await clubInvoiceService.updateInvoiceParty(Number(clubId), Number(partyId), req.body || {});
|
||||
res.json({ party });
|
||||
} catch (error) {
|
||||
console.error('[updateInvoiceParty] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Rechnungspartei konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteInvoiceParty(req, res) {
|
||||
try {
|
||||
const { clubId, partyId } = req.params;
|
||||
await clubInvoiceService.deleteInvoiceParty(Number(clubId), Number(partyId));
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[deleteInvoiceParty] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Rechnungspartei konnte nicht gelöscht werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async createInvoice(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const invoice = await clubInvoiceService.createInvoice(Number(clubId), req.user?.id || null, req.body || {});
|
||||
res.status(201).json({ invoice });
|
||||
} catch (error) {
|
||||
console.error('[createInvoice] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Rechnung konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateInvoice(req, res) {
|
||||
try {
|
||||
const { clubId, invoiceId } = req.params;
|
||||
const invoice = await clubInvoiceService.updateInvoice(Number(clubId), Number(invoiceId), req.body || {});
|
||||
res.json({ invoice });
|
||||
} catch (error) {
|
||||
console.error('[updateInvoice] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Rechnung konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateInvoiceStatus(req, res) {
|
||||
try {
|
||||
const { clubId, invoiceId } = req.params;
|
||||
const invoice = await clubInvoiceService.updateInvoiceStatus(Number(clubId), Number(invoiceId), String(req.body?.status || ''));
|
||||
res.json({ invoice });
|
||||
} catch (error) {
|
||||
console.error('[updateInvoiceStatus] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Rechnungsstatus konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteInvoice(req, res) {
|
||||
try {
|
||||
const { clubId, invoiceId } = req.params;
|
||||
await clubInvoiceService.deleteInvoice(Number(clubId), Number(invoiceId));
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[deleteInvoice] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Rechnung konnte nicht gelöscht werden.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ClubInvoiceController();
|
||||
73
backend/controllers/clubPaymentClaimController.js
Executable file
73
backend/controllers/clubPaymentClaimController.js
Executable file
@@ -0,0 +1,73 @@
|
||||
import clubPaymentClaimService from '../services/clubPaymentClaimService.js';
|
||||
|
||||
class ClubPaymentClaimController {
|
||||
async list(req, res) {
|
||||
try {
|
||||
const payload = await clubPaymentClaimService.listClaims(Number(req.params.clubId));
|
||||
res.json(payload);
|
||||
} catch (error) {
|
||||
console.error('[listClubPaymentClaims] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Zahlungsforderungen konnten nicht geladen werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async create(req, res) {
|
||||
try {
|
||||
const claim = await clubPaymentClaimService.createClaim(Number(req.params.clubId), req.body || {});
|
||||
res.status(201).json({ claim });
|
||||
} catch (error) {
|
||||
console.error('[createClubPaymentClaim] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Zahlungsforderung konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async update(req, res) {
|
||||
try {
|
||||
const claim = await clubPaymentClaimService.updateClaim(Number(req.params.clubId), Number(req.params.claimId), req.body || {});
|
||||
res.json({ claim });
|
||||
} catch (error) {
|
||||
console.error('[updateClubPaymentClaim] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Zahlungsforderung konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateStatus(req, res) {
|
||||
try {
|
||||
const claim = await clubPaymentClaimService.updateClaimStatus(
|
||||
Number(req.params.clubId),
|
||||
Number(req.params.claimId),
|
||||
String(req.body?.status || '')
|
||||
);
|
||||
res.json({ claim });
|
||||
} catch (error) {
|
||||
console.error('[updateClubPaymentClaimStatus] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Status konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async registerPayment(req, res) {
|
||||
try {
|
||||
const claim = await clubPaymentClaimService.registerPayment(
|
||||
Number(req.params.clubId),
|
||||
Number(req.params.claimId),
|
||||
req.body || {}
|
||||
);
|
||||
res.json({ claim });
|
||||
} catch (error) {
|
||||
console.error('[registerClubPaymentClaimPayment] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Teilzahlung konnte nicht gespeichert werden.' });
|
||||
}
|
||||
}
|
||||
|
||||
async delete(req, res) {
|
||||
try {
|
||||
await clubPaymentClaimService.deleteClaim(Number(req.params.clubId), Number(req.params.claimId));
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[deleteClubPaymentClaim] - Error:', error);
|
||||
res.status(error?.status || 500).json({ error: error?.message || 'Zahlungsforderung konnte nicht gelöscht werden.' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ClubPaymentClaimController();
|
||||
174
backend/controllers/clubRequestController.js
Executable file
174
backend/controllers/clubRequestController.js
Executable file
@@ -0,0 +1,174 @@
|
||||
import { ClubRequest, ClubRequestNote } from '../models/index.js';
|
||||
import { getSafeErrorMessage } from '../utils/errorUtils.js';
|
||||
|
||||
const TERMINAL_REQUEST_STATUSES = new Set(['converted', 'rejected', 'archived']);
|
||||
|
||||
function isMissingRequestTableError(error) {
|
||||
return error?.original?.code === 'ER_NO_SUCH_TABLE'
|
||||
&& /club_requests|club_request_notes/.test(String(error?.original?.sqlMessage || ''));
|
||||
}
|
||||
|
||||
function normalizeRequestPayload(payload = {}) {
|
||||
return {
|
||||
requestType: payload.requestType || 'contact',
|
||||
subject: payload.subject?.trim() || null,
|
||||
firstName: payload.firstName?.trim() || null,
|
||||
lastName: payload.lastName?.trim() || null,
|
||||
email: payload.email?.trim() || null,
|
||||
phone: payload.phone?.trim() || null,
|
||||
message: payload.message?.trim() || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadRequestOrThrow(clubId, requestId) {
|
||||
const request = await ClubRequest.findOne({
|
||||
where: {
|
||||
id: requestId,
|
||||
clubId,
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: ClubRequestNote,
|
||||
as: 'notes',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
order: [[{ model: ClubRequestNote, as: 'notes' }, 'createdAt', 'DESC']],
|
||||
});
|
||||
|
||||
if (!request) {
|
||||
const error = new Error('Anfrage wurde nicht gefunden.');
|
||||
error.statusCode = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
export const listClubRequests = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
let requests = [];
|
||||
try {
|
||||
requests = await ClubRequest.findAll({
|
||||
where: { clubId },
|
||||
include: [
|
||||
{
|
||||
model: ClubRequestNote,
|
||||
as: 'notes',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
order: [
|
||||
['receivedAt', 'DESC'],
|
||||
[{ model: ClubRequestNote, as: 'notes' }, 'createdAt', 'DESC'],
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isMissingRequestTableError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ requests });
|
||||
} catch (error) {
|
||||
console.error('[listClubRequests] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Anfragen konnten nicht geladen werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createClubRequest = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const payload = normalizeRequestPayload(req.body);
|
||||
|
||||
if (!payload.subject && !payload.message) {
|
||||
return res.status(400).json({ error: 'Betreff oder Nachricht sind erforderlich.' });
|
||||
}
|
||||
|
||||
const request = await ClubRequest.create({
|
||||
clubId,
|
||||
...payload,
|
||||
receivedAt: new Date(),
|
||||
});
|
||||
|
||||
const created = await loadRequestOrThrow(clubId, request.id);
|
||||
res.status(201).json({ request: created });
|
||||
} catch (error) {
|
||||
console.error('[createClubRequest] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Anfrage konnte nicht gespeichert werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClubRequest = async (req, res) => {
|
||||
try {
|
||||
const { clubId, requestId } = req.params;
|
||||
const request = await loadRequestOrThrow(clubId, requestId);
|
||||
const payload = normalizeRequestPayload(req.body);
|
||||
|
||||
await request.update(payload);
|
||||
const updated = await loadRequestOrThrow(clubId, requestId);
|
||||
res.status(200).json({ request: updated });
|
||||
} catch (error) {
|
||||
console.error('[updateClubRequest] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Anfrage konnte nicht gespeichert werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClubRequestStatus = async (req, res) => {
|
||||
try {
|
||||
const { clubId, requestId } = req.params;
|
||||
const { status } = req.body || {};
|
||||
|
||||
if (!status) {
|
||||
return res.status(400).json({ error: 'Status fehlt.' });
|
||||
}
|
||||
|
||||
const request = await loadRequestOrThrow(clubId, requestId);
|
||||
await request.update({
|
||||
status,
|
||||
closedAt: TERMINAL_REQUEST_STATUSES.has(status) ? new Date() : null,
|
||||
});
|
||||
|
||||
const updated = await loadRequestOrThrow(clubId, requestId);
|
||||
res.status(200).json({ request: updated });
|
||||
} catch (error) {
|
||||
console.error('[updateClubRequestStatus] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Status konnte nicht gespeichert werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const addClubRequestNote = async (req, res) => {
|
||||
try {
|
||||
const { clubId, requestId } = req.params;
|
||||
const body = String(req.body?.body || '').trim();
|
||||
|
||||
if (!body) {
|
||||
return res.status(400).json({ error: 'Notiztext fehlt.' });
|
||||
}
|
||||
|
||||
await loadRequestOrThrow(clubId, requestId);
|
||||
|
||||
await ClubRequestNote.create({
|
||||
clubRequestId: requestId,
|
||||
createdByUserId: req.user?.id || null,
|
||||
body,
|
||||
});
|
||||
|
||||
const updated = await loadRequestOrThrow(clubId, requestId);
|
||||
res.status(201).json({ request: updated });
|
||||
} catch (error) {
|
||||
console.error('[addClubRequestNote] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Notiz konnte nicht gespeichert werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
19
backend/controllers/clubStatisticsController.js
Executable file
19
backend/controllers/clubStatisticsController.js
Executable file
@@ -0,0 +1,19 @@
|
||||
import clubStatisticsService from '../services/clubStatisticsService.js';
|
||||
|
||||
class ClubStatisticsController {
|
||||
async getClubStatistics(req, res) {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const statistics = await clubStatisticsService.getClubStatistics(clubId);
|
||||
res.json(statistics);
|
||||
} catch (error) {
|
||||
console.error('[getClubStatistics] - Error:', error);
|
||||
if (error?.status) {
|
||||
return res.status(error.status).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: 'Fehler beim Laden der Vereinsstatistiken' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ClubStatisticsController();
|
||||
275
backend/controllers/clubTaskController.js
Executable file
275
backend/controllers/clubTaskController.js
Executable file
@@ -0,0 +1,275 @@
|
||||
import { ClubTask, User, UserClub } from '../models/index.js';
|
||||
import { getSafeErrorMessage } from '../utils/errorUtils.js';
|
||||
import clubTaskAutomationService from '../services/clubTaskAutomationService.js';
|
||||
import clubWorkflowSourceService from '../services/clubWorkflowSourceService.js';
|
||||
|
||||
const TERMINAL_TASK_STATUSES = new Set(['done', 'cancelled', 'archived']);
|
||||
|
||||
function isMissingTaskTableError(error) {
|
||||
return error?.original?.code === 'ER_NO_SUCH_TABLE'
|
||||
&& /club_tasks/.test(String(error?.original?.sqlMessage || ''));
|
||||
}
|
||||
|
||||
function isMissingTaskSuppressionTableError(error) {
|
||||
return error?.original?.code === 'ER_NO_SUCH_TABLE'
|
||||
&& /club_task_suppressions/.test(String(error?.original?.sqlMessage || ''));
|
||||
}
|
||||
|
||||
function normalizeTaskPayload(payload = {}) {
|
||||
return {
|
||||
title: String(payload.title || '').trim(),
|
||||
taskType: payload.taskType?.trim() || null,
|
||||
description: payload.description?.trim() || null,
|
||||
status: payload.status || 'open',
|
||||
priority: payload.priority || 'normal',
|
||||
dueAt: payload.dueAt || null,
|
||||
remindAt: payload.remindAt || null,
|
||||
assignedUserId: payload.assignedUserId ? Number(payload.assignedUserId) : null,
|
||||
automationSource: payload.automationSource?.trim() || null,
|
||||
automationKey: payload.automationKey?.trim() || null,
|
||||
relatedEntityType: payload.relatedEntityType?.trim() || null,
|
||||
relatedEntityId: payload.relatedEntityId ? Number(payload.relatedEntityId) : null,
|
||||
sourceSnapshot: payload.sourceSnapshot || null,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadAssignableUsers(clubId) {
|
||||
const entries = await UserClub.findAll({
|
||||
where: { clubId },
|
||||
include: [{ model: User, as: 'user', attributes: ['id', 'email'] }],
|
||||
order: [[{ model: User, as: 'user' }, 'email', 'ASC']],
|
||||
});
|
||||
|
||||
return entries
|
||||
.filter((entry) => entry.user)
|
||||
.filter((entry) => entry.approved || entry.isOwner)
|
||||
.map((entry) => ({
|
||||
userId: entry.userId,
|
||||
email: entry.user.email,
|
||||
isOwner: Boolean(entry.isOwner),
|
||||
approved: Boolean(entry.approved),
|
||||
}));
|
||||
}
|
||||
|
||||
async function validateAssignedUser(clubId, assignedUserId) {
|
||||
if (!assignedUserId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userClub = await UserClub.findOne({
|
||||
where: {
|
||||
clubId,
|
||||
userId: assignedUserId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userClub || (!userClub.approved && !userClub.isOwner)) {
|
||||
const error = new Error('Der zugewiesene Benutzer gehört nicht zu diesem Verein.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return assignedUserId;
|
||||
}
|
||||
|
||||
async function loadTaskOrThrow(clubId, taskId) {
|
||||
const task = await ClubTask.findOne({
|
||||
where: {
|
||||
id: taskId,
|
||||
clubId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
const error = new Error('Aufgabe wurde nicht gefunden.');
|
||||
error.statusCode = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
export const listClubTasks = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
let tasks = [];
|
||||
let automationOverview = { definitions: [], suggestions: [] };
|
||||
|
||||
try {
|
||||
[tasks, automationOverview] = await Promise.all([
|
||||
ClubTask.findAll({
|
||||
where: { clubId },
|
||||
include: [{ model: User, as: 'assignedUser', attributes: ['id', 'email'], required: false }],
|
||||
order: [
|
||||
['status', 'ASC'],
|
||||
['dueAt', 'ASC'],
|
||||
['updatedAt', 'DESC'],
|
||||
],
|
||||
}),
|
||||
clubTaskAutomationService.buildAutomationOverview(clubId),
|
||||
]);
|
||||
} catch (error) {
|
||||
if (!isMissingTaskTableError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const assignableUsers = await loadAssignableUsers(clubId);
|
||||
|
||||
res.status(200).json({
|
||||
tasks,
|
||||
taskDefinitions: automationOverview.definitions,
|
||||
workflowSources: automationOverview.workflowSources || [],
|
||||
taskSuggestions: automationOverview.suggestions,
|
||||
assignableUsers,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[listClubTasks] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Aufgaben konnten nicht geladen werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createClubTask = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const payload = normalizeTaskPayload(req.body);
|
||||
|
||||
if (!payload.title) {
|
||||
return res.status(400).json({ error: 'Titel ist erforderlich.' });
|
||||
}
|
||||
|
||||
payload.assignedUserId = await validateAssignedUser(clubId, payload.assignedUserId);
|
||||
|
||||
const task = await ClubTask.create({
|
||||
clubId,
|
||||
...payload,
|
||||
createdByUserId: req.user?.id || null,
|
||||
});
|
||||
|
||||
res.status(201).json({ task });
|
||||
} catch (error) {
|
||||
console.error('[createClubTask] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Aufgabe konnte nicht gespeichert werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClubTask = async (req, res) => {
|
||||
try {
|
||||
const { clubId, taskId } = req.params;
|
||||
const task = await loadTaskOrThrow(clubId, taskId);
|
||||
const payload = normalizeTaskPayload(req.body);
|
||||
const wasDoneBefore = task.status === 'done';
|
||||
|
||||
if (!payload.title) {
|
||||
return res.status(400).json({ error: 'Titel ist erforderlich.' });
|
||||
}
|
||||
|
||||
payload.assignedUserId = await validateAssignedUser(clubId, payload.assignedUserId);
|
||||
|
||||
await task.update({
|
||||
...payload,
|
||||
completedAt: TERMINAL_TASK_STATUSES.has(payload.status) ? (task.completedAt || new Date()) : null,
|
||||
archivedAt: payload.status === 'archived' ? (task.archivedAt || new Date()) : null,
|
||||
});
|
||||
|
||||
const followUpTasks = !wasDoneBefore && payload.status === 'done'
|
||||
? await clubTaskAutomationService.materializeWorkflowFollowUps(task, req.user?.id || null)
|
||||
: [];
|
||||
const sourceUpdate = !wasDoneBefore && payload.status === 'done'
|
||||
? await clubWorkflowSourceService.syncSourceStateForCompletedTask(task)
|
||||
: null;
|
||||
|
||||
res.status(200).json({ task, followUpTasks, sourceUpdate });
|
||||
} catch (error) {
|
||||
console.error('[updateClubTask] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Aufgabe konnte nicht gespeichert werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClubTaskStatus = async (req, res) => {
|
||||
try {
|
||||
const { clubId, taskId } = req.params;
|
||||
const { status } = req.body || {};
|
||||
if (!status) {
|
||||
return res.status(400).json({ error: 'Status fehlt.' });
|
||||
}
|
||||
|
||||
const task = await loadTaskOrThrow(clubId, taskId);
|
||||
const wasDoneBefore = task.status === 'done';
|
||||
await task.update({
|
||||
status,
|
||||
completedAt: TERMINAL_TASK_STATUSES.has(status) ? (task.completedAt || new Date()) : null,
|
||||
archivedAt: status === 'archived' ? (task.archivedAt || new Date()) : null,
|
||||
});
|
||||
const followUpTasks = !wasDoneBefore && status === 'done'
|
||||
? await clubTaskAutomationService.materializeWorkflowFollowUps(task, req.user?.id || null)
|
||||
: [];
|
||||
const sourceUpdate = !wasDoneBefore && status === 'done'
|
||||
? await clubWorkflowSourceService.syncSourceStateForCompletedTask(task)
|
||||
: null;
|
||||
res.status(200).json({ task, followUpTasks, sourceUpdate });
|
||||
} catch (error) {
|
||||
console.error('[updateClubTaskStatus] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Status konnte nicht gespeichert werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const materializeAutomatedClubTasks = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const automationKeys = Array.isArray(req.body?.automationKeys) ? req.body.automationKeys : [];
|
||||
|
||||
if (automationKeys.length === 0) {
|
||||
return res.status(400).json({ error: 'Es wurden keine Automatik-Schlüssel übergeben.' });
|
||||
}
|
||||
|
||||
const tasks = await clubTaskAutomationService.materializeSuggestions(clubId, req.user?.id || null, automationKeys);
|
||||
res.status(201).json({ tasks });
|
||||
} catch (error) {
|
||||
console.error('[materializeAutomatedClubTasks] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Automatische Aufgaben konnten nicht erstellt werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const dismissAutomatedClubTaskSuggestion = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const payload = req.body || {};
|
||||
const suppression = await clubTaskAutomationService.dismissSuggestion(clubId, req.user?.id || null, payload);
|
||||
res.status(200).json({ success: true, suppression });
|
||||
} catch (error) {
|
||||
console.error('[dismissAutomatedClubTaskSuggestion] - Error:', error);
|
||||
if (isMissingTaskSuppressionTableError(error)) {
|
||||
return res.status(500).json({
|
||||
error: 'Die Tabelle club_task_suppressions fehlt noch. Bitte die aktuelle SQL-Datei auf dem System ausführen.',
|
||||
});
|
||||
}
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Vorschlag konnte nicht ausgeblendet werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteClubTask = async (req, res) => {
|
||||
try {
|
||||
const { clubId, taskId } = req.params;
|
||||
const task = await loadTaskOrThrow(clubId, taskId);
|
||||
await task.destroy();
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[deleteClubTask] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({
|
||||
error: getSafeErrorMessage(error, 'Aufgabe konnte nicht gelöscht werden.'),
|
||||
});
|
||||
}
|
||||
};
|
||||
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
14
backend/controllers/clubsController.js
Normal file → Executable file
14
backend/controllers/clubsController.js
Normal file → Executable file
@@ -67,7 +67,12 @@ export const updateClubSettings = async (req, res) => {
|
||||
autoFetchRankings,
|
||||
countryCode,
|
||||
stateCode,
|
||||
memberDataQualityRequirements
|
||||
memberDataQualityRequirements,
|
||||
feeRules,
|
||||
outgoingInvoicePrefix,
|
||||
outgoingInvoiceNextNumber,
|
||||
incomingInvoicePrefix,
|
||||
incomingInvoiceNextNumber
|
||||
} = req.body;
|
||||
const updated = await ClubService.updateClubSettings(token, clubid, {
|
||||
greetingText,
|
||||
@@ -76,7 +81,12 @@ export const updateClubSettings = async (req, res) => {
|
||||
autoFetchRankings,
|
||||
countryCode,
|
||||
stateCode,
|
||||
memberDataQualityRequirements
|
||||
memberDataQualityRequirements,
|
||||
feeRules,
|
||||
outgoingInvoicePrefix,
|
||||
outgoingInvoiceNextNumber,
|
||||
incomingInvoicePrefix,
|
||||
incomingInvoiceNextNumber
|
||||
});
|
||||
res.status(200).json(updated);
|
||||
} catch (error) {
|
||||
|
||||
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
317
backend/controllers/memberController.js
Normal file → Executable file
317
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) => {
|
||||
@@ -28,12 +41,13 @@ const getWaitingApprovals = async(req, res) => {
|
||||
|
||||
const setClubMembers = async (req, res) => {
|
||||
try {
|
||||
const { id: memberId, firstname: firstName, lastname: lastName, street, city, postalCode, birthdate, phone, email, active,
|
||||
testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, adultReleaseApproved, adultReserveApproved, contacts } = req.body;
|
||||
const { id: memberId, firstname: firstName, lastname: lastName, street, city, postalCode, birthdate, phone, email, active,
|
||||
testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, adultReleaseApproved, adultReserveApproved,
|
||||
contributionGroupCode, contacts } = req.body;
|
||||
const { id: clubId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, postalCode, birthdate,
|
||||
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, adultReleaseApproved, adultReserveApproved, contacts);
|
||||
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, adultReleaseApproved, adultReserveApproved, contributionGroupCode, contacts);
|
||||
|
||||
// Emit Socket-Event wenn Member erfolgreich erstellt/aktualisiert wurde
|
||||
if (addResult.status === 200) {
|
||||
@@ -47,6 +61,33 @@ const setClubMembers = async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getMemberSepaMandate = async (req, res) => {
|
||||
try {
|
||||
const { clubId, memberId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const result = await MemberService.getMemberSepaMandate(userToken, Number(clubId), Number(memberId));
|
||||
res.status(result.status || 500).json(result.response);
|
||||
} catch (error) {
|
||||
console.error('[getMemberSepaMandate] - Error:', error);
|
||||
res.status(500).json({ success: false, error: 'SEPA-Mandat konnte nicht geladen werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const saveMemberSepaMandate = async (req, res) => {
|
||||
try {
|
||||
const { clubId, memberId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const result = await MemberService.saveMemberSepaMandate(userToken, Number(clubId), Number(memberId), req.body || {});
|
||||
if (result.status === 200) {
|
||||
emitMemberChanged(clubId);
|
||||
}
|
||||
res.status(result.status || 500).json(result.response);
|
||||
} catch (error) {
|
||||
console.error('[saveMemberSepaMandate] - Error:', error);
|
||||
res.status(500).json({ success: false, error: 'SEPA-Mandat konnte nicht gespeichert werden.' });
|
||||
}
|
||||
};
|
||||
|
||||
const getMemberPlayInterests = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
@@ -323,10 +364,267 @@ 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,
|
||||
setClubMembers,
|
||||
getMemberSepaMandate,
|
||||
saveMemberSepaMandate,
|
||||
getMemberPlayInterests,
|
||||
setMemberPlayInterest,
|
||||
uploadMemberImage,
|
||||
@@ -342,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
76
backend/controllers/myTischtennisUrlController.js
Normal file → Executable file
76
backend/controllers/myTischtennisUrlController.js
Normal file → Executable file
@@ -10,6 +10,7 @@ import Season from '../models/Season.js';
|
||||
import User from '../models/User.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
import { hasUserClubAccess } from '../utils/userUtils.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const teamDataFetchJobs = new Map();
|
||||
@@ -249,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}`);
|
||||
@@ -635,15 +634,34 @@ class MyTischtennisUrlController {
|
||||
/**
|
||||
* Configure league from myTischtennis table URL
|
||||
* POST /api/mytischtennis/configure-league
|
||||
* Body: { url: string, createSeason?: boolean }
|
||||
* Body: { url: string, clubId: number, createSeason?: boolean }
|
||||
*/
|
||||
async configureLeague(req, res, next) {
|
||||
try {
|
||||
const { url, createSeason } = req.body;
|
||||
const { url, createSeason, clubId } = req.body;
|
||||
const userIdOrEmail = req.headers.userid;
|
||||
|
||||
if (!url) {
|
||||
throw new HttpError('URL is required', 400);
|
||||
if (!url || !clubId) {
|
||||
throw new HttpError('URL and clubId are required', 400);
|
||||
}
|
||||
|
||||
let userId = userIdOrEmail;
|
||||
if (isNaN(userIdOrEmail)) {
|
||||
const user = await User.findOne({ where: { email: userIdOrEmail } });
|
||||
if (!user) {
|
||||
throw new HttpError('User not found', 404);
|
||||
}
|
||||
userId = user.id;
|
||||
}
|
||||
|
||||
const normalizedClubId = Number.parseInt(clubId, 10);
|
||||
if (!Number.isInteger(normalizedClubId) || normalizedClubId <= 0) {
|
||||
throw new HttpError('clubId must be a valid number', 400);
|
||||
}
|
||||
|
||||
const hasAccess = await hasUserClubAccess(userId, normalizedClubId);
|
||||
if (!hasAccess) {
|
||||
throw new HttpError('Keine Berechtigung für diesen Verein', 403);
|
||||
}
|
||||
|
||||
// Parse URL
|
||||
@@ -669,6 +687,7 @@ class MyTischtennisUrlController {
|
||||
// Find or create league
|
||||
let league = await League.findOne({
|
||||
where: {
|
||||
clubId: normalizedClubId,
|
||||
myTischtennisGroupId: parsedData.groupId,
|
||||
association: parsedData.association
|
||||
}
|
||||
@@ -677,6 +696,7 @@ class MyTischtennisUrlController {
|
||||
if (!league) {
|
||||
league = await League.create({
|
||||
name: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version
|
||||
clubId: normalizedClubId,
|
||||
myTischtennisGroupId: parsedData.groupId,
|
||||
association: parsedData.association,
|
||||
groupname: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version
|
||||
|
||||
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
87
backend/controllers/permissionController.js
Normal file → Executable file
87
backend/controllers/permissionController.js
Normal file → Executable file
@@ -76,6 +76,29 @@ export const updateUserRole = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUserRoles = async (req, res) => {
|
||||
try {
|
||||
const { clubId, userId: targetUserId } = req.params;
|
||||
const { roleIds } = req.body;
|
||||
const updatingUserId = req.user.id;
|
||||
|
||||
const result = await permissionService.setUserRoles(
|
||||
parseInt(targetUserId),
|
||||
parseInt(clubId),
|
||||
Array.isArray(roleIds) ? roleIds : [],
|
||||
updatingUserId
|
||||
);
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Error updating user roles:', error);
|
||||
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update user custom permissions
|
||||
*/
|
||||
@@ -128,6 +151,62 @@ export const getPermissionStructure = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getClubRoles = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const roles = await permissionService.getClubRoles(parseInt(clubId, 10), req.user.id);
|
||||
res.json(roles);
|
||||
} catch (error) {
|
||||
console.error('Error getting club roles:', error);
|
||||
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const createClubRole = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
const role = await permissionService.createClubRole(parseInt(clubId, 10), req.body || {}, req.user.id);
|
||||
res.status(201).json(role);
|
||||
} catch (error) {
|
||||
console.error('Error creating club role:', error);
|
||||
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClubRole = async (req, res) => {
|
||||
try {
|
||||
const { clubId, roleId } = req.params;
|
||||
const role = await permissionService.updateClubRole(parseInt(clubId, 10), parseInt(roleId, 10), req.body || {}, req.user.id);
|
||||
res.json(role);
|
||||
} catch (error) {
|
||||
console.error('Error updating club role:', error);
|
||||
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteClubRole = async (req, res) => {
|
||||
try {
|
||||
const { clubId, roleId } = req.params;
|
||||
const result = await permissionService.deleteClubRole(parseInt(clubId, 10), parseInt(roleId, 10), req.user.id);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Error deleting club role:', error);
|
||||
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
|
||||
return res.status(403).json({ error: error.message });
|
||||
}
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update user status (activate/deactivate)
|
||||
*/
|
||||
@@ -158,10 +237,14 @@ export default {
|
||||
getUserPermissions,
|
||||
getClubMembersWithPermissions,
|
||||
updateUserRole,
|
||||
updateUserRoles,
|
||||
updateUserPermissions,
|
||||
updateUserStatus,
|
||||
getAvailableRoles,
|
||||
getPermissionStructure
|
||||
getPermissionStructure,
|
||||
getClubRoles,
|
||||
createClubRole,
|
||||
updateClubRole,
|
||||
deleteClubRole,
|
||||
};
|
||||
|
||||
|
||||
|
||||
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