Fügt die Funktionalität zur Aktualisierung der Vereinseinstellungen hinzu. Implementiert die Methode updateClubSettings im clubsController, um Begrüßungstexte und Mitgliedsnummern zu aktualisieren. Aktualisiert das Club-Modell, um neue Felder für greetingText und associationMemberNumber zu unterstützen. Ergänzt die Routen in clubRoutes, um die neuen Einstellungen zu verarbeiten. Fügt eine neue Ansicht für die Vereins-Einstellungen im Frontend hinzu und aktualisiert die Navigation entsprechend.
This commit is contained in:
@@ -56,6 +56,25 @@ export const getClub = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const updateClubSettings = async (req, res) => {
|
||||
try {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubid } = req.params;
|
||||
const { greetingText, associationMemberNumber } = req.body;
|
||||
const updated = await ClubService.updateClubSettings(token, clubid, { greetingText, associationMemberNumber });
|
||||
res.status(200).json(updated);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
res.status(403).json({ error: 'noaccess' });
|
||||
} else if (error.message === 'clubnotfound') {
|
||||
res.status(404).json({ error: 'clubnotfound' });
|
||||
} else {
|
||||
console.error('[updateClubSettings] - error:', error);
|
||||
res.status(500).json({ error: 'internalerror' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const requestClubAccess = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubid: clubId } = req.params;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE clubs
|
||||
ADD COLUMN IF NOT EXISTS greeting_text TEXT NULL,
|
||||
ADD COLUMN IF NOT EXISTS association_member_number VARCHAR(255) NULL;
|
||||
|
||||
|
||||
@@ -7,6 +7,16 @@ const Club = sequelize.define('Club', {
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
greetingText: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'greeting_text'
|
||||
},
|
||||
associationMemberNumber: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
field: 'association_member_number'
|
||||
},
|
||||
}, {
|
||||
tableName: 'clubs',
|
||||
underscored: true,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import { getClubs, addClub, getClub, requestClubAccess, getPendingApprovals, approveClubAccess, rejectClubAccess } from '../controllers/clubsController.js';
|
||||
import { getClubs, addClub, getClub, requestClubAccess, getPendingApprovals, approveClubAccess, rejectClubAccess, updateClubSettings } from '../controllers/clubsController.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', authenticate, getClubs);
|
||||
router.post('/', authenticate, addClub);
|
||||
router.put('/:clubid/settings', authenticate, updateClubSettings);
|
||||
router.get('/:clubid', authenticate, getClub);
|
||||
router.get('/request/:clubid', authenticate, requestClubAccess);
|
||||
router.get('/pending/:clubid', authenticate, getPendingApprovals);
|
||||
|
||||
@@ -31,7 +31,6 @@ function formatCookies(cookies) {
|
||||
// Meeting-Info API-Endpunkt
|
||||
router.get('/meetinginfo/:code', async (req, res) => {
|
||||
const { code } = req.params;
|
||||
console.log(`📊 Meeting-Info API für Code: ${code}`);
|
||||
|
||||
try {
|
||||
// Hole Cookies für diesen Code (falls vorhanden)
|
||||
@@ -65,7 +64,6 @@ router.get('/meetinginfo/:code', async (req, res) => {
|
||||
const newCookies = extractCookies(response.headers.raw()['set-cookie']);
|
||||
if (Object.keys(newCookies).length > 0) {
|
||||
cookieStore.set(code, { ...cookies, ...newCookies });
|
||||
console.log(`🍪 Cookies für Code ${code} gespeichert:`, Object.keys(newCookies));
|
||||
}
|
||||
|
||||
// CORS-Header setzen
|
||||
@@ -78,7 +76,6 @@ router.get('/meetinginfo/:code', async (req, res) => {
|
||||
});
|
||||
|
||||
res.json(data);
|
||||
console.log(`✅ Meeting-Info für Code ${code} erfolgreich abgerufen`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Fehler beim Abrufen der Meeting-Info für Code ${code}:`, error);
|
||||
@@ -92,7 +89,6 @@ router.get('/meetinginfo/:code', async (req, res) => {
|
||||
// Cookie-Initialisierung (für den ersten Request)
|
||||
router.post('/init-cookies/:code', async (req, res) => {
|
||||
const { code } = req.params;
|
||||
console.log(`🍪 Cookie-Initialisierung für Code: ${code}`);
|
||||
|
||||
try {
|
||||
// Erster Request an die nuscore-Seite um Cookies zu erhalten
|
||||
@@ -111,7 +107,6 @@ router.post('/init-cookies/:code', async (req, res) => {
|
||||
const cookies = extractCookies(response.headers.raw()['set-cookie']);
|
||||
if (Object.keys(cookies).length > 0) {
|
||||
cookieStore.set(code, cookies);
|
||||
console.log(`🍪 Cookies für Code ${code} initialisiert:`, Object.keys(cookies));
|
||||
}
|
||||
|
||||
res.json({
|
||||
@@ -132,7 +127,6 @@ router.post('/init-cookies/:code', async (req, res) => {
|
||||
// Detaillierte Meeting-Daten API-Endpunkt
|
||||
router.get('/meetingdetails/:uuid', async (req, res) => {
|
||||
const { uuid } = req.params;
|
||||
console.log(`📊 Meeting-Details API für UUID: ${uuid}`);
|
||||
|
||||
try {
|
||||
// Hole Cookies für diesen Code (falls vorhanden)
|
||||
@@ -166,7 +160,6 @@ router.get('/meetingdetails/:uuid', async (req, res) => {
|
||||
const newCookies = extractCookies(response.headers.raw()['set-cookie']);
|
||||
if (Object.keys(newCookies).length > 0) {
|
||||
cookieStore.set(uuid, { ...cookies, ...newCookies });
|
||||
console.log(`🍪 Cookies für UUID ${uuid} gespeichert:`, Object.keys(newCookies));
|
||||
}
|
||||
|
||||
// CORS-Header setzen
|
||||
@@ -179,7 +172,6 @@ router.get('/meetingdetails/:uuid', async (req, res) => {
|
||||
});
|
||||
|
||||
res.json(data);
|
||||
console.log(`✅ Meeting-Details für UUID ${uuid} erfolgreich abgerufen`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Fehler beim Abrufen der Meeting-Details für UUID ${uuid}:`, error);
|
||||
|
||||
@@ -55,6 +55,15 @@ class ClubService {
|
||||
});
|
||||
}
|
||||
|
||||
async updateClubSettings(userToken, clubId, { greetingText, associationMemberNumber }) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const club = await Club.findByPk(clubId);
|
||||
if (!club) {
|
||||
throw new Error('clubnotfound');
|
||||
}
|
||||
return await club.update({ greetingText, associationMemberNumber });
|
||||
}
|
||||
|
||||
async approveUserClubAccess(userToken, clubId, toApproveUserId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const toApproveUserClub = await UserClub.findOne({
|
||||
|
||||
@@ -8,7 +8,6 @@ class NuscoreProxyService {
|
||||
|
||||
async initialize() {
|
||||
if (!this.browser) {
|
||||
console.log('🚀 Initialisiere Playwright Browser...');
|
||||
this.browser = await chromium.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
@@ -26,7 +25,6 @@ class NuscoreProxyService {
|
||||
ignoreHTTPSErrors: true
|
||||
});
|
||||
|
||||
console.log('✅ Playwright Browser initialisiert');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +32,6 @@ class NuscoreProxyService {
|
||||
try {
|
||||
await this.initialize();
|
||||
|
||||
console.log(`🔍 Lade nuscore-Seite für Code: ${code}`);
|
||||
|
||||
const page = await this.context.newPage();
|
||||
|
||||
@@ -45,17 +42,14 @@ class NuscoreProxyService {
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
console.log('📄 Seite geladen');
|
||||
|
||||
// Optional: PIN automatisch einfügen falls vorhanden
|
||||
if (pin) {
|
||||
console.log(`🔑 Versuche PIN ${pin} einzufügen...`);
|
||||
try {
|
||||
// Suche nach PIN-Eingabefeld
|
||||
const pinInput = await page.locator('input[type="password"][placeholder*="PIN"], input[placeholder*="Pin"], input[placeholder*="pin"]').first();
|
||||
if (await pinInput.isVisible()) {
|
||||
await pinInput.fill(pin);
|
||||
console.log('✅ PIN eingefügt');
|
||||
|
||||
// Optional: Submit-Button klicken falls vorhanden
|
||||
const submitBtn = await page.locator('button[type="submit"], input[type="submit"], button:has-text("Einloggen"), button:has-text("Anmelden")').first();
|
||||
@@ -65,7 +59,6 @@ class NuscoreProxyService {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ PIN-Einfügung fehlgeschlagen:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +70,6 @@ class NuscoreProxyService {
|
||||
|
||||
await page.close();
|
||||
|
||||
console.log('✅ nuscore-Seite erfolgreich proxiert');
|
||||
return modifiedHtml;
|
||||
|
||||
} catch (error) {
|
||||
@@ -138,7 +130,6 @@ class NuscoreProxyService {
|
||||
await this.browser.close();
|
||||
this.browser = null;
|
||||
}
|
||||
console.log('🧹 Playwright Browser bereinigt');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@
|
||||
<nav class="sidebar-footer">
|
||||
<div class="nav-section">
|
||||
<h4 class="nav-title">Einstellungen</h4>
|
||||
<router-link to="/club-settings" class="nav-link">
|
||||
<span class="nav-icon">🏟️</span>
|
||||
Vereins-Einstellungen
|
||||
</router-link>
|
||||
<a href="/mytischtennis-account" class="nav-link">
|
||||
<span class="nav-icon">🔗</span>
|
||||
myTischtennis-Account
|
||||
|
||||
@@ -71,14 +71,12 @@ export default {
|
||||
...mapActions(['closeDialog', 'minimizeDialog', 'restoreDialog', 'bringDialogToFront']),
|
||||
|
||||
getDialogComponent(componentName) {
|
||||
console.log('🔍 Suche Komponente:', componentName);
|
||||
const components = {
|
||||
'MatchReportDialog': MatchReportDialog,
|
||||
'MatchReportApiDialog': MatchReportApiDialog,
|
||||
'MatchReportHeaderActions': MatchReportHeaderActions
|
||||
};
|
||||
const component = components[componentName] || null;
|
||||
console.log('🔍 Gefundene Komponente:', component ? component.name : 'null');
|
||||
return component;
|
||||
},
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import PendingApprovalsView from './views/PendingApprovalsView.vue';
|
||||
import ScheduleView from './views/ScheduleView.vue';
|
||||
import TournamentsView from './views/TournamentsView.vue';
|
||||
import TrainingStatsView from './views/TrainingStatsView.vue';
|
||||
import ClubSettings from './views/ClubSettings.vue';
|
||||
import PredefinedActivities from './views/PredefinedActivities.vue';
|
||||
import OfficialTournaments from './views/OfficialTournaments.vue';
|
||||
import MyTischtennisAccount from './views/MyTischtennisAccount.vue';
|
||||
@@ -31,6 +32,7 @@ const routes = [
|
||||
{ path: '/schedule', component: ScheduleView},
|
||||
{ path: '/tournaments', component: TournamentsView },
|
||||
{ path: '/training-stats', component: TrainingStatsView },
|
||||
{ path: '/club-settings', component: ClubSettings },
|
||||
{ path: '/predefined-activities', component: PredefinedActivities },
|
||||
{ path: '/official-tournaments', component: OfficialTournaments },
|
||||
{ path: '/mytischtennis-account', component: MyTischtennisAccount },
|
||||
|
||||
107
frontend/src/views/ClubSettings.vue
Normal file
107
frontend/src/views/ClubSettings.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="club-settings">
|
||||
<h1>Vereins-Einstellungen</h1>
|
||||
|
||||
<section class="card">
|
||||
<h2>Begrüßungstext</h2>
|
||||
<div class="greeting-grid">
|
||||
<textarea v-model="greeting" class="greeting-input" rows="10" placeholder="Begrüßungstext für Heimspiele..."></textarea>
|
||||
<div class="legend">
|
||||
<h3>Platzhalter</h3>
|
||||
<ul>
|
||||
<li><code>{home}</code> Name Heimmannschaft</li>
|
||||
<li><code>{guest}</code> Name Gastmannschaft</li>
|
||||
<li><code>{homeplayers}</code> Spieler und Doppel Heimmannschaft</li>
|
||||
<li><code>{guestplayers}</code> Spieler und Doppel Gastmannschaft</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">Dieser Text erscheint im Reiter "Begrüßung" des Spielberichtsbogens.</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Verbands-Mitgliedsnummer</h2>
|
||||
<input v-model="associationMemberNumber" class="text-input" placeholder="z. B. 12-3456" />
|
||||
</section>
|
||||
|
||||
<section class="card actions-card">
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" @click="save">Speichern</button>
|
||||
<span v-if="saved" class="saved-hint">Gespeichert</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '../apiClient';
|
||||
export default {
|
||||
name: 'ClubSettings',
|
||||
data() {
|
||||
return {
|
||||
greeting: '',
|
||||
associationMemberNumber: '',
|
||||
saved: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
const stored = localStorage.getItem('clubGreeting');
|
||||
if (stored) this.greeting = stored;
|
||||
const storedNr = localStorage.getItem('associationMemberNumber');
|
||||
if (storedNr) this.associationMemberNumber = storedNr;
|
||||
},
|
||||
methods: {
|
||||
async save() {
|
||||
localStorage.setItem('clubGreeting', this.greeting || '');
|
||||
localStorage.setItem('associationMemberNumber', this.associationMemberNumber || '');
|
||||
try {
|
||||
// Beispiel: aktuellen Club aus localStorage
|
||||
const clubId = localStorage.getItem('currentClub');
|
||||
if (clubId) {
|
||||
await apiClient.put(`/clubs/${clubId}/settings`, {
|
||||
greetingText: this.greeting,
|
||||
associationMemberNumber: this.associationMemberNumber,
|
||||
});
|
||||
}
|
||||
this.saved = true;
|
||||
setTimeout(() => (this.saved = false), 1500);
|
||||
} catch (e) {
|
||||
this.saved = false;
|
||||
alert('Speichern fehlgeschlagen');
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.club-settings { padding: 20px; }
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
max-width: 800px;
|
||||
}
|
||||
.greeting-grid { display: grid; grid-template-columns: 1fr 280px; gap: 12px; align-items: start; }
|
||||
.legend { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 10px; }
|
||||
.legend h3 { margin: 0 0 8px 0; font-size: 0.95rem; color: #333; }
|
||||
.legend ul { margin: 0; padding-left: 18px; }
|
||||
.legend li { margin-bottom: 6px; color: #444; }
|
||||
.legend code { background: #eef2f7; padding: 1px 4px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
.greeting-input {
|
||||
width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.text-input { width: 100%; border: 1px solid #ddd; border-radius: 6px; padding: 8px; font-size: 14px; }
|
||||
.actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
|
||||
.btn.btn-primary { background: var(--primary-color); color: #fff; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; }
|
||||
.btn.btn-primary:hover { background: var(--primary-hover); }
|
||||
.saved-hint { color: #28a745; font-weight: 600; }
|
||||
.hint { color: #666; font-size: 12px; margin-top: 8px; }
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user