Update training group management and enhance UI components
This commit introduces the `TrainingGroup` model and related functionality, allowing for the management of training groups within the application. The `ClubService` is updated to automatically create preset groups upon club creation. The frontend is enhanced with new views and components, including `TrainingGroupsView` and `TrainingGroupsTab`, to facilitate the display and management of training groups. Additionally, the `MembersView` is updated to allow adding and removing members from training groups, improving the overall user experience and interactivity in managing club members and their associated training groups.
This commit is contained in:
@@ -79,14 +79,14 @@
|
||||
<span class="nav-icon">📊</span>
|
||||
Trainings-Statistik
|
||||
</router-link>
|
||||
<router-link v-if="isAdmin" to="/club-settings" class="nav-link" title="Vereinseinstellungen">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
Vereinseinstellungen
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4 class="nav-title">Organisation</h4>
|
||||
<router-link v-if="isAdmin" to="/club-settings" class="nav-link" title="Vereinseinstellungen">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
Vereinseinstellungen
|
||||
</router-link>
|
||||
<router-link v-if="hasPermission('schedule', 'read')" to="/schedule" class="nav-link" title="Spielpläne">
|
||||
<span class="nav-icon">📅</span>
|
||||
Spielpläne
|
||||
|
||||
565
frontend/src/components/TrainingGroupsTab.vue
Normal file
565
frontend/src/components/TrainingGroupsTab.vue
Normal file
@@ -0,0 +1,565 @@
|
||||
<template>
|
||||
<div class="training-groups-tab">
|
||||
<div class="groups-section">
|
||||
<div class="section-header">
|
||||
<h3>Gruppen</h3>
|
||||
<button @click="showAddGroupForm = true" class="btn-primary">+ Neue Gruppe</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Group Form -->
|
||||
<div v-if="showAddGroupForm" class="add-group-form">
|
||||
<input
|
||||
v-model="newGroupName"
|
||||
type="text"
|
||||
placeholder="Gruppenname"
|
||||
@keyup.enter="createGroup"
|
||||
class="input-field"
|
||||
/>
|
||||
<button @click="createGroup" class="btn-primary">Erstellen</button>
|
||||
<button @click="cancelAddGroup" class="btn-secondary">Abbrechen</button>
|
||||
</div>
|
||||
|
||||
<!-- Groups List -->
|
||||
<div class="groups-list">
|
||||
<div
|
||||
v-for="group in sortedGroups"
|
||||
:key="group.id"
|
||||
class="group-card"
|
||||
:class="{ 'preset-group': group.isPreset }"
|
||||
>
|
||||
<div class="group-header">
|
||||
<h4>{{ group.name }}</h4>
|
||||
<div class="group-actions">
|
||||
<button
|
||||
v-if="!group.isPreset"
|
||||
@click="editGroup(group)"
|
||||
class="btn-icon"
|
||||
title="Bearbeiten"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
v-if="!group.isPreset"
|
||||
@click="deleteGroup(group)"
|
||||
class="btn-icon btn-danger"
|
||||
title="Löschen"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group-members">
|
||||
<div class="members-count">
|
||||
{{ group.members ? group.members.length : 0 }} Mitglieder
|
||||
</div>
|
||||
<div class="members-list">
|
||||
<span
|
||||
v-for="member in (group.members || [])"
|
||||
:key="member.id"
|
||||
class="member-tag"
|
||||
>
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
<button
|
||||
@click="removeMemberFromGroup(group.id, member.id)"
|
||||
class="remove-member-btn"
|
||||
title="Entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Member to Group -->
|
||||
<div class="add-member-section">
|
||||
<select
|
||||
v-model="selectedMembers[group.id]"
|
||||
class="member-select"
|
||||
@change="addMemberToGroup(group.id, $event.target.value)"
|
||||
:disabled="availableMembersForGroup(group.id).length === 0"
|
||||
>
|
||||
<option value="">
|
||||
{{ availableMembersForGroup(group.id).length === 0 ? 'Keine Mitglieder verfügbar' : 'Mitglied hinzufügen...' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="member in availableMembersForGroup(group.id)"
|
||||
:key="member.id"
|
||||
:value="member.id"
|
||||
>
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Group Dialog -->
|
||||
<div v-if="editingGroup" class="edit-group-dialog">
|
||||
<div class="dialog-content">
|
||||
<h3>Gruppe bearbeiten</h3>
|
||||
<input
|
||||
v-model="editingGroup.name"
|
||||
type="text"
|
||||
placeholder="Gruppenname"
|
||||
class="input-field"
|
||||
/>
|
||||
<div class="dialog-actions">
|
||||
<button @click="saveGroupEdit" class="btn-primary">Speichern</button>
|
||||
<button @click="cancelGroupEdit" class="btn-secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info/Confirm Dialogs -->
|
||||
<InfoDialog
|
||||
v-model="infoDialog.isOpen"
|
||||
:title="infoDialog.title"
|
||||
:message="infoDialog.message"
|
||||
:details="infoDialog.details"
|
||||
:type="infoDialog.type"
|
||||
/>
|
||||
<ConfirmDialog
|
||||
v-model="confirmDialog.isOpen"
|
||||
:title="confirmDialog.title"
|
||||
:message="confirmDialog.message"
|
||||
:details="confirmDialog.details"
|
||||
:type="confirmDialog.type"
|
||||
@confirm="confirmDialog.resolveCallback && confirmDialog.resolveCallback(true)"
|
||||
@cancel="confirmDialog.resolveCallback && confirmDialog.resolveCallback(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '../apiClient.js';
|
||||
import { getSafeErrorMessage } from '../utils/errorMessages.js';
|
||||
import InfoDialog from './InfoDialog.vue';
|
||||
import ConfirmDialog from './ConfirmDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'TrainingGroupsTab',
|
||||
components: {
|
||||
InfoDialog,
|
||||
ConfirmDialog,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'token']),
|
||||
|
||||
sortedGroups() {
|
||||
if (!Array.isArray(this.groups)) {
|
||||
return [];
|
||||
}
|
||||
return [...this.groups].sort((a, b) => {
|
||||
// Preset-Gruppen zuerst
|
||||
if (a.isPreset && !b.isPreset) return -1;
|
||||
if (!a.isPreset && b.isPreset) return 1;
|
||||
// Dann nach sortOrder
|
||||
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
|
||||
// Dann alphabetisch
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
groups: [],
|
||||
members: [],
|
||||
showAddGroupForm: false,
|
||||
newGroupName: '',
|
||||
editingGroup: null,
|
||||
selectedMembers: {},
|
||||
infoDialog: {
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
type: 'info',
|
||||
},
|
||||
confirmDialog: {
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
type: 'info',
|
||||
resolveCallback: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
if (this.isAuthenticated && this.currentClub) {
|
||||
await this.loadGroups();
|
||||
await this.loadMembers();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentClub() {
|
||||
if (this.isAuthenticated && this.currentClub) {
|
||||
this.loadGroups();
|
||||
this.loadMembers();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async loadGroups() {
|
||||
try {
|
||||
const response = await apiClient.get(`/training-groups/${this.currentClub}`);
|
||||
// Stelle sicher, dass es ein Array ist und dass jedes Gruppen-Objekt ein members-Array hat
|
||||
const groups = Array.isArray(response.data) ? response.data : [];
|
||||
this.groups = groups.map(group => ({
|
||||
...group,
|
||||
members: Array.isArray(group.members) ? group.members : []
|
||||
}));
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingsgruppen');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
this.groups = [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadMembers() {
|
||||
try {
|
||||
const response = await apiClient.get(`/clubmembers/get/${this.currentClub}/false`);
|
||||
const members = Array.isArray(response.data) ? response.data : [];
|
||||
// Die API filtert bereits nach active, also müssen wir nicht nochmal filtern
|
||||
const filteredMembers = members.filter(m => m != null);
|
||||
// Sortiere alphabetisch nach Nachname, dann Vorname
|
||||
this.members = filteredMembers.sort((a, b) => {
|
||||
const lastNameA = (a.lastName || '').toLowerCase();
|
||||
const lastNameB = (b.lastName || '').toLowerCase();
|
||||
if (lastNameA !== lastNameB) {
|
||||
return lastNameA.localeCompare(lastNameB);
|
||||
}
|
||||
const firstNameA = (a.firstName || '').toLowerCase();
|
||||
const firstNameB = (b.firstName || '').toLowerCase();
|
||||
return firstNameA.localeCompare(firstNameB);
|
||||
});
|
||||
console.log('[loadMembers] Loaded', this.members.length, 'active members');
|
||||
} catch (error) {
|
||||
console.error('[loadMembers] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Mitglieder');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
this.members = [];
|
||||
}
|
||||
},
|
||||
|
||||
availableMembersForGroup(groupId) {
|
||||
if (!Array.isArray(this.members) || this.members.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const group = this.groups.find(g => g.id === groupId);
|
||||
if (!group) {
|
||||
return this.members;
|
||||
}
|
||||
|
||||
const groupMembers = Array.isArray(group.members) ? group.members : [];
|
||||
const memberIdsInGroup = new Set(groupMembers.map(m => m && m.id).filter(id => id != null));
|
||||
return this.members.filter(m => m && m.id && !memberIdsInGroup.has(m.id));
|
||||
},
|
||||
|
||||
async createGroup() {
|
||||
if (!this.newGroupName.trim()) {
|
||||
this.showInfo('Hinweis', 'Bitte geben Sie einen Gruppennamen ein.', '', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(`/training-groups/${this.currentClub}`, {
|
||||
name: this.newGroupName.trim(),
|
||||
});
|
||||
this.newGroupName = '';
|
||||
this.showAddGroupForm = false;
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
cancelAddGroup() {
|
||||
this.newGroupName = '';
|
||||
this.showAddGroupForm = false;
|
||||
},
|
||||
|
||||
editGroup(group) {
|
||||
this.editingGroup = { ...group };
|
||||
},
|
||||
|
||||
async saveGroupEdit() {
|
||||
if (!this.editingGroup.name.trim()) {
|
||||
this.showInfo('Hinweis', 'Bitte geben Sie einen Gruppennamen ein.', '', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.put(`/training-groups/${this.currentClub}/${this.editingGroup.id}`, {
|
||||
name: this.editingGroup.name.trim(),
|
||||
sortOrder: this.editingGroup.sortOrder,
|
||||
});
|
||||
this.editingGroup = null;
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
cancelGroupEdit() {
|
||||
this.editingGroup = null;
|
||||
},
|
||||
|
||||
async deleteGroup(group) {
|
||||
const confirmed = await this.showConfirm(
|
||||
'Gruppe löschen',
|
||||
`Möchten Sie die Gruppe "${group.name}" wirklich löschen?`,
|
||||
'Alle Mitglieder-Zuordnungen werden entfernt.',
|
||||
'warning'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await apiClient.delete(`/training-groups/${this.currentClub}/${group.id}`);
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async addMemberToGroup(groupId, memberId) {
|
||||
if (!memberId || !groupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(`/training-groups/${this.currentClub}/${groupId}/member/${memberId}`);
|
||||
this.selectedMembers[groupId] = '';
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
console.error('[addMemberToGroup] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Hinzufügen des Mitglieds');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async removeMemberFromGroup(groupId, memberId) {
|
||||
try {
|
||||
await apiClient.delete(`/training-groups/${this.currentClub}/${groupId}/member/${memberId}`);
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Entfernen des Mitglieds');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
showInfo(title, message, details, type) {
|
||||
this.infoDialog = { isOpen: true, title, message, details, type };
|
||||
},
|
||||
|
||||
showConfirm(title, message, details, type) {
|
||||
return new Promise((resolve) => {
|
||||
this.confirmDialog = {
|
||||
isOpen: true,
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
type,
|
||||
resolveCallback: resolve,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.training-groups-tab {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.add-group-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.groups-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.group-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.group-card.preset-group {
|
||||
border-color: #4CAF50;
|
||||
background: #f1f8f4;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.group-header h4 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.group-members {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.members-count {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.members-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.member-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #e3f2fd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.remove-member-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
color: #d32f2f;
|
||||
padding: 0;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.remove-member-btn:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.add-member-section {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.member-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.edit-group-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,7 @@ 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 TrainingGroupsView from './views/TrainingGroupsView.vue';
|
||||
import Impressum from './views/Impressum.vue';
|
||||
import Datenschutz from './views/Datenschutz.vue';
|
||||
|
||||
@@ -45,6 +46,7 @@ const routes = [
|
||||
{ path: '/permissions', component: PermissionsView },
|
||||
{ path: '/logs', component: LogsView },
|
||||
{ path: '/member-transfer-settings', component: MemberTransferSettingsView },
|
||||
{ path: '/training-groups', component: TrainingGroupsView },
|
||||
{ path: '/impressum', component: Impressum },
|
||||
{ path: '/datenschutz', component: Datenschutz },
|
||||
];
|
||||
|
||||
@@ -2,6 +2,24 @@
|
||||
<div class="club-settings">
|
||||
<h1>Vereins-Einstellungen</h1>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<div class="tab-navigation">
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'settings' }]"
|
||||
@click="activeTab = 'settings'"
|
||||
>
|
||||
⚙️ Einstellungen
|
||||
</button>
|
||||
<button
|
||||
:class="['tab-button', { active: activeTab === 'training-groups' }]"
|
||||
@click="activeTab = 'training-groups'"
|
||||
>
|
||||
👨👩👧👦 Trainingsgruppen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<div v-if="activeTab === 'settings'">
|
||||
<section class="card">
|
||||
<h2>Begrüßungstext</h2>
|
||||
<div class="greeting-grid">
|
||||
@@ -30,15 +48,29 @@
|
||||
<span v-if="saved" class="saved-hint">Gespeichert</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- End Settings Tab -->
|
||||
|
||||
<!-- Training Groups Tab -->
|
||||
<div v-if="activeTab === 'training-groups'">
|
||||
<TrainingGroupsTab />
|
||||
</div>
|
||||
<!-- End Training Groups Tab -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '../apiClient';
|
||||
import TrainingGroupsTab from '../components/TrainingGroupsTab.vue';
|
||||
|
||||
export default {
|
||||
name: 'ClubSettings',
|
||||
components: {
|
||||
TrainingGroupsTab,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 'settings',
|
||||
greeting: '',
|
||||
associationMemberNumber: '',
|
||||
saved: false,
|
||||
@@ -102,6 +134,36 @@ export default {
|
||||
.btn.btn-primary:hover { background: var(--primary-hover); }
|
||||
.saved-hint { color: #28a745; font-weight: 600; }
|
||||
.hint { color: #666; font-size: 12px; margin-top: 8px; }
|
||||
|
||||
.tab-navigation {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: #333;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: #28a745;
|
||||
border-bottom-color: #28a745;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -93,6 +93,46 @@
|
||||
<label class="checkbox-item"><span>Pics in Internet erlaubt:</span> <input type="checkbox" v-model="newPicsInInternetAllowed"></label>
|
||||
<label class="checkbox-item"><span>Testmitgliedschaft:</span> <input type="checkbox" v-model="testMembership"></label>
|
||||
<label class="checkbox-item"><span>Mitgliedsformular ausgehändigt:</span> <input type="checkbox" v-model="newMemberFormHandedOver"></label>
|
||||
|
||||
<!-- Trainingsgruppen -->
|
||||
<div class="contact-section" v-if="memberToEdit">
|
||||
<label><span>Trainingsgruppen:</span></label>
|
||||
<div v-if="memberTrainingGroups.length > 0" class="member-groups-list">
|
||||
<span
|
||||
v-for="group in memberTrainingGroups"
|
||||
:key="group.id"
|
||||
class="member-group-tag"
|
||||
>
|
||||
{{ group.name }}
|
||||
<button
|
||||
@click="removeMemberFromGroup(group.id)"
|
||||
class="remove-group-btn"
|
||||
title="Entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="no-groups-hint">Keine Gruppen zugeordnet</div>
|
||||
<select
|
||||
v-model="selectedGroupToAdd"
|
||||
class="group-select"
|
||||
@change="addMemberToGroup($event.target.value)"
|
||||
:disabled="availableGroupsForMember.length === 0"
|
||||
>
|
||||
<option value="">
|
||||
{{ availableGroupsForMember.length === 0 ? 'Keine Gruppen verfügbar' : 'Gruppe hinzufügen...' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="group in availableGroupsForMember"
|
||||
:key="group.id"
|
||||
:value="group.id"
|
||||
>
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label><span>Bild:</span>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<input type="file" accept="image/*" @change="onFileSelected" ref="fileInput" style="display: none;" id="member-image-file">
|
||||
@@ -157,7 +197,6 @@
|
||||
<th>Name, Vorname</th>
|
||||
<th>TTR / QTTR</th>
|
||||
<th>Adresse</th>
|
||||
<th>Mitgliedsformular</th>
|
||||
<th>Geburtsdatum</th>
|
||||
<th>Telefon-Nr.</th>
|
||||
<th>Email-Adresse</th>
|
||||
@@ -199,7 +238,6 @@
|
||||
<span v-else class="no-rating">-</span>
|
||||
</td>
|
||||
<td>{{ member.street }}{{ member.postalCode ? ', ' + member.postalCode : '' }}, {{ member.city }}</td>
|
||||
<td>{{ member.memberFormHandedOver ? '✓' : '' }}</td>
|
||||
<td>{{ getFormattedBirthdate(member.birthDate) }}</td>
|
||||
<td>{{ getFormattedPhoneNumbers(member) }}</td>
|
||||
<td>{{ getFormattedEmails(member) }}</td>
|
||||
@@ -370,6 +408,14 @@ export default {
|
||||
|
||||
hasTestMembers() {
|
||||
return this.members.some(member => member.testMembership);
|
||||
},
|
||||
|
||||
availableGroupsForMember() {
|
||||
if (!Array.isArray(this.trainingGroups) || this.trainingGroups.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const memberGroupIds = new Set(this.memberTrainingGroups.map(g => g && g.id).filter(id => id != null));
|
||||
return this.trainingGroups.filter(g => g && g.id && !memberGroupIds.has(g.id));
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -425,12 +471,16 @@ export default {
|
||||
showMemberInfo: false,
|
||||
showActivitiesModal: false,
|
||||
selectedMemberForActivities: null,
|
||||
memberTrainingGroups: [],
|
||||
trainingGroups: [],
|
||||
selectedGroupToAdd: '',
|
||||
showTransferDialog: false,
|
||||
selectedAgeGroup: '',
|
||||
selectedGender: '',
|
||||
showTransferDialog: false
|
||||
selectedGender: ''
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadTrainingGroups();
|
||||
await this.init();
|
||||
},
|
||||
methods: {
|
||||
@@ -919,6 +969,9 @@ export default {
|
||||
this.memberContacts.phones = [{ value: member.phone || '', isParent: false, parentName: '', isPrimary: true }];
|
||||
this.memberContacts.emails = [{ value: member.email || '', isParent: false, parentName: '', isPrimary: true }];
|
||||
}
|
||||
|
||||
// Load training groups for this member
|
||||
await this.loadMemberTrainingGroups(member.id);
|
||||
try {
|
||||
const response = await apiClient.get(`/clubmembers/image/${member.id}`, {
|
||||
responseType: 'blob'
|
||||
@@ -947,8 +1000,66 @@ export default {
|
||||
},
|
||||
resetToNewMember() {
|
||||
this.memberToEdit = null;
|
||||
this.memberTrainingGroups = [];
|
||||
this.selectedGroupToAdd = '';
|
||||
this.resetNewMember();
|
||||
},
|
||||
|
||||
async loadTrainingGroups() {
|
||||
try {
|
||||
const response = await apiClient.get(`/training-groups/${this.currentClub}`);
|
||||
const groups = Array.isArray(response.data) ? response.data : [];
|
||||
this.trainingGroups = groups.map(group => ({
|
||||
...group,
|
||||
members: Array.isArray(group.members) ? group.members : []
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[loadTrainingGroups] Error:', error);
|
||||
this.trainingGroups = [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadMemberTrainingGroups(memberId) {
|
||||
try {
|
||||
const response = await apiClient.get(`/training-groups/${this.currentClub}/member/${memberId}`);
|
||||
this.memberTrainingGroups = Array.isArray(response.data) ? response.data : [];
|
||||
} catch (error) {
|
||||
console.error('[loadMemberTrainingGroups] Error:', error);
|
||||
this.memberTrainingGroups = [];
|
||||
}
|
||||
},
|
||||
|
||||
async addMemberToGroup(groupId) {
|
||||
if (!groupId || !this.memberToEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(`/training-groups/${this.currentClub}/${groupId}/member/${this.memberToEdit.id}`);
|
||||
this.selectedGroupToAdd = '';
|
||||
await this.loadMemberTrainingGroups(this.memberToEdit.id);
|
||||
} catch (error) {
|
||||
console.error('[addMemberToGroup] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Hinzufügen zur Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async removeMemberFromGroup(groupId) {
|
||||
if (!this.memberToEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.delete(`/training-groups/${this.currentClub}/${groupId}/member/${this.memberToEdit.id}`);
|
||||
await this.loadMemberTrainingGroups(this.memberToEdit.id);
|
||||
} catch (error) {
|
||||
console.error('[removeMemberFromGroup] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Entfernen aus der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async loadNotes(member) {
|
||||
this.selectedMember = member;
|
||||
const response = await apiClient.get(`/membernotes/${member.id}`, {
|
||||
@@ -1925,6 +2036,52 @@ table td {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.member-groups-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.member-group-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #e3f2fd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.remove-group-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
color: #d32f2f;
|
||||
padding: 0;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.remove-group-btn:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.group-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.no-groups-hint {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
margin-right: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
|
||||
569
frontend/src/views/TrainingGroupsView.vue
Normal file
569
frontend/src/views/TrainingGroupsView.vue
Normal file
@@ -0,0 +1,569 @@
|
||||
<template>
|
||||
<div class="training-groups-view">
|
||||
<h2>Trainingsgruppen</h2>
|
||||
|
||||
<div class="groups-section">
|
||||
<div class="section-header">
|
||||
<h3>Gruppen</h3>
|
||||
<button @click="showAddGroupForm = true" class="btn-primary">+ Neue Gruppe</button>
|
||||
</div>
|
||||
|
||||
<!-- Add Group Form -->
|
||||
<div v-if="showAddGroupForm" class="add-group-form">
|
||||
<input
|
||||
v-model="newGroupName"
|
||||
type="text"
|
||||
placeholder="Gruppenname"
|
||||
@keyup.enter="createGroup"
|
||||
class="input-field"
|
||||
/>
|
||||
<button @click="createGroup" class="btn-primary">Erstellen</button>
|
||||
<button @click="cancelAddGroup" class="btn-secondary">Abbrechen</button>
|
||||
</div>
|
||||
|
||||
<!-- Groups List -->
|
||||
<div class="groups-list">
|
||||
<div
|
||||
v-for="group in sortedGroups"
|
||||
:key="group.id"
|
||||
class="group-card"
|
||||
:class="{ 'preset-group': group.isPreset }"
|
||||
>
|
||||
<div class="group-header">
|
||||
<h4>{{ group.name }}</h4>
|
||||
<div class="group-actions">
|
||||
<button
|
||||
v-if="!group.isPreset"
|
||||
@click="editGroup(group)"
|
||||
class="btn-icon"
|
||||
title="Bearbeiten"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
v-if="!group.isPreset"
|
||||
@click="deleteGroup(group)"
|
||||
class="btn-icon btn-danger"
|
||||
title="Löschen"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group-members">
|
||||
<div class="members-count">
|
||||
{{ group.members ? group.members.length : 0 }} Mitglieder
|
||||
</div>
|
||||
<div class="members-list">
|
||||
<span
|
||||
v-for="member in (group.members || [])"
|
||||
:key="member.id"
|
||||
class="member-tag"
|
||||
>
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
<button
|
||||
@click="removeMemberFromGroup(group.id, member.id)"
|
||||
class="remove-member-btn"
|
||||
title="Entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Member to Group -->
|
||||
<div class="add-member-section">
|
||||
<select
|
||||
v-model="selectedMembers[group.id]"
|
||||
class="member-select"
|
||||
@change="addMemberToGroup(group.id, $event.target.value)"
|
||||
:disabled="availableMembersForGroup(group.id).length === 0"
|
||||
>
|
||||
<option value="">
|
||||
{{ availableMembersForGroup(group.id).length === 0 ? 'Keine Mitglieder verfügbar' : 'Mitglied hinzufügen...' }}
|
||||
</option>
|
||||
<option
|
||||
v-for="member in availableMembersForGroup(group.id)"
|
||||
:key="member.id"
|
||||
:value="member.id"
|
||||
>
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Group Dialog -->
|
||||
<div v-if="editingGroup" class="edit-group-dialog">
|
||||
<div class="dialog-content">
|
||||
<h3>Gruppe bearbeiten</h3>
|
||||
<input
|
||||
v-model="editingGroup.name"
|
||||
type="text"
|
||||
placeholder="Gruppenname"
|
||||
class="input-field"
|
||||
/>
|
||||
<div class="dialog-actions">
|
||||
<button @click="saveGroupEdit" class="btn-primary">Speichern</button>
|
||||
<button @click="cancelGroupEdit" class="btn-secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info/Confirm Dialogs -->
|
||||
<InfoDialog
|
||||
v-model="infoDialog.isOpen"
|
||||
:title="infoDialog.title"
|
||||
:message="infoDialog.message"
|
||||
:details="infoDialog.details"
|
||||
:type="infoDialog.type"
|
||||
/>
|
||||
<ConfirmDialog
|
||||
v-model="confirmDialog.isOpen"
|
||||
:title="confirmDialog.title"
|
||||
:message="confirmDialog.message"
|
||||
:details="confirmDialog.details"
|
||||
:type="confirmDialog.type"
|
||||
@confirm="confirmDialog.resolveCallback && confirmDialog.resolveCallback(true)"
|
||||
@cancel="confirmDialog.resolveCallback && confirmDialog.resolveCallback(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '../apiClient.js';
|
||||
import { getSafeErrorMessage } from '../utils/errorMessages.js';
|
||||
import InfoDialog from '../components/InfoDialog.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'TrainingGroupsView',
|
||||
components: {
|
||||
InfoDialog,
|
||||
ConfirmDialog,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'token']),
|
||||
|
||||
sortedGroups() {
|
||||
if (!Array.isArray(this.groups)) {
|
||||
return [];
|
||||
}
|
||||
return [...this.groups].sort((a, b) => {
|
||||
// Preset-Gruppen zuerst
|
||||
if (a.isPreset && !b.isPreset) return -1;
|
||||
if (!a.isPreset && b.isPreset) return 1;
|
||||
// Dann nach sortOrder
|
||||
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder;
|
||||
// Dann alphabetisch
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
groups: [],
|
||||
members: [],
|
||||
showAddGroupForm: false,
|
||||
newGroupName: '',
|
||||
editingGroup: null,
|
||||
selectedMembers: {},
|
||||
infoDialog: {
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
type: 'info',
|
||||
},
|
||||
confirmDialog: {
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
type: 'info',
|
||||
resolveCallback: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
if (this.isAuthenticated && this.currentClub) {
|
||||
await this.loadGroups();
|
||||
await this.loadMembers();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentClub() {
|
||||
if (this.isAuthenticated && this.currentClub) {
|
||||
this.loadGroups();
|
||||
this.loadMembers();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async loadGroups() {
|
||||
try {
|
||||
const response = await apiClient.get(`/training-groups/${this.currentClub}`);
|
||||
// Stelle sicher, dass es ein Array ist und dass jedes Gruppen-Objekt ein members-Array hat
|
||||
const groups = Array.isArray(response.data) ? response.data : [];
|
||||
this.groups = groups.map(group => ({
|
||||
...group,
|
||||
members: Array.isArray(group.members) ? group.members : []
|
||||
}));
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingsgruppen');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
this.groups = [];
|
||||
}
|
||||
},
|
||||
|
||||
async loadMembers() {
|
||||
try {
|
||||
const response = await apiClient.get(`/clubmembers/get/${this.currentClub}/false`);
|
||||
const members = Array.isArray(response.data) ? response.data : [];
|
||||
// Die API filtert bereits nach active, also müssen wir nicht nochmal filtern
|
||||
const filteredMembers = members.filter(m => m != null);
|
||||
// Sortiere alphabetisch nach Nachname, dann Vorname
|
||||
this.members = filteredMembers.sort((a, b) => {
|
||||
const lastNameA = (a.lastName || '').toLowerCase();
|
||||
const lastNameB = (b.lastName || '').toLowerCase();
|
||||
if (lastNameA !== lastNameB) {
|
||||
return lastNameA.localeCompare(lastNameB);
|
||||
}
|
||||
const firstNameA = (a.firstName || '').toLowerCase();
|
||||
const firstNameB = (b.firstName || '').toLowerCase();
|
||||
return firstNameA.localeCompare(firstNameB);
|
||||
});
|
||||
console.log('[loadMembers] Loaded', this.members.length, 'active members');
|
||||
} catch (error) {
|
||||
console.error('[loadMembers] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Mitglieder');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
this.members = [];
|
||||
}
|
||||
},
|
||||
|
||||
availableMembersForGroup(groupId) {
|
||||
if (!Array.isArray(this.members) || this.members.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const group = this.groups.find(g => g.id === groupId);
|
||||
if (!group) {
|
||||
return this.members;
|
||||
}
|
||||
|
||||
const groupMembers = Array.isArray(group.members) ? group.members : [];
|
||||
const memberIdsInGroup = new Set(groupMembers.map(m => m && m.id).filter(id => id != null));
|
||||
return this.members.filter(m => m && m.id && !memberIdsInGroup.has(m.id));
|
||||
},
|
||||
|
||||
async createGroup() {
|
||||
if (!this.newGroupName.trim()) {
|
||||
this.showInfo('Hinweis', 'Bitte geben Sie einen Gruppennamen ein.', '', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(`/training-groups/${this.currentClub}`, {
|
||||
name: this.newGroupName.trim(),
|
||||
});
|
||||
this.newGroupName = '';
|
||||
this.showAddGroupForm = false;
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
cancelAddGroup() {
|
||||
this.newGroupName = '';
|
||||
this.showAddGroupForm = false;
|
||||
},
|
||||
|
||||
editGroup(group) {
|
||||
this.editingGroup = { ...group };
|
||||
},
|
||||
|
||||
async saveGroupEdit() {
|
||||
if (!this.editingGroup.name.trim()) {
|
||||
this.showInfo('Hinweis', 'Bitte geben Sie einen Gruppennamen ein.', '', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.put(`/training-groups/${this.currentClub}/${this.editingGroup.id}`, {
|
||||
name: this.editingGroup.name.trim(),
|
||||
sortOrder: this.editingGroup.sortOrder,
|
||||
});
|
||||
this.editingGroup = null;
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
cancelGroupEdit() {
|
||||
this.editingGroup = null;
|
||||
},
|
||||
|
||||
async deleteGroup(group) {
|
||||
const confirmed = await this.showConfirm(
|
||||
'Gruppe löschen',
|
||||
`Möchten Sie die Gruppe "${group.name}" wirklich löschen?`,
|
||||
'Alle Mitglieder-Zuordnungen werden entfernt.',
|
||||
'warning'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
await apiClient.delete(`/training-groups/${this.currentClub}/${group.id}`);
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Gruppe');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async addMemberToGroup(groupId, memberId) {
|
||||
if (!memberId || !groupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.post(`/training-groups/${this.currentClub}/${groupId}/member/${memberId}`);
|
||||
this.selectedMembers[groupId] = '';
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
console.error('[addMemberToGroup] Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Hinzufügen des Mitglieds');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async removeMemberFromGroup(groupId, memberId) {
|
||||
try {
|
||||
await apiClient.delete(`/training-groups/${this.currentClub}/${groupId}/member/${memberId}`);
|
||||
await this.loadGroups();
|
||||
} catch (error) {
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Entfernen des Mitglieds');
|
||||
this.showInfo('Fehler', msg, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
showInfo(title, message, details, type) {
|
||||
this.infoDialog = { isOpen: true, title, message, details, type };
|
||||
},
|
||||
|
||||
showConfirm(title, message, details, type) {
|
||||
return new Promise((resolve) => {
|
||||
this.confirmDialog = {
|
||||
isOpen: true,
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
type,
|
||||
resolveCallback: resolve,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.training-groups-view {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.add-group-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.input-field {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.groups-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.group-card {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.group-card.preset-group {
|
||||
border-color: #4CAF50;
|
||||
background: #f1f8f4;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.group-header h4 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.group-members {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.members-count {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.members-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.member-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #e3f2fd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.remove-member-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
color: #d32f2f;
|
||||
padding: 0;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.remove-member-btn:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.add-member-section {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.member-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.edit-group-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user