Add member transfer functionality to memberController and update routes and UI
Implemented a new transferMembers function in memberController to handle member transfers, including validation for transfer endpoint and template. Updated memberRoutes to include a new route for member transfers. Enhanced MembersView with a button to open a transfer dialog and integrated a MemberTransferDialog component for user interaction during the transfer process.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import MemberService from "../services/memberService.js";
|
||||
import MemberTransferService from "../services/memberTransferService.js";
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
const getClubMembers = async(req, res) => {
|
||||
@@ -99,4 +100,36 @@ const rotateMemberImage = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage };
|
||||
const transferMembers = async (req, res) => {
|
||||
try {
|
||||
const { id: clubId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const config = req.body;
|
||||
|
||||
// Validierung
|
||||
if (!config.transferEndpoint) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Übertragungs-Endpoint ist erforderlich'
|
||||
});
|
||||
}
|
||||
|
||||
if (!config.transferTemplate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Übertragungs-Template ist erforderlich'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await MemberTransferService.transferMembers(userToken, clubId, config);
|
||||
res.status(result.status).json(result.response);
|
||||
} catch (error) {
|
||||
console.error('[transferMembers] - Error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler bei der Übertragung: ' + error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage, transferMembers };
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage } from '../controllers/memberController.js';
|
||||
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage, transferMembers } from '../controllers/memberController.js';
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import { authorize } from '../middleware/authorizationMiddleware.js';
|
||||
@@ -16,5 +16,6 @@ router.post('/set/:id', authenticate, authorize('members', 'write'), setClubMemb
|
||||
router.get('/notapproved/:id', authenticate, authorize('members', 'read'), getWaitingApprovals);
|
||||
router.post('/update-ratings/:id', authenticate, authorize('mytischtennis', 'write'), updateRatingsFromMyTischtennis);
|
||||
router.post('/rotate-image/:clubId/:memberId', authenticate, authorize('members', 'write'), rotateMemberImage);
|
||||
router.post('/transfer/:id', authenticate, authorize('members', 'write'), transferMembers);
|
||||
|
||||
export default router;
|
||||
|
||||
561
backend/services/memberTransferService.js
Normal file
561
backend/services/memberTransferService.js
Normal file
@@ -0,0 +1,561 @@
|
||||
import axios from 'axios';
|
||||
import Member from '../models/Member.js';
|
||||
import { checkAccess, getUserByToken } from '../utils/userUtils.js';
|
||||
import { encryptData, decryptData } from '../utils/encrypt.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
|
||||
class MemberTransferService {
|
||||
/**
|
||||
* Überträgt alle Mitglieder mit testMembership = false an einen externen Endpoint
|
||||
*
|
||||
* @param {string} userToken - User Token für Authentifizierung
|
||||
* @param {number} clubId - Club ID
|
||||
* @param {Object} config - Übertragungskonfiguration
|
||||
* @param {string} config.loginEndpoint - Login-Endpoint URL
|
||||
* @param {string} config.loginFormat - Format für Login (json, form-data, x-www-form-urlencoded)
|
||||
* @param {Object} config.loginCredentials - Login-Daten (z.B. { username: '...', password: '...' })
|
||||
* @param {string} config.transferEndpoint - Übertragungs-Endpoint URL
|
||||
* @param {string} config.transferMethod - HTTP-Methode (POST, PUT, GET)
|
||||
* @param {string} config.transferFormat - Format für Übertragung (json, xml, form-data, x-www-form-urlencoded)
|
||||
* @param {string} config.transferTemplate - Template mit Platzhaltern (z.B. '{"name": "{{firstName}} {{lastName}}", "email": "{{email}}"}')
|
||||
* @param {boolean} config.useBulkMode - Wenn true, werden alle Mitglieder in einem Bulk-Request übertragen (als {members: [...]})
|
||||
* @returns {Promise<Object>} Ergebnis der Übertragung
|
||||
*/
|
||||
async transferMembers(userToken, clubId, config) {
|
||||
try {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
// 1. Alle Mitglieder mit testMembership = false laden
|
||||
const members = await Member.findAll({
|
||||
where: {
|
||||
clubId: clubId,
|
||||
testMembership: false
|
||||
}
|
||||
});
|
||||
|
||||
if (members.length === 0) {
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
message: 'Keine Mitglieder zum Übertragen gefunden',
|
||||
transferred: 0,
|
||||
errors: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Login durchführen
|
||||
let authToken = null;
|
||||
let sessionCookie = null;
|
||||
|
||||
if (config.loginEndpoint && config.loginCredentials) {
|
||||
try {
|
||||
const loginResult = await this.performLogin(
|
||||
config.loginEndpoint,
|
||||
config.loginFormat || 'json',
|
||||
config.loginCredentials
|
||||
);
|
||||
|
||||
if (!loginResult.success) {
|
||||
return {
|
||||
status: 401,
|
||||
response: {
|
||||
success: false,
|
||||
message: 'Login fehlgeschlagen',
|
||||
error: loginResult.error,
|
||||
transferred: 0,
|
||||
errors: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
authToken = loginResult.token;
|
||||
sessionCookie = loginResult.cookie;
|
||||
} catch (loginError) {
|
||||
devLog('[transferMembers] Login error:', loginError);
|
||||
return {
|
||||
status: 401,
|
||||
response: {
|
||||
success: false,
|
||||
message: 'Login fehlgeschlagen: ' + loginError.message,
|
||||
transferred: 0,
|
||||
errors: []
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Mitglieder übertragen
|
||||
const useBulkMode = config.useBulkMode === true;
|
||||
|
||||
if (useBulkMode) {
|
||||
// Bulk-Import: Alle Mitglieder auf einmal übertragen
|
||||
try {
|
||||
const membersArray = [];
|
||||
const memberErrors = [];
|
||||
|
||||
for (const member of members) {
|
||||
try {
|
||||
const memberData = this.replacePlaceholders(
|
||||
config.transferTemplate,
|
||||
member
|
||||
);
|
||||
membersArray.push(memberData);
|
||||
} catch (error) {
|
||||
devLog(`[transferMembers] Error processing ${member.firstName} ${member.lastName}:`, error);
|
||||
memberErrors.push({
|
||||
member: `${member.firstName} ${member.lastName}`,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk-Objekt erstellen
|
||||
const bulkData = {
|
||||
members: membersArray
|
||||
};
|
||||
|
||||
const transferResult = await this.transferMember(
|
||||
config.transferEndpoint,
|
||||
config.transferMethod || 'POST',
|
||||
config.transferFormat || 'json',
|
||||
bulkData,
|
||||
authToken,
|
||||
sessionCookie
|
||||
);
|
||||
|
||||
if (transferResult.success) {
|
||||
const summary = transferResult.data?.summary || {};
|
||||
const transferred = summary.imported || membersArray.length;
|
||||
const bulkErrors = transferResult.data?.results?.errors || [];
|
||||
const bulkDuplicates = transferResult.data?.results?.duplicates || [];
|
||||
|
||||
let message = `${transferred} von ${members.length} Mitglieder(n) erfolgreich übertragen.`;
|
||||
if (bulkDuplicates.length > 0) {
|
||||
message += ` ${bulkDuplicates.length} Duplikat(e) gefunden.`;
|
||||
}
|
||||
if (bulkErrors.length > 0 || memberErrors.length > 0) {
|
||||
message += ` ${bulkErrors.length + memberErrors.length} Fehler.`;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
message: message,
|
||||
transferred: transferred,
|
||||
total: members.length,
|
||||
duplicates: bulkDuplicates.length,
|
||||
errors: [...bulkErrors, ...memberErrors],
|
||||
details: transferResult.data
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: transferResult.status || 500,
|
||||
response: {
|
||||
success: false,
|
||||
message: 'Bulk-Übertragung fehlgeschlagen',
|
||||
error: transferResult.error,
|
||||
transferred: 0,
|
||||
total: members.length,
|
||||
errors: memberErrors
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
devLog('[transferMembers] Bulk transfer error:', error);
|
||||
return {
|
||||
status: 500,
|
||||
response: {
|
||||
success: false,
|
||||
message: 'Fehler bei der Bulk-Übertragung: ' + error.message,
|
||||
transferred: 0,
|
||||
total: members.length,
|
||||
errors: []
|
||||
}
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Einzelne Übertragung: Jedes Mitglied einzeln übertragen
|
||||
const results = [];
|
||||
const errors = [];
|
||||
let successCount = 0;
|
||||
|
||||
for (const member of members) {
|
||||
try {
|
||||
const memberData = this.replacePlaceholders(
|
||||
config.transferTemplate,
|
||||
member
|
||||
);
|
||||
|
||||
const transferResult = await this.transferMember(
|
||||
config.transferEndpoint,
|
||||
config.transferMethod || 'POST',
|
||||
config.transferFormat || 'json',
|
||||
memberData,
|
||||
authToken,
|
||||
sessionCookie
|
||||
);
|
||||
|
||||
if (transferResult.success) {
|
||||
successCount++;
|
||||
results.push({
|
||||
member: `${member.firstName} ${member.lastName}`,
|
||||
success: true
|
||||
});
|
||||
} else {
|
||||
errors.push({
|
||||
member: `${member.firstName} ${member.lastName}`,
|
||||
error: transferResult.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
devLog(`[transferMembers] Error transferring ${member.firstName} ${member.lastName}:`, error);
|
||||
errors.push({
|
||||
member: `${member.firstName} ${member.lastName}`,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const message = `${successCount} von ${members.length} Mitglieder(n) erfolgreich übertragen.`;
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
message: message,
|
||||
transferred: successCount,
|
||||
total: members.length,
|
||||
results: results,
|
||||
errors: errors
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
devLog('[transferMembers] Error:', error);
|
||||
return {
|
||||
status: 500,
|
||||
response: {
|
||||
success: false,
|
||||
message: 'Fehler bei der Übertragung: ' + error.message,
|
||||
transferred: 0,
|
||||
errors: [error.message]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt Login am externen Endpoint durch
|
||||
*/
|
||||
async performLogin(endpoint, format, credentials) {
|
||||
try {
|
||||
let requestConfig = {
|
||||
url: endpoint,
|
||||
method: 'POST',
|
||||
timeout: 10000,
|
||||
maxRedirects: 5,
|
||||
validateStatus: (status) => status >= 200 && status < 400
|
||||
};
|
||||
|
||||
// Headers und Daten je nach Format setzen
|
||||
if (format === 'json' || format === 'application/json') {
|
||||
requestConfig.headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
requestConfig.data = credentials;
|
||||
} else if (format === 'form-data' || format === 'multipart/form-data') {
|
||||
const FormData = (await import('form-data')).default;
|
||||
const formData = new FormData();
|
||||
for (const [key, value] of Object.entries(credentials)) {
|
||||
formData.append(key, value);
|
||||
}
|
||||
requestConfig.headers = formData.getHeaders();
|
||||
requestConfig.data = formData;
|
||||
} else if (format === 'x-www-form-urlencoded' || format === 'urlencoded') {
|
||||
requestConfig.headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
};
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(credentials)) {
|
||||
params.append(key, value);
|
||||
}
|
||||
requestConfig.data = params.toString();
|
||||
} else {
|
||||
// Default: JSON
|
||||
requestConfig.headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
requestConfig.data = credentials;
|
||||
}
|
||||
|
||||
const response = await axios(requestConfig);
|
||||
|
||||
// Versuche Token/Cookie aus Response zu extrahieren
|
||||
// Typische Felder: token, accessToken, authToken, cookie, session
|
||||
const token = response.data?.token ||
|
||||
response.data?.accessToken ||
|
||||
response.data?.authToken ||
|
||||
response.headers['authorization'] ||
|
||||
response.headers['x-auth-token'];
|
||||
|
||||
const cookie = response.headers['set-cookie']?.[0] ||
|
||||
response.data?.cookie ||
|
||||
response.data?.session;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
token: token,
|
||||
cookie: cookie,
|
||||
response: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
devLog('[performLogin] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || error.message || 'Login fehlgeschlagen'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Überträgt ein einzelnes Mitglied
|
||||
*/
|
||||
async transferMember(endpoint, method, format, data, authToken, sessionCookie) {
|
||||
try {
|
||||
let requestConfig = {
|
||||
url: endpoint,
|
||||
method: method.toUpperCase(),
|
||||
timeout: 10000,
|
||||
maxRedirects: 5
|
||||
};
|
||||
|
||||
// Auth-Header setzen falls vorhanden
|
||||
if (authToken) {
|
||||
requestConfig.headers = {
|
||||
...requestConfig.headers,
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
'X-Auth-Token': authToken
|
||||
};
|
||||
}
|
||||
|
||||
// Cookie setzen falls vorhanden
|
||||
if (sessionCookie) {
|
||||
requestConfig.headers = {
|
||||
...requestConfig.headers,
|
||||
'Cookie': sessionCookie
|
||||
};
|
||||
}
|
||||
|
||||
// Daten je nach Format setzen
|
||||
if (format === 'json' || format === 'application/json') {
|
||||
requestConfig.headers = {
|
||||
...requestConfig.headers,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
requestConfig.data = typeof data === 'string' ? JSON.parse(data) : data;
|
||||
} else if (format === 'xml' || format === 'application/xml' || format === 'text/xml') {
|
||||
requestConfig.headers = {
|
||||
...requestConfig.headers,
|
||||
'Content-Type': 'application/xml'
|
||||
};
|
||||
requestConfig.data = data; // XML als String
|
||||
} else if (format === 'form-data' || format === 'multipart/form-data') {
|
||||
const FormData = (await import('form-data')).default;
|
||||
const formData = new FormData();
|
||||
|
||||
// Wenn data ein Objekt ist, alle Felder hinzufügen
|
||||
if (typeof data === 'object') {
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
formData.append(key, value);
|
||||
}
|
||||
} else {
|
||||
// Wenn data ein String ist, versuche es zu parsen
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
for (const [key, value] of Object.entries(parsed)) {
|
||||
formData.append(key, value);
|
||||
}
|
||||
} catch {
|
||||
// Fallback: als Text hinzufügen
|
||||
formData.append('data', data);
|
||||
}
|
||||
}
|
||||
|
||||
requestConfig.headers = {
|
||||
...formData.getHeaders(),
|
||||
...requestConfig.headers
|
||||
};
|
||||
requestConfig.data = formData;
|
||||
} else if (format === 'x-www-form-urlencoded' || format === 'urlencoded') {
|
||||
requestConfig.headers = {
|
||||
...requestConfig.headers,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
};
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (typeof data === 'object') {
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
params.append(key, value);
|
||||
}
|
||||
} else {
|
||||
// Versuche JSON zu parsen
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
for (const [key, value] of Object.entries(parsed)) {
|
||||
params.append(key, value);
|
||||
}
|
||||
} catch {
|
||||
params.append('data', data);
|
||||
}
|
||||
}
|
||||
|
||||
requestConfig.data = params.toString();
|
||||
} else {
|
||||
// Default: JSON
|
||||
requestConfig.headers = {
|
||||
...requestConfig.headers,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
requestConfig.data = typeof data === 'string' ? JSON.parse(data) : data;
|
||||
}
|
||||
|
||||
const response = await axios(requestConfig);
|
||||
|
||||
return {
|
||||
success: response.status >= 200 && response.status < 300,
|
||||
status: response.status,
|
||||
data: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
devLog('[transferMember] Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || error.message || 'Übertragung fehlgeschlagen',
|
||||
status: error.response?.status
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ersetzt Platzhalter im Template mit Mitgliedsdaten
|
||||
* Unterstützte Platzhalter: {{firstName}}, {{lastName}}, {{email}}, {{phone}},
|
||||
* {{street}}, {{city}}, {{birthDate}}, {{ttr}}, {{qttr}}, {{gender}}
|
||||
*/
|
||||
replacePlaceholders(template, member) {
|
||||
if (!template) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Wenn Template ein JSON-String ist, parsen
|
||||
let parsedTemplate;
|
||||
try {
|
||||
parsedTemplate = JSON.parse(template);
|
||||
} catch {
|
||||
// Wenn kein JSON, als einfachen String behandeln
|
||||
let result = template;
|
||||
|
||||
// Alle verfügbaren Platzhalter ersetzen
|
||||
const placeholders = {
|
||||
'{{firstName}}': member.firstName || '',
|
||||
'{{lastName}}': member.lastName || '',
|
||||
'{{email}}': member.email || '',
|
||||
'{{phone}}': member.phone || '',
|
||||
'{{street}}': member.street || '',
|
||||
'{{city}}': member.city || '',
|
||||
'{{birthDate}}': this.formatBirthDate(member.birthDate) || '',
|
||||
'{{geburtsdatum}}': this.formatBirthDate(member.birthDate) || '',
|
||||
'{{address}}': `${member.street || ''}, ${member.city || ''}`.trim().replace(/^,\s*|,\s*$/g, ''),
|
||||
'{{ttr}}': member.ttr || '',
|
||||
'{{qttr}}': member.qttr || '',
|
||||
'{{gender}}': member.gender || '',
|
||||
'{{fullName}}': `${member.firstName || ''} ${member.lastName || ''}`.trim()
|
||||
};
|
||||
|
||||
for (const [placeholder, value] of Object.entries(placeholders)) {
|
||||
result = result.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Wenn es ein Objekt ist, rekursiv Platzhalter ersetzen
|
||||
const replaceInObject = (obj) => {
|
||||
if (typeof obj === 'string') {
|
||||
let result = obj;
|
||||
const placeholders = {
|
||||
'{{firstName}}': member.firstName || '',
|
||||
'{{lastName}}': member.lastName || '',
|
||||
'{{email}}': member.email || '',
|
||||
'{{phone}}': member.phone || '',
|
||||
'{{street}}': member.street || '',
|
||||
'{{city}}': member.city || '',
|
||||
'{{birthDate}}': this.formatBirthDate(member.birthDate) || '',
|
||||
'{{geburtsdatum}}': this.formatBirthDate(member.birthDate) || '',
|
||||
'{{address}}': `${member.street || ''}, ${member.city || ''}`.trim().replace(/^,\s*|,\s*$/g, ''),
|
||||
'{{ttr}}': member.ttr || '',
|
||||
'{{qttr}}': member.qttr || '',
|
||||
'{{gender}}': member.gender || '',
|
||||
'{{fullName}}': `${member.firstName || ''} ${member.lastName || ''}`.trim()
|
||||
};
|
||||
|
||||
for (const [placeholder, value] of Object.entries(placeholders)) {
|
||||
result = result.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), String(value));
|
||||
}
|
||||
|
||||
return result;
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map(item => replaceInObject(item));
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
result[replaceInObject(key)] = replaceInObject(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
return replaceInObject(parsedTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert Geburtsdatum in YYYY-MM-DD Format
|
||||
*/
|
||||
formatBirthDate(birthDate) {
|
||||
if (!birthDate) return '';
|
||||
|
||||
// Wenn bereits im richtigen Format
|
||||
if (typeof birthDate === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(birthDate)) {
|
||||
return birthDate;
|
||||
}
|
||||
|
||||
// Versuche zu parsen
|
||||
try {
|
||||
const date = new Date(birthDate);
|
||||
if (!isNaN(date.getTime())) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback: Versuche Format dd.mm.yyyy zu parsen
|
||||
const match = String(birthDate).match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
||||
if (match) {
|
||||
const day = match[1].padStart(2, '0');
|
||||
const month = match[2].padStart(2, '0');
|
||||
const year = match[3];
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
}
|
||||
|
||||
return birthDate || '';
|
||||
}
|
||||
}
|
||||
|
||||
export default new MemberTransferService();
|
||||
|
||||
528
frontend/src/components/MemberTransferDialog.vue
Normal file
528
frontend/src/components/MemberTransferDialog.vue
Normal file
@@ -0,0 +1,528 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
title="Mitglieder übertragen"
|
||||
size="large"
|
||||
:close-on-overlay="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="transfer-form">
|
||||
<div class="form-section">
|
||||
<h4>Login-Konfiguration</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="loginEndpoint">Login-Endpoint URL:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="loginEndpoint"
|
||||
v-model="config.loginEndpoint"
|
||||
placeholder="https://example.com/api/login"
|
||||
class="form-input"
|
||||
/>
|
||||
<span class="hint">Optional: Falls für die Übertragung ein Login erforderlich ist</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="loginFormat">Login-Format:</label>
|
||||
<select id="loginFormat" v-model="config.loginFormat" class="form-select">
|
||||
<option value="json">JSON</option>
|
||||
<option value="form-data">Form Data</option>
|
||||
<option value="x-www-form-urlencoded">URL Encoded</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Login-Daten:</label>
|
||||
<div class="credentials-group">
|
||||
<div class="credential-row">
|
||||
<input
|
||||
type="text"
|
||||
v-model="loginCredentials.username"
|
||||
placeholder="Benutzername / Email"
|
||||
class="form-input"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
v-model="loginCredentials.password"
|
||||
placeholder="Passwort"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="credential-row">
|
||||
<input
|
||||
type="text"
|
||||
v-model="loginCredentials.additionalField1"
|
||||
:placeholder="additionalField1Placeholder"
|
||||
class="form-input"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
v-model="loginCredentials.additionalField2"
|
||||
:placeholder="additionalField2Placeholder"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="hint">Zusätzliche Felder werden nur verwendet, wenn sie ausgefüllt sind</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-row">
|
||||
<div class="form-group">
|
||||
<label for="transferMethod">HTTP-Methode:</label>
|
||||
<select id="transferMethod" v-model="config.transferMethod" class="form-select">
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="GET">GET</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="transferFormat">Übertragungs-Format:</label>
|
||||
<select id="transferFormat" v-model="config.transferFormat" class="form-select">
|
||||
<option value="json">JSON</option>
|
||||
<option value="xml">XML</option>
|
||||
<option value="form-data">Form Data</option>
|
||||
<option value="x-www-form-urlencoded">URL Encoded</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" v-model="config.useBulkMode" />
|
||||
<span>Bulk-Import-Modus (alle Mitglieder auf einmal übertragen)</span>
|
||||
</label>
|
||||
<span class="hint">Wenn aktiviert, werden alle Mitglieder in einem Request als Array übertragen. Das Template definiert das Format für ein einzelnes Mitglied.</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="transferTemplate">Übertragungs-Template: <span class="required">*</span></label>
|
||||
<textarea
|
||||
id="transferTemplate"
|
||||
v-model="config.transferTemplate"
|
||||
rows="8"
|
||||
class="form-textarea"
|
||||
placeholder='{"name": "{{firstName}} {{lastName}}", "email": "{{email}}", "phone": "{{phone}}"}'
|
||||
required
|
||||
></textarea>
|
||||
<div class="template-help">
|
||||
<strong>Verfügbare Platzhalter:</strong>
|
||||
<ul>
|
||||
<li><code>{{firstName}}</code> - Vorname</li>
|
||||
<li><code>{{lastName}}</code> - Nachname</li>
|
||||
<li><code>{{fullName}}</code> - Vollständiger Name</li>
|
||||
<li><code>{{email}}</code> - E-Mail-Adresse</li>
|
||||
<li><code>{{phone}}</code> - Telefonnummer</li>
|
||||
<li><code>{{street}}</code> - Straße</li>
|
||||
<li><code>{{city}}</code> - Ort</li>
|
||||
<li><code>{{birthDate}}</code> - Geburtsdatum (YYYY-MM-DD)</li>
|
||||
<li><code>{{geburtsdatum}}</code> - Geburtsdatum (YYYY-MM-DD, alternativ)</li>
|
||||
<li><code>{{address}}</code> - Kombinierte Adresse (Straße, Ort)</li>
|
||||
<li><code>{{ttr}}</code> - TTR-Wert</li>
|
||||
<li><code>{{qttr}}</code> - QTTR-Wert</li>
|
||||
<li><code>{{gender}}</code> - Geschlecht</li>
|
||||
</ul>
|
||||
<p><strong>Beispiel JSON (für einzelnes Mitglied):</strong></p>
|
||||
<pre>{{ jsonExample }}</pre>
|
||||
<p v-if="config.useBulkMode" class="bulk-hint">
|
||||
<strong>Bulk-Modus aktiv:</strong> Das Template wird für jedes Mitglied angewendet und automatisch in <code>{"members": [...]}</code> gewrappt.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<button class="btn-secondary" @click="handleClose">Abbrechen</button>
|
||||
<button
|
||||
class="btn-primary"
|
||||
@click="handleTransfer"
|
||||
:disabled="!isValid || isTransferring"
|
||||
>
|
||||
{{ isTransferring ? 'Übertrage...' : 'Übertragen' }}
|
||||
</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseDialog from './BaseDialog.vue';
|
||||
import apiClient from '../apiClient.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'MemberTransferDialog',
|
||||
components: {
|
||||
BaseDialog
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentClub']),
|
||||
|
||||
isValid() {
|
||||
return !!(this.config.transferEndpoint && this.config.transferTemplate);
|
||||
},
|
||||
|
||||
additionalField1Placeholder() {
|
||||
if (this.config.loginFormat === 'form-data' || this.config.loginFormat === 'x-www-form-urlencoded') {
|
||||
return 'Feldname: Wert (z.B. client_id: abc123)';
|
||||
}
|
||||
return 'Zusätzliches Feld (z.B. client_id)';
|
||||
},
|
||||
|
||||
additionalField2Placeholder() {
|
||||
if (this.config.loginFormat === 'form-data' || this.config.loginFormat === 'x-www-form-urlencoded') {
|
||||
return 'Feldname: Wert (z.B. client_secret: xyz789)';
|
||||
}
|
||||
return 'Zusätzliches Feld (z.B. client_secret)';
|
||||
},
|
||||
|
||||
jsonExample() {
|
||||
return JSON.stringify({
|
||||
firstName: "{{firstName}}",
|
||||
lastName: "{{lastName}}",
|
||||
geburtsdatum: "{{geburtsdatum}}",
|
||||
email: "{{email}}",
|
||||
phone: "{{phone}}",
|
||||
address: "{{address}}"
|
||||
}, null, 2);
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
config: {
|
||||
loginEndpoint: '',
|
||||
loginFormat: 'json',
|
||||
transferEndpoint: '',
|
||||
transferMethod: 'POST',
|
||||
transferFormat: 'json',
|
||||
transferTemplate: '',
|
||||
useBulkMode: false
|
||||
},
|
||||
loginCredentials: {
|
||||
username: '',
|
||||
password: '',
|
||||
additionalField1: '',
|
||||
additionalField2: ''
|
||||
},
|
||||
isTransferring: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:modelValue', false);
|
||||
this.resetForm();
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.config = {
|
||||
loginEndpoint: '',
|
||||
loginFormat: 'json',
|
||||
transferEndpoint: '',
|
||||
transferMethod: 'POST',
|
||||
transferFormat: 'json',
|
||||
transferTemplate: '',
|
||||
useBulkMode: false
|
||||
};
|
||||
this.loginCredentials = {
|
||||
username: '',
|
||||
password: '',
|
||||
additionalField1: '',
|
||||
additionalField2: ''
|
||||
};
|
||||
this.isTransferring = false;
|
||||
},
|
||||
|
||||
async handleTransfer() {
|
||||
if (!this.isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isTransferring = true;
|
||||
|
||||
try {
|
||||
// Login-Credentials zusammenstellen
|
||||
const loginCredentials = {};
|
||||
if (this.loginCredentials.username) {
|
||||
loginCredentials.username = this.loginCredentials.username;
|
||||
}
|
||||
if (this.loginCredentials.password) {
|
||||
loginCredentials.password = this.loginCredentials.password;
|
||||
}
|
||||
|
||||
// Zusätzliche Felder verarbeiten
|
||||
if (this.loginCredentials.additionalField1) {
|
||||
const parts1 = this.loginCredentials.additionalField1.split(':').map(s => s.trim());
|
||||
if (parts1.length === 2) {
|
||||
loginCredentials[parts1[0]] = parts1[1];
|
||||
} else {
|
||||
loginCredentials[parts1[0]] = parts1[0];
|
||||
}
|
||||
}
|
||||
if (this.loginCredentials.additionalField2) {
|
||||
const parts2 = this.loginCredentials.additionalField2.split(':').map(s => s.trim());
|
||||
if (parts2.length === 2) {
|
||||
loginCredentials[parts2[0]] = parts2[1];
|
||||
} else {
|
||||
loginCredentials[parts2[0]] = parts2[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Konfiguration zusammenstellen
|
||||
const transferConfig = {
|
||||
transferEndpoint: this.config.transferEndpoint,
|
||||
transferMethod: this.config.transferMethod,
|
||||
transferFormat: this.config.transferFormat,
|
||||
transferTemplate: this.config.transferTemplate
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
const response = await apiClient.post(
|
||||
`/clubmembers/transfer/${this.currentClub}`,
|
||||
transferConfig
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
const message = response.data.message ||
|
||||
`${response.data.transferred} von ${response.data.total} Mitglieder(n) erfolgreich übertragen.`;
|
||||
|
||||
let details = '';
|
||||
if (response.data.errors && response.data.errors.length > 0) {
|
||||
details = 'Fehler:\n' + response.data.errors.map(e =>
|
||||
`${e.member}: ${e.error}`
|
||||
).join('\n');
|
||||
}
|
||||
|
||||
this.$emit('success', {
|
||||
message: message,
|
||||
details: details,
|
||||
results: response.data
|
||||
});
|
||||
|
||||
this.handleClose();
|
||||
} else {
|
||||
this.$emit('error', {
|
||||
message: response.data.message || 'Übertragung fehlgeschlagen',
|
||||
error: response.data.error
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Transfer error:', error);
|
||||
const errorMessage = error.response?.data?.error ||
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
'Fehler bei der Übertragung';
|
||||
|
||||
this.$emit('error', {
|
||||
message: 'Fehler bei der Übertragung',
|
||||
error: errorMessage
|
||||
});
|
||||
} finally {
|
||||
this.isTransferring = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.transfer-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 1.5rem;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.form-section h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group .required {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
font-family: 'Courier New', monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.credentials-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.credential-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.template-help {
|
||||
margin-top: 0.75rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e9ecef;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.template-help strong {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.template-help ul {
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.template-help li {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.template-help code {
|
||||
background-color: #e9ecef;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.template-help pre {
|
||||
background-color: #282c34;
|
||||
color: #abb2bf;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.bulk-hint {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background-color: #e7f3ff;
|
||||
border-left: 4px solid #007bff;
|
||||
border-radius: 4px;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.bulk-hint code {
|
||||
background-color: #cce5ff;
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
<button @click="updateRatingsFromMyTischtennis" class="btn-update-ratings" :disabled="isUpdatingRatings">
|
||||
{{ isUpdatingRatings ? 'Aktualisiere...' : 'TTR/QTTR von myTischtennis aktualisieren' }}
|
||||
</button>
|
||||
<button @click="openTransferDialog" class="btn-transfer">
|
||||
Mitglieder übertragen
|
||||
</button>
|
||||
</div>
|
||||
<div class="newmember">
|
||||
<div class="toggle-new-member">
|
||||
@@ -156,35 +159,7 @@
|
||||
<button @click.stop="openActivitiesModal(member)" class="btn-activities">Übungen</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
<!-- Info Dialog -->
|
||||
<InfoDialog
|
||||
v-model="infoDialog.isOpen"
|
||||
:title="infoDialog.title"
|
||||
:message="infoDialog.message"
|
||||
:details="infoDialog.details"
|
||||
:type="infoDialog.type"
|
||||
/>
|
||||
|
||||
<!-- Confirm Dialog -->
|
||||
<ConfirmDialog
|
||||
v-model="confirmDialog.isOpen"
|
||||
:title="confirmDialog.title"
|
||||
:message="confirmDialog.message"
|
||||
:details="confirmDialog.details"
|
||||
:type="confirmDialog.type"
|
||||
@confirm="handleConfirmResult(true)"
|
||||
@cancel="handleConfirmResult(false)"
|
||||
/>
|
||||
|
||||
<!-- Member Activities Dialog -->
|
||||
<MemberActivitiesDialog
|
||||
v-model="showActivitiesModal"
|
||||
:member="selectedMemberForActivities"
|
||||
:club-id="currentClub"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -212,28 +187,41 @@
|
||||
@delete-note="deleteNote"
|
||||
@close="closeNotesModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Info Dialog -->
|
||||
<InfoDialog
|
||||
v-model="infoDialog.isOpen"
|
||||
:title="infoDialog.title"
|
||||
:message="infoDialog.message"
|
||||
:details="infoDialog.details"
|
||||
:type="infoDialog.type"
|
||||
/>
|
||||
|
||||
<!-- Confirm Dialog -->
|
||||
<ConfirmDialog
|
||||
v-model="confirmDialog.isOpen"
|
||||
:title="confirmDialog.title"
|
||||
:message="confirmDialog.message"
|
||||
:details="confirmDialog.details"
|
||||
:type="confirmDialog.type"
|
||||
@confirm="handleConfirmResult(true)"
|
||||
@cancel="handleConfirmResult(false)"
|
||||
/>
|
||||
<!-- Info Dialog -->
|
||||
<InfoDialog
|
||||
v-model="infoDialog.isOpen"
|
||||
:title="infoDialog.title"
|
||||
:message="infoDialog.message"
|
||||
:details="infoDialog.details"
|
||||
:type="infoDialog.type"
|
||||
/>
|
||||
|
||||
<!-- Confirm Dialog -->
|
||||
<ConfirmDialog
|
||||
v-model="confirmDialog.isOpen"
|
||||
:title="confirmDialog.title"
|
||||
:message="confirmDialog.message"
|
||||
:details="confirmDialog.details"
|
||||
:type="confirmDialog.type"
|
||||
@confirm="handleConfirmResult(true)"
|
||||
@cancel="handleConfirmResult(false)"
|
||||
/>
|
||||
|
||||
<!-- Member Activities Dialog -->
|
||||
<MemberActivitiesDialog
|
||||
v-model="showActivitiesModal"
|
||||
:member="selectedMemberForActivities"
|
||||
:club-id="currentClub"
|
||||
/>
|
||||
|
||||
<!-- Member Transfer Dialog -->
|
||||
<MemberTransferDialog
|
||||
v-model="showTransferDialog"
|
||||
@success="handleTransferSuccess"
|
||||
@error="handleTransferError"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -247,6 +235,7 @@ import ImageViewerDialog from '../components/ImageViewerDialog.vue';
|
||||
import BaseDialog from '../components/BaseDialog.vue';
|
||||
import MemberNotesDialog from '../components/MemberNotesDialog.vue';
|
||||
import MemberActivitiesDialog from '../components/MemberActivitiesDialog.vue';
|
||||
import MemberTransferDialog from '../components/MemberTransferDialog.vue';
|
||||
export default {
|
||||
name: 'MembersView',
|
||||
components: {
|
||||
@@ -255,7 +244,8 @@ export default {
|
||||
ImageViewerDialog,
|
||||
BaseDialog,
|
||||
MemberNotesDialog,
|
||||
MemberActivitiesDialog
|
||||
MemberActivitiesDialog,
|
||||
MemberTransferDialog
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub']),
|
||||
@@ -345,7 +335,8 @@ export default {
|
||||
showActivitiesModal: false,
|
||||
selectedMemberForActivities: null,
|
||||
selectedAgeGroup: '',
|
||||
selectedGender: ''
|
||||
selectedGender: '',
|
||||
showTransferDialog: false
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -737,6 +728,30 @@ export default {
|
||||
this.selectedAgeGroup = '';
|
||||
this.selectedGender = '';
|
||||
this.showInactiveMembers = false;
|
||||
},
|
||||
|
||||
openTransferDialog() {
|
||||
this.showTransferDialog = true;
|
||||
},
|
||||
|
||||
handleTransferSuccess(event) {
|
||||
const { message, details, results } = event;
|
||||
this.showInfo(
|
||||
'Übertragung erfolgreich',
|
||||
message,
|
||||
details,
|
||||
'success'
|
||||
);
|
||||
},
|
||||
|
||||
handleTransferError(event) {
|
||||
const { message, error } = event;
|
||||
this.showInfo(
|
||||
'Fehler bei der Übertragung',
|
||||
message,
|
||||
error,
|
||||
'error'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1142,4 +1157,19 @@ table td {
|
||||
.btn-clear-filters:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
.btn-transfer {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-transfer:hover {
|
||||
background-color: #138496;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user