Refactor modals in DiaryView, MembersView, OfficialTournaments, ScheduleView, and TrainingStatsView to use dedicated dialog components for improved maintainability and user experience. Update styles and structure for consistency across the application.
This commit is contained in:
221
frontend/src/components/AccidentFormDialog.vue
Normal file
221
frontend/src/components/AccidentFormDialog.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
title="Unfall melden"
|
||||
size="medium"
|
||||
@close="handleClose"
|
||||
>
|
||||
<form @submit.prevent="handleSubmit" class="accident-form">
|
||||
<div class="form-group">
|
||||
<label for="memberId">Mitglied:</label>
|
||||
<select id="memberId" v-model="localAccident.memberId" class="form-select">
|
||||
<option value="">Bitte wählen</option>
|
||||
<option
|
||||
v-for="member in availableMembers"
|
||||
:key="member.id"
|
||||
:value="member.id"
|
||||
>
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="accident">Unfall:</label>
|
||||
<textarea
|
||||
id="accident"
|
||||
v-model="localAccident.accident"
|
||||
required
|
||||
class="form-textarea"
|
||||
placeholder="Beschreibung des Unfalls..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div v-if="accidents.length > 0" class="accidents-list">
|
||||
<h4>Gemeldete Unfälle</h4>
|
||||
<ul>
|
||||
<li v-for="accident in accidents" :key="accident.id" class="accident-item">
|
||||
<strong>{{ accident.firstName }} {{ accident.lastName }}:</strong> {{ accident.accident }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<button type="button" @click="handleClose" class="btn-secondary">Schließen</button>
|
||||
<button type="button" @click="handleSubmit" class="btn-primary" :disabled="!isValid">Eintragen</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseDialog from './BaseDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'AccidentFormDialog',
|
||||
components: {
|
||||
BaseDialog
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
accident: {
|
||||
type: Object,
|
||||
default: () => ({ memberId: '', accident: '' })
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
participants: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
accidents: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close', 'submit', 'update:accident'],
|
||||
data() {
|
||||
return {
|
||||
localAccident: { ...this.accident }
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
availableMembers() {
|
||||
return this.members.filter(m => this.participants.indexOf(m.id) >= 0);
|
||||
},
|
||||
isValid() {
|
||||
return this.localAccident.memberId && this.localAccident.accident && this.localAccident.accident.trim() !== '';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
accident: {
|
||||
handler(newVal) {
|
||||
this.localAccident = { ...newVal };
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
localAccident: {
|
||||
handler(newVal) {
|
||||
this.$emit('update:accident', newVal);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:modelValue', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
handleSubmit() {
|
||||
if (this.isValid) {
|
||||
this.$emit('submit', this.localAccident);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.accident-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px var(--primary-light);
|
||||
}
|
||||
|
||||
.accidents-list {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.accidents-list h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.accidents-list ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.accident-item {
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: var(--background-light);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
padding: 0.5rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -294,7 +294,7 @@ export default {
|
||||
.dialog-header {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
padding: 4px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -353,7 +353,7 @@ export default {
|
||||
|
||||
/* Footer */
|
||||
.dialog-footer {
|
||||
padding: 12px 16px;
|
||||
padding: 4px 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
185
frontend/src/components/CsvImportDialog.vue
Normal file
185
frontend/src/components/CsvImportDialog.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
title="Spielplan importieren"
|
||||
size="small"
|
||||
@close="handleClose"
|
||||
>
|
||||
<form @submit.prevent="handleSubmit" class="import-form">
|
||||
<div class="form-group">
|
||||
<label for="csvFile">CSV-Datei hochladen:</label>
|
||||
<input
|
||||
type="file"
|
||||
id="csvFile"
|
||||
@change="handleFileChange"
|
||||
accept=".csv"
|
||||
required
|
||||
class="file-input"
|
||||
ref="fileInput"
|
||||
/>
|
||||
<div v-if="selectedFile" class="file-info">
|
||||
<span class="file-icon">📄</span>
|
||||
<span class="file-name">{{ selectedFile.name }}</span>
|
||||
<span class="file-size">({{ formatFileSize(selectedFile.size) }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
<button @click="handleClose" class="btn-secondary">Abbrechen</button>
|
||||
<button @click="handleSubmit" class="btn-primary" :disabled="!selectedFile">Importieren</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseDialog from './BaseDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'CsvImportDialog',
|
||||
components: {
|
||||
BaseDialog
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close', 'import'],
|
||||
data() {
|
||||
return {
|
||||
selectedFile: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.selectedFile = null;
|
||||
if (this.$refs.fileInput) {
|
||||
this.$refs.fileInput.value = '';
|
||||
}
|
||||
this.$emit('update:modelValue', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
handleFileChange(event) {
|
||||
this.selectedFile = event.target.files && event.target.files[0] ? event.target.files[0] : null;
|
||||
},
|
||||
handleSubmit() {
|
||||
if (this.selectedFile) {
|
||||
this.$emit('import', this.selectedFile);
|
||||
}
|
||||
},
|
||||
formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue(newVal) {
|
||||
if (!newVal) {
|
||||
// Dialog geschlossen - Reset
|
||||
this.selectedFile = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.import-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.file-input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-input::-webkit-file-upload-button {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.file-input::-webkit-file-upload-button:hover {
|
||||
background: var(--background-light);
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--background-light);
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
padding: 0.5rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
</style>
|
||||
|
||||
249
frontend/src/components/MemberNotesDialog.vue
Normal file
249
frontend/src/components/MemberNotesDialog.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="`Notizen für ${member ? member.firstName + ' ' + member.lastName : ''}`"
|
||||
size="large"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-if="member" class="notes-modal-content">
|
||||
<div class="notes-header-info">
|
||||
Telefon-Nr.: {{ member.phone }}
|
||||
</div>
|
||||
<div class="notes-body">
|
||||
<div class="notes-left">
|
||||
<img v-if="member.imageUrl" :src="member.imageUrl" alt="Mitgliedsbild"
|
||||
class="member-image" />
|
||||
</div>
|
||||
<div class="notes-right">
|
||||
<div class="form-group">
|
||||
<label>Tags</label>
|
||||
<multiselect
|
||||
v-model="localSelectedTags"
|
||||
:options="availableTags"
|
||||
placeholder="Tags auswählen"
|
||||
label="name"
|
||||
track-by="id"
|
||||
multiple
|
||||
:close-on-select="false"
|
||||
@tag="$emit('add-tag', $event)"
|
||||
@remove="$emit('remove-tag', $event)"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Neue Notiz</label>
|
||||
<textarea v-model="localNoteContent" placeholder="Neue Notiz" rows="4" class="note-textarea"></textarea>
|
||||
<button @click="handleAddNote" class="btn-primary">Hinzufügen</button>
|
||||
</div>
|
||||
<div class="notes-list">
|
||||
<h4>Notizen</h4>
|
||||
<ul>
|
||||
<li v-for="note in notes" :key="note.id" class="note-item">
|
||||
<button @click="$emit('delete-note', note.id)" class="trash-btn">🗑️</button>
|
||||
<span class="note-content">{{ note.content }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseDialog from './BaseDialog.vue';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
|
||||
export default {
|
||||
name: 'MemberNotesDialog',
|
||||
components: {
|
||||
BaseDialog,
|
||||
Multiselect
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
member: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
notes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedTags: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
availableTags: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
noteContent: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close', 'add-note', 'delete-note', 'add-tag', 'remove-tag', 'update:noteContent', 'update:selectedTags'],
|
||||
data() {
|
||||
return {
|
||||
localNoteContent: this.noteContent,
|
||||
localSelectedTags: this.selectedTags
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
noteContent(newVal) {
|
||||
this.localNoteContent = newVal;
|
||||
},
|
||||
selectedTags(newVal) {
|
||||
this.localSelectedTags = newVal;
|
||||
},
|
||||
localNoteContent(newVal) {
|
||||
this.$emit('update:noteContent', newVal);
|
||||
},
|
||||
localSelectedTags(newVal) {
|
||||
this.$emit('update:selectedTags', newVal);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:modelValue', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
handleAddNote() {
|
||||
this.$emit('add-note', this.localNoteContent);
|
||||
this.localNoteContent = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notes-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.notes-header-info {
|
||||
padding: 0.5rem;
|
||||
background: var(--background-light);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.notes-body {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.notes-left {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-image {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.notes-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.note-textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.notes-list h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.notes-list ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: var(--background-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.note-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.trash-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0.25rem;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.trash-btn:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
align-self: flex-start;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.notes-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.member-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
260
frontend/src/components/MemberSelectionDialog.vue
Normal file
260
frontend/src/components/MemberSelectionDialog.vue
Normal file
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
title="Mitglieder auswählen"
|
||||
size="large"
|
||||
:close-on-overlay="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="member-selection-content">
|
||||
<div class="controls-bar">
|
||||
<button class="btn-secondary" @click="$emit('select-all')">Alle auswählen</button>
|
||||
<button class="btn-secondary" @click="$emit('deselect-all')">Alle abwählen</button>
|
||||
</div>
|
||||
|
||||
<div class="selection-layout">
|
||||
<div class="members-column">
|
||||
<h4>Mitglieder</h4>
|
||||
<div class="checkbox-list">
|
||||
<label v-for="m in members" :key="m.id" class="checkbox-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="m.id"
|
||||
:checked="selectedIds.includes(m.id)"
|
||||
@change="handleMemberToggle(m.id, $event.target.checked)"
|
||||
/>
|
||||
<span :class="{ active: activeMemberId === m.id }">
|
||||
{{ m.firstName }} {{ m.lastName }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recommendations-column" v-if="activeMember && showRecommendations">
|
||||
<h4>Empfehlungen</h4>
|
||||
<div v-if="recommendations && recommendations.length" class="checkbox-list">
|
||||
<label v-for="rec in recommendations" :key="rec.key" class="checkbox-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isRecommended(rec.key)"
|
||||
@change="handleRecommendationToggle(rec.key, $event.target.checked)"
|
||||
/>
|
||||
<span>{{ rec.name }} — {{ rec.date }} {{ rec.time }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else class="no-data">
|
||||
<em>Keine passenden Empfehlungen gefunden.</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<button class="btn-secondary" @click="handleClose">Schließen</button>
|
||||
<button
|
||||
class="btn-primary"
|
||||
:disabled="selectedIds.length === 0"
|
||||
@click="$emit('generate-pdf')"
|
||||
>
|
||||
PDF erzeugen
|
||||
</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseDialog from './BaseDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'MemberSelectionDialog',
|
||||
components: {
|
||||
BaseDialog
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedIds: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
activeMemberId: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
},
|
||||
recommendations: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
recommendedKeys: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
showRecommendations: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'update:modelValue',
|
||||
'close',
|
||||
'select-all',
|
||||
'deselect-all',
|
||||
'toggle-member',
|
||||
'toggle-recommendation',
|
||||
'generate-pdf',
|
||||
'update:selectedIds',
|
||||
'update:activeMemberId'
|
||||
],
|
||||
computed: {
|
||||
activeMember() {
|
||||
return this.members.find(m => m.id === this.activeMemberId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:modelValue', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
handleMemberToggle(memberId, checked) {
|
||||
this.$emit('update:activeMemberId', memberId);
|
||||
this.$emit('toggle-member', { memberId, checked });
|
||||
},
|
||||
handleRecommendationToggle(key, checked) {
|
||||
this.$emit('toggle-recommendation', { memberId: this.activeMemberId, key, checked });
|
||||
},
|
||||
isRecommended(key) {
|
||||
return this.recommendedKeys.includes(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.member-selection-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.controls-bar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.selection-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.members-column,
|
||||
.recommendations-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.members-column h4,
|
||||
.recommendations-column h4 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.checkbox-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: 500px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.checkbox-item:hover {
|
||||
background: var(--background-light);
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-item span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.checkbox-item span.active {
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.no-data {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
padding: 0.5rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.selection-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.checkbox-list {
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
208
frontend/src/components/QuickAddMemberDialog.vue
Normal file
208
frontend/src/components/QuickAddMemberDialog.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
title="Neues Mitglied hinzufügen"
|
||||
size="medium"
|
||||
:close-on-overlay="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="quick-add-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="firstName">Vorname:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
v-model="localMember.firstName"
|
||||
required
|
||||
class="form-input"
|
||||
placeholder="Vorname"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="lastName">Nachname (optional):</label>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
v-model="localMember.lastName"
|
||||
class="form-input"
|
||||
placeholder="Nachname"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="birthDate">Geburtsdatum:</label>
|
||||
<input
|
||||
type="date"
|
||||
id="birthDate"
|
||||
v-model="localMember.birthDate"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="gender">Geschlecht:</label>
|
||||
<select id="gender" v-model="localMember.gender" class="form-select">
|
||||
<option value="">Bitte wählen</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="diverse">Divers</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<button class="btn-secondary" @click="handleClose">Abbrechen</button>
|
||||
<button
|
||||
class="btn-primary"
|
||||
@click="handleSubmit"
|
||||
:disabled="!isValid"
|
||||
>
|
||||
Erstellen & Hinzufügen
|
||||
</button>
|
||||
</template>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseDialog from './BaseDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'QuickAddMemberDialog',
|
||||
components: {
|
||||
BaseDialog
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
member: {
|
||||
type: Object,
|
||||
default: () => ({ firstName: '', lastName: '', birthDate: '', gender: '' })
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close', 'submit', 'update:member'],
|
||||
data() {
|
||||
return {
|
||||
localMember: { ...this.member }
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isValid() {
|
||||
return this.localMember.firstName && this.localMember.firstName.trim() !== '';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
member: {
|
||||
handler(newVal) {
|
||||
this.localMember = { ...newVal };
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
localMember: {
|
||||
handler(newVal) {
|
||||
this.$emit('update:member', newVal);
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:modelValue', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
handleSubmit() {
|
||||
if (this.isValid) {
|
||||
this.$emit('submit', this.localMember);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quick-add-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px var(--primary-light);
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
padding: 0.5rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
154
frontend/src/components/TagHistoryDialog.vue
Normal file
154
frontend/src/components/TagHistoryDialog.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="`Tag-Historie ${member ? member.firstName + ' ' + member.lastName : ''}`"
|
||||
size="medium"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="tag-history-content">
|
||||
<div class="form-group">
|
||||
<label>Tags auswählen</label>
|
||||
<multiselect
|
||||
v-model="localSelectedTags"
|
||||
:options="activityTags"
|
||||
placeholder="Tags auswählen"
|
||||
label="name"
|
||||
track-by="id"
|
||||
multiple
|
||||
:close-on-select="false"
|
||||
:taggable="false"
|
||||
@select="$emit('select-tag', $event)"
|
||||
:allow-empty="false"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="tagHistory && tagHistory.length" class="tag-history-list">
|
||||
<div v-for="tag in tagHistory" :key="tag.id" class="tag-history-item">
|
||||
<div class="tag-header">{{ tag.name }}</div>
|
||||
<ul class="tag-list">
|
||||
<li v-for="entry in tag.diaryMemberTags" :key="entry.id">
|
||||
{{ formatDate(entry.diaryDates.date) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-history">
|
||||
<em>Keine Tag-Historie vorhanden</em>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseDialog from './BaseDialog.vue';
|
||||
import Multiselect from 'vue-multiselect';
|
||||
|
||||
export default {
|
||||
name: 'TagHistoryDialog',
|
||||
components: {
|
||||
BaseDialog,
|
||||
Multiselect
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
member: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
tagHistory: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedTags: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
activityTags: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close', 'select-tag', 'update:selectedTags'],
|
||||
data() {
|
||||
return {
|
||||
localSelectedTags: this.selectedTags
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
selectedTags(newVal) {
|
||||
this.localSelectedTags = newVal;
|
||||
},
|
||||
localSelectedTags(newVal) {
|
||||
this.$emit('update:selectedTags', newVal);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:modelValue', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tag-history-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.tag-history-list {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.tag-history-item {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--background-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tag-header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
list-style: none;
|
||||
padding-left: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tag-list li {
|
||||
padding: 0.25rem 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.no-history {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
222
frontend/src/components/TrainingDetailsDialog.vue
Normal file
222
frontend/src/components/TrainingDetailsDialog.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<BaseDialog
|
||||
:model-value="modelValue"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
:title="`Trainings-Details: ${member ? member.firstName + ' ' + member.lastName : ''}`"
|
||||
size="large"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-if="member" class="details-modal-content">
|
||||
<div class="member-info">
|
||||
<div class="info-item">
|
||||
<strong>Geburtsdatum:</strong> {{ formatBirthdate(member.birthDate) }}
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<strong>Geburtsjahr:</strong> {{ getBirthYear(member.birthDate) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="participation-summary">
|
||||
<div class="summary-item">
|
||||
<span class="label">Letzte 12 Monate:</span>
|
||||
<span class="value">{{ member.participation12Months }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">Letzte 3 Monate:</span>
|
||||
<span class="value">{{ member.participation3Months }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">Gesamt:</span>
|
||||
<span class="value">{{ member.participationTotal }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="training-details">
|
||||
<h4>Trainingsteilnahmen (absteigend sortiert)</h4>
|
||||
<div class="training-list">
|
||||
<div
|
||||
v-for="training in member.trainingDetails"
|
||||
:key="training.id"
|
||||
class="training-item"
|
||||
>
|
||||
<div class="training-date">{{ formatDate(training.date) }}</div>
|
||||
<div class="training-activity">{{ training.activityName }}</div>
|
||||
<div class="training-time">{{ training.startTime }} - {{ training.endTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!member.trainingDetails || member.trainingDetails.length === 0" class="no-trainings">
|
||||
<em>Keine Trainingsteilnahmen vorhanden</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseDialog from './BaseDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'TrainingDetailsDialog',
|
||||
components: {
|
||||
BaseDialog
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
member: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'close'],
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:modelValue', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
formatBirthdate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE');
|
||||
},
|
||||
getBirthYear(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.getFullYear();
|
||||
},
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.details-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.member-info {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
padding: 1rem;
|
||||
background: var(--background-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.info-item strong {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.participation-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
padding: 1rem;
|
||||
background: var(--background-light);
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-item .label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.summary-item .value {
|
||||
display: block;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.training-details h4 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.training-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.training-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.training-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.training-item:hover {
|
||||
background: var(--background-light);
|
||||
}
|
||||
|
||||
.training-date {
|
||||
font-weight: 600;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.training-activity {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.training-time {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.no-trainings {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.member-info {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.participation-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.training-item {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.training-date {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -289,55 +289,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showTagHistoryModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" @click="closeTagHistoryModal">×</span>
|
||||
<h3>Tag-Historie {{ tagHistoryMember.firstName }} {{ tagHistoryMember.lastName }}</h3>
|
||||
<div>
|
||||
<multiselect v-model="selectedMemberDayTags" :options="selectedActivityTags"
|
||||
placeholder="Tags auswählen" label="name" track-by="id" multiple :close-on-select="false"
|
||||
:taggable="false" @select="addNewTagForDay" :allow-empty="false" />
|
||||
</div>
|
||||
<div v-if="tagHistory">
|
||||
<div v-for="tag in tagHistory" :key="tag.id">
|
||||
<div class="tag-headerplement">{{ tag.name }}</div>
|
||||
<ul class="tag-list">
|
||||
<li v-for="entry in tag.diaryMemberTags" :key="entry.id">{{
|
||||
getFormattedDate(entry.diaryDates.date) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tag History Modal -->
|
||||
<TagHistoryDialog
|
||||
v-model="showTagHistoryModal"
|
||||
:member="tagHistoryMember"
|
||||
:tag-history="tagHistory"
|
||||
v-model:selected-tags="selectedMemberDayTags"
|
||||
:activity-tags="selectedActivityTags"
|
||||
@select-tag="addNewTagForDay"
|
||||
@close="closeTagHistoryModal"
|
||||
/>
|
||||
|
||||
<div v-if="showNotesModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" @click="closeNotesModal">×</span>
|
||||
<h3>Notizen für {{ noteMember.firstName }} {{ noteMember.lastName }}</h3>
|
||||
<div>Telefon-Nr.: {{ noteMember.phone }}</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-left">
|
||||
<img v-if="noteMember.imageUrl" :src="noteMember.imageUrl" alt="Mitgliedsbild"
|
||||
style="width: 250px; height: 250px; object-fit: cover;" />
|
||||
</div>
|
||||
<div class="modal-right">
|
||||
<multiselect v-model="selectedMemberTags" :options="availableTags" placeholder="Tags auswählen"
|
||||
label="name" track-by="id" multiple :close-on-select="false"
|
||||
@tag="addNewTagForMemberFromSelection" @remove="removeMemberDayTag" allow-empty="false" />
|
||||
<div>
|
||||
<textarea v-model="newNoteContent" placeholder="Neue Notiz" rows="4" cols="30"></textarea>
|
||||
<button @click="addMemberNote">Hinzufügen</button>
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="note in notes" :key="note.id">
|
||||
<button @click="deleteNote(note.id)" class="trash-btn">🗑️</button>
|
||||
{{ note.content }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Notizen Modal -->
|
||||
<MemberNotesDialog
|
||||
v-model="showNotesModal"
|
||||
:member="noteMember"
|
||||
:notes="notes"
|
||||
v-model:selected-tags="selectedMemberTags"
|
||||
:available-tags="availableTags"
|
||||
v-model:note-content="newNoteContent"
|
||||
@add-note="addMemberNote"
|
||||
@delete-note="deleteNote"
|
||||
@add-tag="addNewTagForMemberFromSelection"
|
||||
@remove-tag="removeMemberDayTag"
|
||||
@close="closeNotesModal"
|
||||
/>
|
||||
<!-- Image Dialog für normale Bilder -->
|
||||
<ImageDialog
|
||||
v-model="showImage"
|
||||
@@ -355,74 +331,24 @@
|
||||
<CourtDrawingRender :drawing-data="renderModalData" :width="560" :height="360" />
|
||||
</div>
|
||||
</BaseDialog>
|
||||
<div v-if="showAccidentForm" class="accidentForm">
|
||||
<form @submit.prevent="submitAccident">
|
||||
<div>
|
||||
<label for="memberId">Mitglied:</label>
|
||||
<select id="memberId" v-model="accident.memberId">
|
||||
<template v-for="member in members" :key="member.id" :value="member.id">
|
||||
<option v-if="participants.indexOf(member.id) >= 0" :value="member.id">{{ member.firstName +
|
||||
' '
|
||||
+ member.lastName }}</option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="accident">Unfall:</label>
|
||||
<textarea id="accident" v-model="accident.accident" required></textarea>
|
||||
</div>
|
||||
<button type="button" @click="saveAccident">Eintragen</button>
|
||||
<button type="button" @click="closeAccidentForm">Schießen</button>
|
||||
<ul>
|
||||
<li v-for="accident in accidents" :key="accident.id">{{ accident.firstName + ' ' + accident.lastName
|
||||
+
|
||||
': '
|
||||
+ accident.accident}}</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Accident Form Modal -->
|
||||
<AccidentFormDialog
|
||||
v-model="showAccidentForm"
|
||||
v-model:accident="accident"
|
||||
:members="members"
|
||||
:participants="participants"
|
||||
:accidents="accidents"
|
||||
@submit="saveAccident"
|
||||
@close="closeAccidentForm"
|
||||
/>
|
||||
|
||||
<!-- Schnell hinzufügen Dialog -->
|
||||
<div v-if="showQuickAddDialog" class="modal-overlay" @click.self="closeQuickAddDialog">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>Neues Mitglied hinzufügen</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="firstName">Vorname:</label>
|
||||
<input type="text" id="firstName" v-model="newMember.firstName" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="lastName">Nachname (optional):</label>
|
||||
<input type="text" id="lastName" v-model="newMember.lastName" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="birthDate">Geburtsdatum:</label>
|
||||
<input type="date" id="birthDate" v-model="newMember.birthDate" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="gender">Geschlecht:</label>
|
||||
<select id="gender" v-model="newMember.gender">
|
||||
<option value="">Bitte wählen</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="diverse">Divers</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<button class="btn-secondary" @click="closeQuickAddDialog">Abbrechen</button>
|
||||
<button class="btn-primary" @click="createAndAddMember" :disabled="!isNewMemberValid">
|
||||
Erstellen & Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<QuickAddMemberDialog
|
||||
v-model="showQuickAddDialog"
|
||||
v-model:member="newMember"
|
||||
@submit="createAndAddMember"
|
||||
@close="closeQuickAddDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Info Dialog -->
|
||||
@@ -457,6 +383,10 @@ import InfoDialog from '../components/InfoDialog.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import ImageDialog from '../components/ImageDialog.vue';
|
||||
import BaseDialog from '../components/BaseDialog.vue';
|
||||
import MemberNotesDialog from '../components/MemberNotesDialog.vue';
|
||||
import TagHistoryDialog from '../components/TagHistoryDialog.vue';
|
||||
import AccidentFormDialog from '../components/AccidentFormDialog.vue';
|
||||
import QuickAddMemberDialog from '../components/QuickAddMemberDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'DiaryView',
|
||||
@@ -466,7 +396,11 @@ export default {
|
||||
InfoDialog,
|
||||
ConfirmDialog,
|
||||
ImageDialog,
|
||||
BaseDialog
|
||||
BaseDialog,
|
||||
MemberNotesDialog,
|
||||
TagHistoryDialog,
|
||||
AccidentFormDialog,
|
||||
QuickAddMemberDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -2426,4 +2360,99 @@ img {
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.6 !important;
|
||||
}
|
||||
|
||||
/* Notes Modal Styles */
|
||||
.notes-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.notes-header-info {
|
||||
padding: 0.5rem;
|
||||
background: var(--background-light);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.notes-body {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.notes-left {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-image {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.notes-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.note-textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.notes-list h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.notes-list ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: var(--background-light);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.note-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.notes-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.member-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Render Container */
|
||||
.render-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -138,22 +138,18 @@
|
||||
@rotate="handleRotate"
|
||||
/>
|
||||
|
||||
<div v-if="showNotesModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" @click="closeNotesModal">×</span>
|
||||
<h3>Notizen für {{ memberToEdit.firstName }} {{ memberToEdit.lastName }}</h3>
|
||||
<div>
|
||||
<textarea v-model="newNoteContent" placeholder="Neue Notiz" rows="4" cols="30"></textarea>
|
||||
<button @click="addNote">Hinzufügen</button>
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="note in notes" :key="note.id">
|
||||
<button @click="deleteNote(note.id)" class="trash-btn">🗑️</button>
|
||||
{{ note.content }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Notes Modal -->
|
||||
<MemberNotesDialog
|
||||
v-model="showNotesModal"
|
||||
:member="memberToEdit"
|
||||
:notes="notes"
|
||||
v-model:note-content="newNoteContent"
|
||||
:selected-tags="[]"
|
||||
:available-tags="[]"
|
||||
@add-note="addNote"
|
||||
@delete-note="deleteNote"
|
||||
@close="closeNotesModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -186,12 +182,16 @@ import PDFGenerator from '../components/PDFGenerator.js';
|
||||
import InfoDialog from '../components/InfoDialog.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import ImageViewerDialog from '../components/ImageViewerDialog.vue';
|
||||
import BaseDialog from '../components/BaseDialog.vue';
|
||||
import MemberNotesDialog from '../components/MemberNotesDialog.vue';
|
||||
export default {
|
||||
name: 'MembersView',
|
||||
components: {
|
||||
InfoDialog,
|
||||
ConfirmDialog,
|
||||
ImageViewerDialog
|
||||
ImageViewerDialog,
|
||||
BaseDialog,
|
||||
MemberNotesDialog
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub']),
|
||||
@@ -790,4 +790,24 @@ table td {
|
||||
.rotate-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Dialog-spezifische Styles */
|
||||
.member-notes-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -381,44 +381,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showMemberDialog" class="modal-overlay" @click.self="closeMemberDialog">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>Mitglieder auswählen</h3>
|
||||
</div>
|
||||
<div class="modal-controls">
|
||||
<button class="btn-secondary" @click="selectAllMembers">Alle auswählen</button>
|
||||
<button class="btn-secondary" @click="deselectAllMembers">Alle abwählen</button>
|
||||
<div style="flex:1;"></div>
|
||||
<button class="btn-secondary" @click="closeMemberDialog">Schließen</button>
|
||||
<button class="btn-primary" :disabled="!selectedMemberIds.length" @click="generateMembersPdfAndClose">PDF erzeugen</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="dialog-layout">
|
||||
<div class="dialog-col members-col">
|
||||
<div class="checkbox-column">
|
||||
<label v-for="m in activeMembers" :key="m.id" class="check-item">
|
||||
<input type="checkbox" :value="m.id" v-model="selectedMemberIds" @change="selectedMemberIdForDialog = m.id" />
|
||||
<span :class="{ active: selectedMemberIdForDialog === m.id }">{{ m.firstName }} {{ m.lastName }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-col recommendations-col" v-if="selectedMemberInDialog">
|
||||
<h4>Empfehlungen</h4>
|
||||
<div v-if="selectedMemberCompetitions.length">
|
||||
<label v-for="row in selectedMemberCompetitions" :key="row.key" class="check-item">
|
||||
<input type="checkbox" :checked="isRecommended(selectedMemberInDialog.id, row.key)" @change="toggleRecommendation(selectedMemberInDialog.id, row.key)" />
|
||||
<span>{{ row.name }} — {{ row.date }} {{ row.time }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div v-else>
|
||||
<em>Keine passenden Wettbewerbe gefunden.</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Member Selection Dialog -->
|
||||
<MemberSelectionDialog
|
||||
v-model="showMemberDialog"
|
||||
:members="activeMembers"
|
||||
v-model:selected-ids="selectedMemberIds"
|
||||
v-model:active-member-id="selectedMemberIdForDialog"
|
||||
:recommendations="selectedMemberCompetitions"
|
||||
:recommended-keys="getRecommendedKeys()"
|
||||
@select-all="selectAllMembers"
|
||||
@deselect-all="deselectAllMembers"
|
||||
@toggle-member="handleMemberToggle"
|
||||
@toggle-recommendation="handleRecommendationToggle"
|
||||
@generate-pdf="generateMembersPdfAndClose"
|
||||
@close="closeMemberDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -450,8 +427,16 @@ import PDFGenerator from '../components/PDFGenerator.js';
|
||||
|
||||
import InfoDialog from '../components/InfoDialog.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import BaseDialog from '../components/BaseDialog.vue';
|
||||
import MemberSelectionDialog from '../components/MemberSelectionDialog.vue';
|
||||
export default {
|
||||
name: 'OfficialTournaments',
|
||||
components: {
|
||||
InfoDialog,
|
||||
ConfirmDialog,
|
||||
BaseDialog,
|
||||
MemberSelectionDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Dialog States
|
||||
@@ -1032,6 +1017,32 @@ export default {
|
||||
},
|
||||
selectAllMembers() { this.selectedMemberIds = this.activeMembers.map(m => m.id); },
|
||||
deselectAllMembers() { this.selectedMemberIds = []; },
|
||||
handleMemberToggle({ memberId, checked }) {
|
||||
this.selectedMemberIdForDialog = memberId;
|
||||
if (checked && !this.selectedMemberIds.includes(memberId)) {
|
||||
this.selectedMemberIds.push(memberId);
|
||||
} else if (!checked) {
|
||||
this.selectedMemberIds = this.selectedMemberIds.filter(id => id !== memberId);
|
||||
}
|
||||
},
|
||||
handleRecommendationToggle({ memberId, key, checked }) {
|
||||
if (checked) {
|
||||
if (!this.memberRecommendations[memberId]) {
|
||||
this.$set ? this.$set(this.memberRecommendations, memberId, new Set()) : (this.memberRecommendations[memberId] = new Set());
|
||||
}
|
||||
this.memberRecommendations[memberId].add(key);
|
||||
} else {
|
||||
if (this.memberRecommendations[memberId]) {
|
||||
this.memberRecommendations[memberId].delete(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
getRecommendedKeys() {
|
||||
if (!this.selectedMemberIdForDialog || !this.memberRecommendations[this.selectedMemberIdForDialog]) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(this.memberRecommendations[this.selectedMemberIdForDialog]);
|
||||
},
|
||||
splitDateTime(str) {
|
||||
if (!str) return { date: '–', time: '–' };
|
||||
const s = String(str);
|
||||
@@ -1464,6 +1475,25 @@ th, td { border-bottom: 1px solid var(--border-color); padding: 0.5rem; text-ali
|
||||
font-weight: 700;
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
/* Dialog-spezifische Styles */
|
||||
.member-dialog-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.member-dialog-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.member-dialog-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -74,17 +74,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showImportModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" @click="closeImportModal">×</span>
|
||||
<h3>Spielplan importieren</h3>
|
||||
<form @submit.prevent="importCSV">
|
||||
<label for="csvFile">CSV-Datei hochladen:</label>
|
||||
<input type="file" id="csvFile" @change="onFileSelected" accept=".csv" required />
|
||||
<button type="submit">Importieren</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Import Modal -->
|
||||
<CsvImportDialog
|
||||
v-model="showImportModal"
|
||||
@import="handleCsvImport"
|
||||
@close="closeImportModal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -118,14 +113,18 @@ import MatchReportDialog from '../components/MatchReportDialog.vue';
|
||||
|
||||
import InfoDialog from '../components/InfoDialog.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import BaseDialog from '../components/BaseDialog.vue';
|
||||
import CsvImportDialog from '../components/CsvImportDialog.vue';
|
||||
export default {
|
||||
name: 'ScheduleView',
|
||||
components: {
|
||||
SeasonSelector,
|
||||
MatchReportDialog
|
||||
,
|
||||
MatchReportDialog,
|
||||
InfoDialog,
|
||||
ConfirmDialog},
|
||||
ConfirmDialog,
|
||||
BaseDialog,
|
||||
CsvImportDialog
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub', 'clubs', 'currentClubName']),
|
||||
},
|
||||
@@ -198,8 +197,9 @@ export default {
|
||||
this.showImportModal = false;
|
||||
this.selectedFile = null;
|
||||
},
|
||||
onFileSelected(event) {
|
||||
this.selectedFile = event.target.files[0];
|
||||
handleCsvImport(file) {
|
||||
this.selectedFile = file;
|
||||
this.importCSV();
|
||||
},
|
||||
async importCSV() {
|
||||
if (!this.selectedFile) return;
|
||||
|
||||
@@ -113,52 +113,25 @@
|
||||
</div>
|
||||
|
||||
<!-- Modal für Mitgliedsdetails -->
|
||||
<div v-if="showDetailsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" @click="closeDetailsModal">×</span>
|
||||
<h3>Trainings-Details: {{ selectedMember.firstName }} {{ selectedMember.lastName }}</h3>
|
||||
|
||||
<div class="member-info">
|
||||
<p><strong>Geburtsdatum:</strong> {{ formatBirthdate(selectedMember.birthDate) }}</p>
|
||||
<p><strong>Geburtsjahr:</strong> {{ getBirthYear(selectedMember.birthDate) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="participation-summary">
|
||||
<div class="summary-item">
|
||||
<span class="label">Letzte 12 Monate:</span>
|
||||
<span class="value">{{ selectedMember.participation12Months }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">Letzte 3 Monate:</span>
|
||||
<span class="value">{{ selectedMember.participation3Months }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">Gesamt:</span>
|
||||
<span class="value">{{ selectedMember.participationTotal }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="training-details">
|
||||
<h4>Trainingsteilnahmen (absteigend sortiert)</h4>
|
||||
<div class="training-list">
|
||||
<div v-for="training in selectedMember.trainingDetails" :key="training.id" class="training-item">
|
||||
<div class="training-date">{{ formatDate(training.date) }}</div>
|
||||
<div class="training-activity">{{ training.activityName }}</div>
|
||||
<div class="training-time">{{ training.startTime }} - {{ training.endTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Training Details Modal -->
|
||||
<TrainingDetailsDialog
|
||||
v-model="showDetailsModal"
|
||||
:member="selectedMember"
|
||||
@close="closeDetailsModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '../apiClient';
|
||||
import TrainingDetailsDialog from '../components/TrainingDetailsDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'TrainingStatsView',
|
||||
components: {
|
||||
TrainingDetailsDialog
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isAuthenticated', 'currentClub']),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user