Add member transfer configuration and UI enhancements
Introduced MemberTransferConfig model and integrated it into the backend, allowing for the storage and retrieval of member transfer settings. Updated server routes to include member transfer configuration endpoints. Enhanced the frontend with a new MemberTransferDialog component for user interaction, added a dedicated route for member transfer settings, and updated the App.vue to include a link for accessing these settings. Improved the loading state and configuration handling in the dialog for better user experience.
This commit is contained in:
51
backend/controllers/memberTransferConfigController.js
Normal file
51
backend/controllers/memberTransferConfigController.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import MemberTransferConfigService from '../services/memberTransferConfigService.js';
|
||||
|
||||
export const getConfig = async (req, res) => {
|
||||
try {
|
||||
const { id: clubId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
|
||||
const result = await MemberTransferConfigService.getConfig(userToken, clubId);
|
||||
res.status(result.status).json(result.response);
|
||||
} catch (error) {
|
||||
console.error('[getConfig] Error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Konfiguration'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const saveConfig = async (req, res) => {
|
||||
try {
|
||||
const { id: clubId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const configData = req.body;
|
||||
|
||||
const result = await MemberTransferConfigService.saveConfig(userToken, clubId, configData);
|
||||
res.status(result.status).json(result.response);
|
||||
} catch (error) {
|
||||
console.error('[saveConfig] Error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Speichern der Konfiguration'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteConfig = async (req, res) => {
|
||||
try {
|
||||
const { id: clubId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
|
||||
const result = await MemberTransferConfigService.deleteConfig(userToken, clubId);
|
||||
res.status(result.status).json(result.response);
|
||||
} catch (error) {
|
||||
console.error('[deleteConfig] Error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Löschen der Konfiguration'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
117
backend/models/MemberTransferConfig.js
Normal file
117
backend/models/MemberTransferConfig.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import Club from './Club.js';
|
||||
import { encryptData, decryptData } from '../utils/encrypt.js';
|
||||
|
||||
const MemberTransferConfig = sequelize.define('MemberTransferConfig', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
clubId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
references: {
|
||||
model: Club,
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
server: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
field: 'server',
|
||||
comment: 'Base URL des Servers (z.B. https://example.com)'
|
||||
},
|
||||
loginEndpoint: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
field: 'login_endpoint',
|
||||
comment: 'Relativer Pfad zum Login-Endpoint (z.B. /api/auth/login)'
|
||||
},
|
||||
loginFormat: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: 'json',
|
||||
field: 'login_format'
|
||||
},
|
||||
encryptedLoginCredentials: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'encrypted_login_credentials',
|
||||
comment: 'Verschlüsselte Login-Daten als JSON-String'
|
||||
},
|
||||
transferEndpoint: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
field: 'transfer_endpoint',
|
||||
comment: 'Relativer Pfad zum Übertragungs-Endpoint (z.B. /api/members/bulk)'
|
||||
},
|
||||
transferMethod: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'POST',
|
||||
field: 'transfer_method'
|
||||
},
|
||||
transferFormat: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'json',
|
||||
field: 'transfer_format'
|
||||
},
|
||||
transferTemplate: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
field: 'transfer_template'
|
||||
},
|
||||
useBulkMode: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
field: 'use_bulk_mode'
|
||||
},
|
||||
bulkWrapperTemplate: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'bulk_wrapper_template',
|
||||
comment: 'Optionales Template für die äußere Struktur im Bulk-Modus (z.B. {"data": {"members": "{{members}}"}})'
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'member_transfer_config',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// Getter/Setter für verschlüsselte Login-Daten
|
||||
MemberTransferConfig.prototype.getLoginCredentials = function() {
|
||||
if (!this.encryptedLoginCredentials) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const decrypted = decryptData(this.encryptedLoginCredentials);
|
||||
return JSON.parse(decrypted);
|
||||
} catch (error) {
|
||||
console.error('[MemberTransferConfig] Error decrypting login credentials:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
MemberTransferConfig.prototype.setLoginCredentials = function(credentials) {
|
||||
if (!credentials || Object.keys(credentials).length === 0) {
|
||||
this.encryptedLoginCredentials = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const jsonString = JSON.stringify(credentials);
|
||||
this.encryptedLoginCredentials = encryptData(jsonString);
|
||||
} catch (error) {
|
||||
console.error('[MemberTransferConfig] Error encrypting login credentials:', error);
|
||||
this.encryptedLoginCredentials = null;
|
||||
}
|
||||
};
|
||||
|
||||
export default MemberTransferConfig;
|
||||
|
||||
@@ -39,6 +39,7 @@ import MyTischtennis from './MyTischtennis.js';
|
||||
import MyTischtennisUpdateHistory from './MyTischtennisUpdateHistory.js';
|
||||
import MyTischtennisFetchLog from './MyTischtennisFetchLog.js';
|
||||
import ApiLog from './ApiLog.js';
|
||||
import MemberTransferConfig from './MemberTransferConfig.js';
|
||||
// Official tournaments relations
|
||||
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
|
||||
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
@@ -242,6 +243,9 @@ MyTischtennisFetchLog.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||
User.hasMany(ApiLog, { foreignKey: 'userId', as: 'apiLogs' });
|
||||
ApiLog.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||
|
||||
Club.hasOne(MemberTransferConfig, { foreignKey: 'clubId', as: 'memberTransferConfig' });
|
||||
MemberTransferConfig.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
|
||||
|
||||
export {
|
||||
User,
|
||||
Log,
|
||||
@@ -283,4 +287,5 @@ export {
|
||||
MyTischtennisUpdateHistory,
|
||||
MyTischtennisFetchLog,
|
||||
ApiLog,
|
||||
MemberTransferConfig,
|
||||
};
|
||||
|
||||
14
backend/routes/memberTransferConfigRoutes.js
Normal file
14
backend/routes/memberTransferConfigRoutes.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import { authorize } from '../middleware/authorizationMiddleware.js';
|
||||
import { getConfig, saveConfig, deleteConfig } from '../controllers/memberTransferConfigController.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/:id', authenticate, authorize('members', 'read'), getConfig);
|
||||
router.post('/:id', authenticate, authorize('members', 'write'), saveConfig);
|
||||
router.put('/:id', authenticate, authorize('members', 'write'), saveConfig);
|
||||
router.delete('/:id', authenticate, authorize('members', 'write'), deleteConfig);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
|
||||
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, TeamDocument, Group,
|
||||
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
|
||||
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis, MyTischtennisUpdateHistory, MyTischtennisFetchLog, ApiLog
|
||||
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis, MyTischtennisUpdateHistory, MyTischtennisFetchLog, ApiLog, MemberTransferConfig
|
||||
} from './models/index.js';
|
||||
import authRoutes from './routes/authRoutes.js';
|
||||
import clubRoutes from './routes/clubRoutes.js';
|
||||
@@ -41,6 +41,7 @@ import seasonRoutes from './routes/seasonRoutes.js';
|
||||
import memberActivityRoutes from './routes/memberActivityRoutes.js';
|
||||
import permissionRoutes from './routes/permissionRoutes.js';
|
||||
import apiLogRoutes from './routes/apiLogRoutes.js';
|
||||
import memberTransferConfigRoutes from './routes/memberTransferConfigRoutes.js';
|
||||
import schedulerService from './services/schedulerService.js';
|
||||
import { requestLoggingMiddleware } from './middleware/requestLoggingMiddleware.js';
|
||||
|
||||
@@ -99,6 +100,7 @@ app.use('/api/seasons', seasonRoutes);
|
||||
app.use('/api/member-activities', memberActivityRoutes);
|
||||
app.use('/api/permissions', permissionRoutes);
|
||||
app.use('/api/logs', apiLogRoutes);
|
||||
app.use('/api/member-transfer-config', memberTransferConfigRoutes);
|
||||
|
||||
app.use(express.static(path.join(__dirname, '../frontend/dist')));
|
||||
|
||||
@@ -202,6 +204,7 @@ app.get('*', (req, res) => {
|
||||
await safeSync(MyTischtennisUpdateHistory);
|
||||
await safeSync(MyTischtennisFetchLog);
|
||||
await safeSync(ApiLog);
|
||||
await safeSync(MemberTransferConfig);
|
||||
|
||||
// Start scheduler service
|
||||
schedulerService.start();
|
||||
|
||||
227
backend/services/memberTransferConfigService.js
Normal file
227
backend/services/memberTransferConfigService.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import MemberTransferConfig from '../models/MemberTransferConfig.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
|
||||
class MemberTransferConfigService {
|
||||
/**
|
||||
* Ruft die Konfiguration für einen Verein ab
|
||||
*/
|
||||
async getConfig(userToken, clubId) {
|
||||
try {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const config = await MemberTransferConfig.findOne({
|
||||
where: { clubId }
|
||||
});
|
||||
|
||||
if (!config) {
|
||||
return {
|
||||
status: 404,
|
||||
response: {
|
||||
success: false,
|
||||
message: 'Keine Konfiguration gefunden'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Login-Credentials entschlüsseln
|
||||
const loginCredentials = config.getLoginCredentials();
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
config: {
|
||||
id: config.id,
|
||||
clubId: config.clubId,
|
||||
server: config.server,
|
||||
loginEndpoint: config.loginEndpoint,
|
||||
loginFormat: config.loginFormat,
|
||||
loginCredentials: loginCredentials || {},
|
||||
transferEndpoint: config.transferEndpoint,
|
||||
transferMethod: config.transferMethod,
|
||||
transferFormat: config.transferFormat,
|
||||
transferTemplate: config.transferTemplate,
|
||||
useBulkMode: config.useBulkMode,
|
||||
bulkWrapperTemplate: config.bulkWrapperTemplate
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
devLog('[getConfig] Error:', error);
|
||||
return {
|
||||
status: 500,
|
||||
response: {
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Konfiguration: ' + error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert oder aktualisiert die Konfiguration
|
||||
*/
|
||||
async saveConfig(userToken, clubId, configData) {
|
||||
try {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
// Validierung
|
||||
if (!configData.server) {
|
||||
return {
|
||||
status: 400,
|
||||
response: {
|
||||
success: false,
|
||||
error: 'Server-Basis-URL ist erforderlich'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!configData.transferEndpoint) {
|
||||
return {
|
||||
status: 400,
|
||||
response: {
|
||||
success: false,
|
||||
error: 'Übertragungs-Endpoint ist erforderlich'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!configData.transferTemplate) {
|
||||
return {
|
||||
status: 400,
|
||||
response: {
|
||||
success: false,
|
||||
error: 'Übertragungs-Template ist erforderlich'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Prüfen, ob bereits eine Konfiguration existiert
|
||||
let config = await MemberTransferConfig.findOne({
|
||||
where: { clubId }
|
||||
});
|
||||
|
||||
const configToSave = {
|
||||
clubId: clubId,
|
||||
server: configData.server,
|
||||
loginEndpoint: configData.loginEndpoint || null,
|
||||
loginFormat: configData.loginFormat || 'json',
|
||||
transferEndpoint: configData.transferEndpoint,
|
||||
transferMethod: configData.transferMethod || 'POST',
|
||||
transferFormat: configData.transferFormat || 'json',
|
||||
transferTemplate: configData.transferTemplate,
|
||||
useBulkMode: configData.useBulkMode || false,
|
||||
bulkWrapperTemplate: configData.bulkWrapperTemplate || null
|
||||
};
|
||||
|
||||
if (config) {
|
||||
// Update
|
||||
config.server = configToSave.server;
|
||||
config.loginEndpoint = configToSave.loginEndpoint;
|
||||
config.loginFormat = configToSave.loginFormat;
|
||||
config.transferEndpoint = configToSave.transferEndpoint;
|
||||
config.transferMethod = configToSave.transferMethod;
|
||||
config.transferFormat = configToSave.transferFormat;
|
||||
config.transferTemplate = configToSave.transferTemplate;
|
||||
config.useBulkMode = configToSave.useBulkMode;
|
||||
config.bulkWrapperTemplate = configToSave.bulkWrapperTemplate;
|
||||
|
||||
// Login-Credentials setzen (nur wenn vorhanden)
|
||||
if (configData.loginCredentials) {
|
||||
config.setLoginCredentials(configData.loginCredentials);
|
||||
} else {
|
||||
// Wenn keine Credentials übergeben wurden, aber welche vorhanden sind, behalten
|
||||
// Nur löschen, wenn explizit null/undefined übergeben wurde
|
||||
if (configData.loginCredentials === null || configData.loginCredentials === undefined) {
|
||||
config.encryptedLoginCredentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
await config.save();
|
||||
} else {
|
||||
// Create
|
||||
config = await MemberTransferConfig.create(configToSave);
|
||||
|
||||
// Login-Credentials setzen (nur wenn vorhanden)
|
||||
if (configData.loginCredentials) {
|
||||
config.setLoginCredentials(configData.loginCredentials);
|
||||
await config.save();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
message: 'Konfiguration erfolgreich gespeichert',
|
||||
config: {
|
||||
id: config.id,
|
||||
clubId: config.clubId,
|
||||
server: config.server,
|
||||
loginEndpoint: config.loginEndpoint,
|
||||
loginFormat: config.loginFormat,
|
||||
transferEndpoint: config.transferEndpoint,
|
||||
transferMethod: config.transferMethod,
|
||||
transferFormat: config.transferFormat,
|
||||
transferTemplate: config.transferTemplate,
|
||||
useBulkMode: config.useBulkMode,
|
||||
bulkWrapperTemplate: config.bulkWrapperTemplate
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
devLog('[saveConfig] Error:', error);
|
||||
return {
|
||||
status: 500,
|
||||
response: {
|
||||
success: false,
|
||||
error: 'Fehler beim Speichern der Konfiguration: ' + error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht die Konfiguration
|
||||
*/
|
||||
async deleteConfig(userToken, clubId) {
|
||||
try {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const deleted = await MemberTransferConfig.destroy({
|
||||
where: { clubId }
|
||||
});
|
||||
|
||||
if (deleted === 0) {
|
||||
return {
|
||||
status: 404,
|
||||
response: {
|
||||
success: false,
|
||||
message: 'Keine Konfiguration gefunden'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
message: 'Konfiguration erfolgreich gelöscht'
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
devLog('[deleteConfig] Error:', error);
|
||||
return {
|
||||
status: 500,
|
||||
response: {
|
||||
success: false,
|
||||
error: 'Fehler beim Löschen der Konfiguration: ' + error.message
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MemberTransferConfigService();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import Member from '../models/Member.js';
|
||||
import MemberTransferConfig from '../models/MemberTransferConfig.js';
|
||||
import { checkAccess, getUserByToken } from '../utils/userUtils.js';
|
||||
import { encryptData, decryptData } from '../utils/encrypt.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
@@ -49,12 +50,52 @@ class MemberTransferService {
|
||||
let authToken = null;
|
||||
let sessionCookie = null;
|
||||
|
||||
if (config.loginEndpoint && config.loginCredentials) {
|
||||
// Server und Endpoints: Wenn vollständige URLs übergeben wurden, behalten wir sie
|
||||
// Ansonsten kombinieren wir Server + relativen Pfad aus gespeicherter Konfiguration
|
||||
let loginEndpointUrl = config.loginEndpoint;
|
||||
let transferEndpointUrl = config.transferEndpoint;
|
||||
let loginCredentials = config.loginCredentials;
|
||||
let savedConfig = null;
|
||||
|
||||
// Wenn relative Pfade verwendet werden, Server aus gespeicherter Konfiguration holen
|
||||
if ((config.loginEndpoint && !config.loginEndpoint.startsWith('http')) ||
|
||||
(config.transferEndpoint && !config.transferEndpoint.startsWith('http')) ||
|
||||
config.useBulkMode) {
|
||||
savedConfig = await MemberTransferConfig.findOne({
|
||||
where: { clubId }
|
||||
});
|
||||
}
|
||||
|
||||
if (config.loginEndpoint && !config.loginEndpoint.startsWith('http') && savedConfig) {
|
||||
if (savedConfig.server) {
|
||||
// Server + relativen Pfad kombinieren
|
||||
loginEndpointUrl = savedConfig.server.replace(/\/$/, '') + '/' + config.loginEndpoint.replace(/^\//, '');
|
||||
|
||||
// Login-Credentials aus gespeicherter Konfiguration laden, falls vorhanden
|
||||
if (!loginCredentials && savedConfig.loginEndpoint === config.loginEndpoint) {
|
||||
loginCredentials = savedConfig.getLoginCredentials();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.transferEndpoint && !config.transferEndpoint.startsWith('http') && savedConfig) {
|
||||
if (savedConfig.server) {
|
||||
// Server + relativen Pfad kombinieren
|
||||
transferEndpointUrl = savedConfig.server.replace(/\/$/, '') + '/' + config.transferEndpoint.replace(/^\//, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk-Wrapper-Template aus gespeicherter Konfiguration laden, falls nicht übergeben
|
||||
if (!config.bulkWrapperTemplate && config.useBulkMode && savedConfig) {
|
||||
config.bulkWrapperTemplate = savedConfig.bulkWrapperTemplate;
|
||||
}
|
||||
|
||||
if (loginEndpointUrl && loginCredentials) {
|
||||
try {
|
||||
const loginResult = await this.performLogin(
|
||||
config.loginEndpoint,
|
||||
loginEndpointUrl,
|
||||
config.loginFormat || 'json',
|
||||
config.loginCredentials
|
||||
loginCredentials
|
||||
);
|
||||
|
||||
if (!loginResult.success) {
|
||||
@@ -112,12 +153,51 @@ class MemberTransferService {
|
||||
}
|
||||
|
||||
// Bulk-Objekt erstellen
|
||||
const bulkData = {
|
||||
members: membersArray
|
||||
};
|
||||
let bulkData;
|
||||
|
||||
// Wenn ein Wrapper-Template vorhanden ist, verwende es
|
||||
if (config.bulkWrapperTemplate) {
|
||||
// Wrapper-Template verwenden, {{members}} wird durch das Array ersetzt
|
||||
const wrapperTemplate = config.bulkWrapperTemplate;
|
||||
const membersArrayString = JSON.stringify(membersArray);
|
||||
|
||||
// Ersetze {{members}} Platzhalter im Wrapper-Template
|
||||
// Der Platzhalter kann in zwei Formen vorkommen:
|
||||
// 1. "{{members}}" (als JSON-String mit Anführungszeichen)
|
||||
// 2. [{{members}}] (als Array-Platzhalter ohne Anführungszeichen)
|
||||
let wrapperString = wrapperTemplate;
|
||||
|
||||
// Ersetze zuerst "{{members}}" (String-Platzhalter in Anführungszeichen)
|
||||
// durch das Array als JSON-String (ohne die äußeren Anführungszeichen)
|
||||
wrapperString = wrapperString.replace(/"\{\{members\}\}"/g, membersArrayString);
|
||||
|
||||
// Ersetze auch [{{members}}] (Array-Platzhalter)
|
||||
wrapperString = wrapperString.replace(/\[\{\{members\}\}\]/g, membersArrayString);
|
||||
|
||||
// Ersetze auch {{members}} ohne Anführungszeichen (falls direkt verwendet)
|
||||
wrapperString = wrapperString.replace(/\{\{members\}\}/g, membersArrayString);
|
||||
|
||||
// Parse zu Objekt
|
||||
try {
|
||||
bulkData = JSON.parse(wrapperString);
|
||||
} catch (parseError) {
|
||||
devLog('[transferMembers] Error parsing wrapper template after replacement:', parseError);
|
||||
devLog('[transferMembers] Wrapper template:', wrapperTemplate);
|
||||
devLog('[transferMembers] After replacement:', wrapperString);
|
||||
// Fallback: Standard-Format
|
||||
bulkData = {
|
||||
members: membersArray
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Standard: {"members": [...]}
|
||||
bulkData = {
|
||||
members: membersArray
|
||||
};
|
||||
}
|
||||
|
||||
const transferResult = await this.transferMember(
|
||||
config.transferEndpoint,
|
||||
transferEndpointUrl,
|
||||
config.transferMethod || 'POST',
|
||||
config.transferFormat || 'json',
|
||||
bulkData,
|
||||
@@ -191,7 +271,7 @@ class MemberTransferService {
|
||||
);
|
||||
|
||||
const transferResult = await this.transferMember(
|
||||
config.transferEndpoint,
|
||||
transferEndpointUrl,
|
||||
config.transferMethod || 'POST',
|
||||
config.transferFormat || 'json',
|
||||
memberData,
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
<span class="dropdown-icon">🔐</span>
|
||||
Berechtigungen
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('members', 'write')" to="/member-transfer-settings" class="dropdown-item" @click="userDropdownOpen = false">
|
||||
<span class="dropdown-icon">📤</span>
|
||||
Mitgliederübertragung
|
||||
</router-link>
|
||||
<router-link v-if="isAdmin" to="/logs" class="dropdown-item" @click="userDropdownOpen = false">
|
||||
<span class="dropdown-icon">📋</span>
|
||||
System-Logs
|
||||
|
||||
@@ -7,20 +7,41 @@
|
||||
:close-on-overlay="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="transfer-form">
|
||||
<div v-if="loadingConfig" class="loading-config">
|
||||
Gespeicherte Konfiguration wird geladen...
|
||||
</div>
|
||||
|
||||
<div v-else class="transfer-form">
|
||||
<div class="form-section">
|
||||
<h4>Server-Konfiguration</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="server">Server-Basis-URL:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="server"
|
||||
v-model="config.server"
|
||||
placeholder="https://example.com"
|
||||
class="form-input"
|
||||
readonly
|
||||
/>
|
||||
<span class="hint">Aus Einstellungen geladen (nur lesend)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h4>Login-Konfiguration</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="loginEndpoint">Login-Endpoint URL:</label>
|
||||
<label for="loginEndpoint">Login-Endpoint Pfad:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="loginEndpoint"
|
||||
v-model="config.loginEndpoint"
|
||||
placeholder="https://example.com/api/login"
|
||||
placeholder="/api/auth/login"
|
||||
class="form-input"
|
||||
/>
|
||||
<span class="hint">Optional: Falls für die Übertragung ein Login erforderlich ist</span>
|
||||
<span class="hint">Optional: Relativer Pfad zum Login-Endpoint</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -71,17 +92,18 @@
|
||||
<div class="form-section">
|
||||
<h4>Übertragungs-Konfiguration</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="transferEndpoint">Übertragungs-Endpoint URL: <span class="required">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
id="transferEndpoint"
|
||||
v-model="config.transferEndpoint"
|
||||
placeholder="https://example.com/api/members"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="transferEndpoint">Übertragungs-Endpoint Pfad: <span class="required">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
id="transferEndpoint"
|
||||
v-model="config.transferEndpoint"
|
||||
placeholder="/api/members/bulk"
|
||||
class="form-input"
|
||||
required
|
||||
/>
|
||||
<span class="hint">Relativer Pfad zum Übertragungs-Endpoint</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
@@ -182,7 +204,7 @@ export default {
|
||||
...mapGetters(['currentClub']),
|
||||
|
||||
isValid() {
|
||||
return !!(this.config.transferEndpoint && this.config.transferTemplate);
|
||||
return !!(this.config.server && this.config.transferEndpoint && this.config.transferTemplate);
|
||||
},
|
||||
|
||||
additionalField1Placeholder() {
|
||||
@@ -213,6 +235,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
config: {
|
||||
server: '',
|
||||
loginEndpoint: '',
|
||||
loginFormat: 'json',
|
||||
transferEndpoint: '',
|
||||
@@ -227,10 +250,57 @@ export default {
|
||||
additionalField1: '',
|
||||
additionalField2: ''
|
||||
},
|
||||
isTransferring: false
|
||||
isTransferring: false,
|
||||
loadingConfig: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
modelValue(newVal) {
|
||||
if (newVal) {
|
||||
this.loadSavedConfig();
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadSavedConfig() {
|
||||
if (!this.currentClub) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingConfig = true;
|
||||
try {
|
||||
const response = await apiClient.get(`/member-transfer-config/${this.currentClub}`);
|
||||
|
||||
if (response.data.success && response.data.config) {
|
||||
const savedConfig = response.data.config;
|
||||
|
||||
// Konfiguration aus gespeicherten Daten laden
|
||||
this.config.server = savedConfig.server || '';
|
||||
this.config.loginEndpoint = savedConfig.loginEndpoint || '';
|
||||
this.config.loginFormat = savedConfig.loginFormat || 'json';
|
||||
this.config.transferEndpoint = savedConfig.transferEndpoint || '';
|
||||
this.config.transferMethod = savedConfig.transferMethod || 'POST';
|
||||
this.config.transferFormat = savedConfig.transferFormat || 'json';
|
||||
this.config.transferTemplate = savedConfig.transferTemplate || '';
|
||||
this.config.useBulkMode = savedConfig.useBulkMode || false;
|
||||
|
||||
// Login-Credentials (Passwort wird nicht zurückgegeben, nur wenn vorhanden)
|
||||
if (savedConfig.loginCredentials) {
|
||||
this.loginCredentials.username = savedConfig.loginCredentials.username || '';
|
||||
this.loginCredentials.password = ''; // Passwort wird nicht zurückgegeben
|
||||
// Zusätzliche Felder können nicht direkt zugewiesen werden, da sie verschlüsselt sind
|
||||
// Benutzer muss diese neu eingeben
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Keine Konfiguration vorhanden - das ist OK, Dialog bleibt leer
|
||||
if (error.response?.status !== 404) {
|
||||
console.error('Fehler beim Laden der gespeicherten Konfiguration:', error);
|
||||
}
|
||||
} finally {
|
||||
this.loadingConfig = false;
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
this.$emit('update:modelValue', false);
|
||||
this.resetForm();
|
||||
@@ -238,6 +308,7 @@ export default {
|
||||
|
||||
resetForm() {
|
||||
this.config = {
|
||||
server: '',
|
||||
loginEndpoint: '',
|
||||
loginFormat: 'json',
|
||||
transferEndpoint: '',
|
||||
@@ -295,14 +366,20 @@ export default {
|
||||
transferEndpoint: this.config.transferEndpoint,
|
||||
transferMethod: this.config.transferMethod,
|
||||
transferFormat: this.config.transferFormat,
|
||||
transferTemplate: this.config.transferTemplate
|
||||
transferTemplate: this.config.transferTemplate,
|
||||
useBulkMode: this.config.useBulkMode
|
||||
};
|
||||
|
||||
// Login-Konfiguration nur hinzufügen, wenn Endpoint vorhanden
|
||||
if (this.config.loginEndpoint) {
|
||||
transferConfig.loginEndpoint = this.config.loginEndpoint;
|
||||
transferConfig.loginFormat = this.config.loginFormat;
|
||||
transferConfig.loginCredentials = loginCredentials;
|
||||
|
||||
// Nur Login-Credentials hinzufügen, wenn welche eingegeben wurden
|
||||
// Wenn keine eingegeben wurden, werden die gespeicherten verwendet (Backend holt diese)
|
||||
if (Object.keys(loginCredentials).length > 0) {
|
||||
transferConfig.loginCredentials = loginCredentials;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await apiClient.post(
|
||||
@@ -401,6 +478,12 @@ export default {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-input[readonly] {
|
||||
background-color: #f8f9fa;
|
||||
cursor: not-allowed;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
font-family: 'Courier New', monospace;
|
||||
resize: vertical;
|
||||
@@ -489,6 +572,12 @@ export default {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.loading-config {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
|
||||
@@ -17,6 +17,7 @@ import MyTischtennisAccount from './views/MyTischtennisAccount.vue';
|
||||
import TeamManagementView from './views/TeamManagementView.vue';
|
||||
import PermissionsView from './views/PermissionsView.vue';
|
||||
import LogsView from './views/LogsView.vue';
|
||||
import MemberTransferSettingsView from './views/MemberTransferSettingsView.vue';
|
||||
import Impressum from './views/Impressum.vue';
|
||||
import Datenschutz from './views/Datenschutz.vue';
|
||||
|
||||
@@ -39,6 +40,7 @@ const routes = [
|
||||
{ path: '/team-management', component: TeamManagementView },
|
||||
{ path: '/permissions', component: PermissionsView },
|
||||
{ path: '/logs', component: LogsView },
|
||||
{ path: '/member-transfer-settings', component: MemberTransferSettingsView },
|
||||
{ path: '/impressum', component: Impressum },
|
||||
{ path: '/datenschutz', component: Datenschutz },
|
||||
];
|
||||
|
||||
1206
frontend/src/views/MemberTransferSettingsView.vue
Normal file
1206
frontend/src/views/MemberTransferSettingsView.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user