feat(clubSettings): enhance club settings with myTischtennis integration
- Added new fields for myTischtennis club ID, federation nickname, and auto-fetch rankings in the club settings. - Updated the backend to handle the new settings in the updateClubSettings method. - Implemented automatic ranking updates for clubs based on the new settings in the autoUpdateRatingsService. - Enhanced the frontend to support the new settings, including validation and user interface updates for better user experience.
This commit is contained in:
@@ -60,8 +60,14 @@ 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 });
|
||||
const { greetingText, associationMemberNumber, myTischtennisClubId, myTischtennisFedNickname, autoFetchRankings } = req.body;
|
||||
const updated = await ClubService.updateClubSettings(token, clubid, {
|
||||
greetingText,
|
||||
associationMemberNumber,
|
||||
myTischtennisClubId,
|
||||
myTischtennisFedNickname,
|
||||
autoFetchRankings
|
||||
});
|
||||
res.status(200).json(updated);
|
||||
} catch (error) {
|
||||
if (error.message === 'noaccess') {
|
||||
|
||||
10
backend/migrations/20260315_add_club_rankings_settings.sql
Normal file
10
backend/migrations/20260315_add_club_rankings_settings.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Migration: Add myTischtennis rankings settings to clubs table
|
||||
-- Enables per-club configuration of TTR/QTTR rankings fetch (club ID, federation, enable flag)
|
||||
|
||||
ALTER TABLE clubs
|
||||
ADD COLUMN IF NOT EXISTS my_tischtennis_club_id VARCHAR(50) NULL
|
||||
COMMENT 'myTischtennis club number for rankings (e.g. 43030)',
|
||||
ADD COLUMN IF NOT EXISTS my_tischtennis_fed_nickname VARCHAR(50) NULL
|
||||
COMMENT 'Federation short name for rankings (e.g. HeTTV)',
|
||||
ADD COLUMN IF NOT EXISTS auto_fetch_rankings BOOLEAN NOT NULL DEFAULT FALSE
|
||||
COMMENT 'Enable automatic TTR/QTTR rankings fetch for this club';
|
||||
@@ -17,6 +17,25 @@ const Club = sequelize.define('Club', {
|
||||
allowNull: true,
|
||||
field: 'association_member_number'
|
||||
},
|
||||
myTischtennisClubId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
field: 'my_tischtennis_club_id',
|
||||
comment: 'myTischtennis club number for rankings (e.g. 43030)'
|
||||
},
|
||||
myTischtennisFedNickname: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
field: 'my_tischtennis_fed_nickname',
|
||||
comment: 'Federation short name for rankings (e.g. HeTTV)'
|
||||
},
|
||||
autoFetchRankings: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
field: 'auto_fetch_rankings',
|
||||
comment: 'Enable automatic TTR/QTTR rankings fetch for this club'
|
||||
}
|
||||
}, {
|
||||
tableName: 'clubs',
|
||||
underscored: true,
|
||||
|
||||
@@ -2,38 +2,35 @@ import myTischtennisService from './myTischtennisService.js';
|
||||
import myTischtennisFetchLogService from './myTischtennisFetchLogService.js';
|
||||
import memberService from './memberService.js';
|
||||
import MyTischtennis from '../models/MyTischtennis.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
import Club from '../models/Club.js';
|
||||
import UserClub from '../models/UserClub.js';
|
||||
import { Op } from 'sequelize';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
|
||||
class AutoUpdateRatingsService {
|
||||
/**
|
||||
* Execute automatic rating updates for all users with enabled auto-updates
|
||||
* Execute automatic rating updates for all clubs with autoFetchRankings enabled.
|
||||
* For each club, finds a user with MyTischtennis account and updates that club's members.
|
||||
*/
|
||||
async executeAutomaticUpdates() {
|
||||
devLog('Starting automatic rating updates...');
|
||||
|
||||
try {
|
||||
// Find all users with auto-updates enabled
|
||||
const accounts = await MyTischtennis.findAll({
|
||||
where: {
|
||||
autoUpdateRatings: true,
|
||||
savePassword: true
|
||||
}
|
||||
// No attributes restriction — all fields needed for session handling and re-login
|
||||
const clubs = await Club.findAll({
|
||||
where: { autoFetchRankings: true }
|
||||
});
|
||||
|
||||
devLog(`Found ${accounts.length} accounts with auto-updates enabled`);
|
||||
devLog(`Found ${clubs.length} clubs with autoFetchRankings enabled`);
|
||||
|
||||
if (accounts.length === 0) {
|
||||
devLog('No accounts found with auto-updates enabled');
|
||||
return;
|
||||
if (clubs.length === 0) {
|
||||
devLog('No clubs found with autoFetchRankings enabled');
|
||||
return { success: true, totalUpdated: 0, summaries: [] };
|
||||
}
|
||||
|
||||
// Process each account and collect summaries
|
||||
const summaries = [];
|
||||
for (const account of accounts) {
|
||||
const summary = await this.processAccount(account);
|
||||
summaries.push({ userId: account.userId, ...summary });
|
||||
for (const club of clubs) {
|
||||
const summary = await this.processClub(club);
|
||||
summaries.push({ clubId: club.id, clubName: club.name, ...summary });
|
||||
}
|
||||
|
||||
devLog('Automatic rating updates completed');
|
||||
@@ -41,28 +38,51 @@ class AutoUpdateRatingsService {
|
||||
return { success: true, totalUpdated, summaries };
|
||||
} catch (error) {
|
||||
console.error('Error in automatic rating updates:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single account for rating updates
|
||||
* Process a single club for rating updates.
|
||||
* Finds a user with MyTischtennis account (autoUpdateRatings, savePassword) and club access.
|
||||
*/
|
||||
async processAccount(account) {
|
||||
async processClub(club) {
|
||||
const startTime = Date.now();
|
||||
let success = false;
|
||||
let message = '';
|
||||
let errorDetails = null;
|
||||
let updatedCount = 0;
|
||||
let userId = null;
|
||||
|
||||
try {
|
||||
devLog(`Processing account ${account.email} (User ID: ${account.userId})`);
|
||||
const userClubs = await UserClub.findAll({
|
||||
where: { clubId: club.id, approved: true },
|
||||
attributes: ['userId']
|
||||
});
|
||||
|
||||
const userIds = userClubs.map((uc) => uc.userId);
|
||||
if (userIds.length === 0) {
|
||||
throw new Error(`Keine freigeschalteten Benutzer für Verein "${club.name}" gefunden`);
|
||||
}
|
||||
|
||||
const account = await MyTischtennis.findOne({
|
||||
where: {
|
||||
userId: { [Op.in]: userIds },
|
||||
autoUpdateRatings: true,
|
||||
savePassword: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new Error(`Kein myTischtennis-Account mit Auto-Update für Verein "${club.name}" gefunden`);
|
||||
}
|
||||
|
||||
userId = account.userId;
|
||||
devLog(`Processing club ${club.name} (Club ID: ${club.id}) via User ${userId}`);
|
||||
|
||||
// Check if session is still valid
|
||||
if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) {
|
||||
devLog(`Session expired for ${account.email}, attempting re-login via verifyLogin (incl. Playwright fallback)`);
|
||||
// verifyLogin handles CAPTCHA via Playwright and persists the session to DB.
|
||||
devLog(`Session expired, attempting re-login via verifyLogin (incl. Playwright fallback)`);
|
||||
await myTischtennisService.verifyLogin(account.userId);
|
||||
// Reload the account to get the fresh session data written by verifyLogin.
|
||||
const refreshed = await MyTischtennis.findOne({ where: { userId: account.userId } });
|
||||
if (!refreshed?.cookie) {
|
||||
throw new Error('Re-login via verifyLogin did not produce a valid session');
|
||||
@@ -71,91 +91,36 @@ class AutoUpdateRatingsService {
|
||||
account.refreshToken = refreshed.refreshToken;
|
||||
account.expiresAt = refreshed.expiresAt;
|
||||
account.cookie = refreshed.cookie;
|
||||
devLog(`Successfully re-logged in for ${account.email}`);
|
||||
devLog(`Successfully re-logged in`);
|
||||
}
|
||||
|
||||
// Perform rating update
|
||||
const updateResult = await this.updateRatings(account);
|
||||
updatedCount = updateResult.updatedCount || 0;
|
||||
|
||||
const result = await memberService.updateRatingsFromMyTischtennisByUserId(account.userId, club.id);
|
||||
updatedCount = result?.response?.updated ?? 0;
|
||||
success = true;
|
||||
message = `Successfully updated ${updatedCount} ratings`;
|
||||
devLog(`Updated ${updatedCount} ratings for ${account.email}`);
|
||||
devLog(`Updated ${updatedCount} ratings for club ${club.name}`);
|
||||
|
||||
} catch (error) {
|
||||
success = false;
|
||||
message = 'Update failed';
|
||||
errorDetails = error.message;
|
||||
console.error(`Error updating ratings for ${account.email}:`, error);
|
||||
console.error(`Error updating ratings for club ${club.name}:`, error);
|
||||
}
|
||||
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
// Log the attempt to MyTischtennisFetchLog
|
||||
await myTischtennisFetchLogService.logFetch(
|
||||
account.userId,
|
||||
'ratings',
|
||||
success,
|
||||
message,
|
||||
{
|
||||
errorDetails,
|
||||
recordsProcessed: updatedCount,
|
||||
executionTime,
|
||||
isAutomatic: true
|
||||
}
|
||||
);
|
||||
|
||||
// Also log to update history (for backwards compatibility)
|
||||
await myTischtennisService.logUpdateAttempt(
|
||||
account.userId,
|
||||
success,
|
||||
message,
|
||||
errorDetails,
|
||||
updatedCount,
|
||||
executionTime
|
||||
);
|
||||
|
||||
// Return summary for scheduler
|
||||
return { success, message, updatedCount, errorDetails, executionTime };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ratings for a specific account
|
||||
*/
|
||||
async updateRatings(account) {
|
||||
devLog(`Updating ratings for ${account.email}`);
|
||||
|
||||
try {
|
||||
// Ermittle einen freigeschalteten Vereinszugang für den Benutzer
|
||||
const userClub = await UserClub.findOne({
|
||||
where: {
|
||||
userId: account.userId,
|
||||
approved: true
|
||||
},
|
||||
order: [['createdAt', 'ASC']],
|
||||
attributes: ['clubId']
|
||||
});
|
||||
|
||||
if (!userClub) {
|
||||
throw new Error('Kein freigeschalteter Vereinszugang gefunden');
|
||||
}
|
||||
|
||||
const clubId = userClub.clubId;
|
||||
|
||||
// Verwende den Service-Aufruf, der mit userId/clubId arbeitet
|
||||
const result = await memberService.updateRatingsFromMyTischtennisByUserId(
|
||||
account.userId,
|
||||
clubId
|
||||
if (userId) {
|
||||
await myTischtennisFetchLogService.logFetch(
|
||||
userId,
|
||||
'ratings',
|
||||
success,
|
||||
message,
|
||||
{ errorDetails, recordsProcessed: updatedCount, executionTime, isAutomatic: true, clubId: club.id }
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
updatedCount: result.updatedCount || 0
|
||||
};
|
||||
} catch (error) {
|
||||
devLog(`Error updating ratings: ${error.message}`);
|
||||
throw error;
|
||||
await myTischtennisService.logUpdateAttempt(userId, success, message, errorDetails, updatedCount, executionTime);
|
||||
}
|
||||
|
||||
return { success, message, updatedCount, errorDetails, executionTime };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -67,13 +67,23 @@ class ClubService {
|
||||
});
|
||||
}
|
||||
|
||||
async updateClubSettings(userToken, clubId, { greetingText, associationMemberNumber }) {
|
||||
async updateClubSettings(userToken, clubId, {
|
||||
greetingText,
|
||||
associationMemberNumber,
|
||||
myTischtennisClubId,
|
||||
myTischtennisFedNickname,
|
||||
autoFetchRankings
|
||||
}) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const club = await Club.findByPk(clubId);
|
||||
if (!club) {
|
||||
throw new Error('clubnotfound');
|
||||
}
|
||||
return await club.update({ greetingText, associationMemberNumber });
|
||||
const updates = { greetingText, associationMemberNumber };
|
||||
if (myTischtennisClubId !== undefined) updates.myTischtennisClubId = myTischtennisClubId || null;
|
||||
if (myTischtennisFedNickname !== undefined) updates.myTischtennisFedNickname = myTischtennisFedNickname || null;
|
||||
if (autoFetchRankings !== undefined) updates.autoFetchRankings = !!autoFetchRankings;
|
||||
return await club.update(updates);
|
||||
}
|
||||
|
||||
async approveUserClubAccess(userToken, clubId, toApproveUserId) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import UserClub from "../models/UserClub.js";
|
||||
import Club from "../models/Club.js";
|
||||
import { checkAccess, getUserByToken, hasUserClubAccess } from "../utils/userUtils.js";
|
||||
import Member from "../models/Member.js";
|
||||
import MemberImage from "../models/MemberImage.js";
|
||||
@@ -375,23 +376,44 @@ class MemberService {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!account.clubId || !account.fedNickname) {
|
||||
|
||||
const club = await Club.findByPk(clubId);
|
||||
if (!club) {
|
||||
return {
|
||||
status: 404,
|
||||
response: { message: 'Verein nicht gefunden.', updated: 0, errors: [] }
|
||||
};
|
||||
}
|
||||
|
||||
if (!club.autoFetchRankings) {
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
message: 'Ranglisten-Abruf für diesen Verein ist deaktiviert.',
|
||||
updated: 0,
|
||||
errors: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const effectiveClubId = (club.myTischtennisClubId || '').trim() || account.clubId;
|
||||
const effectiveFedNickname = (club.myTischtennisFedNickname || '').trim() || account.fedNickname;
|
||||
|
||||
if (!effectiveClubId || !effectiveFedNickname) {
|
||||
console.error('[updateRatingsFromMyTischtennis] - Missing clubId or fedNickname:', {
|
||||
clubId: account.clubId,
|
||||
fedNickname: account.fedNickname
|
||||
clubId: effectiveClubId,
|
||||
fedNickname: effectiveFedNickname,
|
||||
fromClub: !!(club.myTischtennisClubId && club.myTischtennisFedNickname)
|
||||
});
|
||||
return {
|
||||
status: 400,
|
||||
response: {
|
||||
message: 'Club-ID oder Verbandskürzel nicht verfügbar. Bitte einmal einloggen.',
|
||||
message: 'Club-ID oder Verbandskürzel nicht verfügbar. Bitte in den Vereinseinstellungen hinterlegen oder einmal in myTischtennis einloggen.',
|
||||
updated: 0,
|
||||
errors: [],
|
||||
debug: {
|
||||
hasClubId: !!account.clubId,
|
||||
hasFedNickname: !!account.fedNickname,
|
||||
clubId: account.clubId,
|
||||
fedNickname: account.fedNickname
|
||||
hasClubId: !!effectiveClubId,
|
||||
hasFedNickname: !!effectiveFedNickname
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -403,7 +425,7 @@ class MemberService {
|
||||
await (await import('./apiLogService.js')).default.logRequest({
|
||||
userId,
|
||||
method: 'GET',
|
||||
path: `/rankings/andro-rangliste?club=${account.clubId}&fed=${account.fedNickname}¤t=yes`,
|
||||
path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}¤t=yes`,
|
||||
statusCode: null,
|
||||
requestBody: null,
|
||||
responseBody: null,
|
||||
@@ -416,15 +438,15 @@ class MemberService {
|
||||
const ttrStart = Date.now();
|
||||
const rankingsCurrent = await myTischtennisClient.getClubRankings(
|
||||
session.cookie,
|
||||
account.clubId,
|
||||
account.fedNickname,
|
||||
effectiveClubId,
|
||||
effectiveFedNickname,
|
||||
'yes'
|
||||
);
|
||||
try {
|
||||
await (await import('./apiLogService.js')).default.logRequest({
|
||||
userId,
|
||||
method: 'GET',
|
||||
path: `/rankings/andro-rangliste?club=${account.clubId}&fed=${account.fedNickname}¤t=yes`,
|
||||
path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}¤t=yes`,
|
||||
statusCode: rankingsCurrent.success ? 200 : 500,
|
||||
requestBody: null,
|
||||
responseBody: JSON.stringify(rankingsCurrent),
|
||||
@@ -440,7 +462,7 @@ class MemberService {
|
||||
await (await import('./apiLogService.js')).default.logRequest({
|
||||
userId,
|
||||
method: 'GET',
|
||||
path: `/rankings/andro-rangliste?club=${account.clubId}&fed=${account.fedNickname}¤t=no`,
|
||||
path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}¤t=no`,
|
||||
statusCode: null,
|
||||
requestBody: null,
|
||||
responseBody: null,
|
||||
@@ -453,8 +475,8 @@ class MemberService {
|
||||
const qttrStart = Date.now();
|
||||
const rankingsQuarter = await myTischtennisClient.getClubRankings(
|
||||
session.cookie,
|
||||
account.clubId,
|
||||
account.fedNickname,
|
||||
effectiveClubId,
|
||||
effectiveFedNickname,
|
||||
'no'
|
||||
);
|
||||
let qttrWarning = null;
|
||||
@@ -462,7 +484,7 @@ class MemberService {
|
||||
await (await import('./apiLogService.js')).default.logRequest({
|
||||
userId,
|
||||
method: 'GET',
|
||||
path: `/rankings/andro-rangliste?club=${account.clubId}&fed=${account.fedNickname}¤t=no`,
|
||||
path: `/rankings/andro-rangliste?club=${effectiveClubId}&fed=${effectiveFedNickname}¤t=no`,
|
||||
statusCode: rankingsQuarter.success ? 200 : 500,
|
||||
requestBody: null,
|
||||
responseBody: JSON.stringify(rankingsQuarter),
|
||||
@@ -481,8 +503,8 @@ class MemberService {
|
||||
updated: 0,
|
||||
errors: [],
|
||||
debug: {
|
||||
clubId: account.clubId,
|
||||
fedNickname: account.fedNickname,
|
||||
clubId: effectiveClubId,
|
||||
fedNickname: effectiveFedNickname,
|
||||
rankingsError: rankingsCurrent.error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,7 +737,14 @@
|
||||
"saved": "Gespeichert",
|
||||
"saveFailed": "Speichern fehlgeschlagen",
|
||||
"loadFailed": "Einstellungen konnten nicht geladen werden",
|
||||
"noClubSelected": "Bitte wählen Sie zuerst einen Verein aus."
|
||||
"noClubSelected": "Bitte wählen Sie zuerst einen Verein aus.",
|
||||
"myTischtennisRankings": "myTischtennis TTR/QTTR-Ranglisten",
|
||||
"myTischtennisRankingsHint": "Automatischer Abruf der Vereins-Rangliste für TTR- und QTTR-Updates der Mitglieder.",
|
||||
"autoFetchRankings": "Ranglisten automatisch abrufen",
|
||||
"myTischtennisClubId": "myTischtennis Vereinsnummer",
|
||||
"myTischtennisClubIdPlaceholder": "z. B. 43030",
|
||||
"myTischtennisFedNickname": "Verbandskürzel",
|
||||
"myTischtennisFedNicknamePlaceholder": "z. B. HeTTV"
|
||||
},
|
||||
"predefinedActivities": {
|
||||
"title": "Vordefinierte Aktivitäten",
|
||||
|
||||
@@ -51,6 +51,27 @@
|
||||
<input v-model="associationMemberNumber" class="text-input" :placeholder="$t('clubSettings.associationMemberNumberPlaceholder')" />
|
||||
</section>
|
||||
|
||||
<section v-if="currentClub && !loading" class="card">
|
||||
<h2>{{ $t('clubSettings.myTischtennisRankings') }}</h2>
|
||||
<p class="hint">{{ $t('clubSettings.myTischtennisRankingsHint') }}</p>
|
||||
<div class="rankings-row">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="autoFetchRankings" />
|
||||
{{ $t('clubSettings.autoFetchRankings') }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="autoFetchRankings" class="rankings-fields">
|
||||
<div class="field-group">
|
||||
<label>{{ $t('clubSettings.myTischtennisClubId') }}</label>
|
||||
<input v-model="myTischtennisClubId" class="text-input" :placeholder="$t('clubSettings.myTischtennisClubIdPlaceholder')" />
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label>{{ $t('clubSettings.myTischtennisFedNickname') }}</label>
|
||||
<input v-model="myTischtennisFedNickname" class="text-input" :placeholder="$t('clubSettings.myTischtennisFedNicknamePlaceholder')" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="currentClub && !loading" class="card actions-card">
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" @click="save">{{ $t('clubSettings.save') }}</button>
|
||||
@@ -91,6 +112,9 @@ export default {
|
||||
activeTab: 'settings',
|
||||
greeting: '',
|
||||
associationMemberNumber: '',
|
||||
myTischtennisClubId: '',
|
||||
myTischtennisFedNickname: '',
|
||||
autoFetchRankings: false,
|
||||
saved: false,
|
||||
loading: false,
|
||||
loadError: null,
|
||||
@@ -112,6 +136,9 @@ export default {
|
||||
if (!this.currentClub) {
|
||||
this.greeting = '';
|
||||
this.associationMemberNumber = '';
|
||||
this.myTischtennisClubId = '';
|
||||
this.myTischtennisFedNickname = '';
|
||||
this.autoFetchRankings = false;
|
||||
this.loadError = null;
|
||||
return;
|
||||
}
|
||||
@@ -122,10 +149,16 @@ export default {
|
||||
const club = response.data;
|
||||
this.greeting = club?.greetingText ?? '';
|
||||
this.associationMemberNumber = club?.associationMemberNumber ?? '';
|
||||
this.myTischtennisClubId = club?.myTischtennisClubId ?? '';
|
||||
this.myTischtennisFedNickname = club?.myTischtennisFedNickname ?? '';
|
||||
this.autoFetchRankings = !!club?.autoFetchRankings;
|
||||
} catch (e) {
|
||||
this.loadError = this.$t('clubSettings.loadFailed');
|
||||
this.greeting = '';
|
||||
this.associationMemberNumber = '';
|
||||
this.myTischtennisClubId = '';
|
||||
this.myTischtennisFedNickname = '';
|
||||
this.autoFetchRankings = false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -139,6 +172,9 @@ export default {
|
||||
await apiClient.put(`/clubs/${this.currentClub}/settings`, {
|
||||
greetingText: this.greeting,
|
||||
associationMemberNumber: this.associationMemberNumber,
|
||||
myTischtennisClubId: this.myTischtennisClubId || null,
|
||||
myTischtennisFedNickname: this.myTischtennisFedNickname || null,
|
||||
autoFetchRankings: this.autoFetchRankings,
|
||||
});
|
||||
this.saved = true;
|
||||
setTimeout(() => (this.saved = false), 1500);
|
||||
@@ -174,6 +210,11 @@ export default {
|
||||
font-size: 14px;
|
||||
}
|
||||
.text-input { width: 100%; border: 1px solid #ddd; border-radius: 6px; padding: 8px; font-size: 14px; }
|
||||
.rankings-row { margin-bottom: 12px; }
|
||||
.rankings-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 12px; }
|
||||
.field-group label { display: block; margin-bottom: 4px; font-weight: 500; color: #333; }
|
||||
.checkbox-label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
|
||||
.checkbox-label input[type="checkbox"] { width: auto; }
|
||||
.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); }
|
||||
|
||||
Reference in New Issue
Block a user