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:
Torsten Schulz (local)
2025-11-05 15:30:12 +01:00
parent 5bba9522b3
commit 1f47a11091
11 changed files with 1826 additions and 28 deletions

View 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();

View File

@@ -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,