Add member activity routes and UI enhancements in MembersView

Integrate member activity management by adding new routes in the backend for member activities. Update MembersView.vue to include a button for opening the activities modal and implement the MemberActivitiesDialog component for displaying member activities. Enhance the UI with new button styles for better user interaction.
This commit is contained in:
Torsten Schulz (local)
2025-10-16 22:36:49 +02:00
parent 01bbb85485
commit c74217f6d8
5 changed files with 487 additions and 2 deletions

View File

@@ -0,0 +1,318 @@
<template>
<BaseDialog
v-model="isOpen"
:title="`Übungen von ${memberName}`"
@close="closeDialog"
:width="800"
>
<div class="activities-content">
<!-- Period Selection -->
<div class="period-selector">
<label>Zeitraum:</label>
<select v-model="selectedPeriod" @change="loadActivities">
<option value="month">Letzte 4 Wochen</option>
<option value="3months">Letzte 3 Monate</option>
<option value="6months">Letztes halbes Jahr</option>
<option value="year">Letztes Jahr</option>
<option value="all">Alle</option>
</select>
</div>
<!-- Loading State -->
<div v-if="loading" class="loading">
Lade Übungen...
</div>
<!-- No Activities -->
<div v-else-if="activities.length === 0" class="no-activities">
Keine Übungen im gewählten Zeitraum gefunden.
</div>
<!-- Activities List -->
<div v-else class="activities-list">
<table class="activities-table">
<thead>
<tr>
<th>Übung</th>
<th>Häufigkeit</th>
<th>Daten</th>
</tr>
</thead>
<tbody>
<tr v-for="activity in activities" :key="activity.name">
<td class="activity-name">{{ activity.name }}</td>
<td class="activity-count">{{ activity.count }}x</td>
<td class="activity-dates">
<div class="dates-container">
<span
v-for="(date, index) in activity.dates.slice(0, showAllDates[activity.name] ? undefined : 5)"
:key="index"
class="date-badge"
>
{{ formatDate(date) }}
</span>
<button
v-if="activity.dates.length > 5 && !showAllDates[activity.name]"
@click="toggleShowAllDates(activity.name)"
class="show-more-btn"
>
+{{ activity.dates.length - 5 }} weitere
</button>
<button
v-if="activity.dates.length > 5 && showAllDates[activity.name]"
@click="toggleShowAllDates(activity.name)"
class="show-less-btn"
>
weniger anzeigen
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<template #footer>
<button @click="closeDialog" class="btn-close">Schließen</button>
</template>
</BaseDialog>
</template>
<script>
import { ref, computed, watch } from 'vue';
import BaseDialog from './BaseDialog.vue';
import apiClient from '../apiClient.js';
export default {
name: 'MemberActivitiesDialog',
components: {
BaseDialog
},
props: {
modelValue: {
type: Boolean,
default: false
},
member: {
type: Object,
default: null
},
clubId: {
type: [String, Number],
required: true
}
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const isOpen = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
});
const memberName = computed(() => {
if (!props.member) return '';
return `${props.member.firstName} ${props.member.lastName}`;
});
const selectedPeriod = ref('6months');
const loading = ref(false);
const activities = ref([]);
const showAllDates = ref({});
const loadActivities = async () => {
if (!props.member) return;
loading.value = true;
try {
const response = await apiClient.get(
`/member-activities/${props.clubId}/${props.member.id}`,
{
params: {
period: selectedPeriod.value
}
}
);
activities.value = response.data;
// Reset showAllDates
showAllDates.value = {};
} catch (error) {
console.error('Error loading member activities:', error);
activities.value = [];
} finally {
loading.value = false;
}
};
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}.${month}.${year}`;
};
const toggleShowAllDates = (activityName) => {
showAllDates.value[activityName] = !showAllDates.value[activityName];
};
const closeDialog = () => {
isOpen.value = false;
};
// Watch for dialog open to load activities
watch(() => props.modelValue, (newValue) => {
if (newValue && props.member) {
loadActivities();
}
});
return {
isOpen,
memberName,
selectedPeriod,
loading,
activities,
showAllDates,
loadActivities,
formatDate,
toggleShowAllDates,
closeDialog
};
}
};
</script>
<style scoped>
.activities-content {
padding: 1rem;
}
.period-selector {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.period-selector label {
font-weight: 600;
color: var(--text-color, #2c3e50);
}
.period-selector select {
padding: 0.5rem 1rem;
border: 1px solid var(--border-color, #ddd);
border-radius: 4px;
font-size: 1rem;
background: white;
cursor: pointer;
}
.loading {
text-align: center;
padding: 2rem;
color: var(--text-secondary, #666);
font-style: italic;
}
.no-activities {
text-align: center;
padding: 2rem;
color: var(--text-secondary, #666);
font-style: italic;
}
.activities-table {
width: 100%;
border-collapse: collapse;
}
.activities-table thead th {
background-color: var(--header-bg, #f5f5f5);
padding: 0.75rem;
text-align: left;
font-weight: 600;
border-bottom: 2px solid var(--border-color, #ddd);
}
.activities-table tbody tr {
border-bottom: 1px solid var(--border-color, #eee);
}
.activities-table tbody tr:hover {
background-color: var(--row-hover, #f9f9f9);
}
.activities-table td {
padding: 0.75rem;
vertical-align: top;
}
.activity-name {
font-weight: 500;
color: var(--primary-color, #2c3e50);
}
.activity-count {
font-weight: 600;
color: var(--accent-color, #3498db);
text-align: center;
min-width: 80px;
}
.activity-dates {
max-width: 450px;
}
.dates-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.date-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background-color: var(--badge-bg, #e8f4f8);
color: var(--badge-text, #2980b9);
border-radius: 4px;
font-size: 0.875rem;
white-space: nowrap;
}
.show-more-btn,
.show-less-btn {
padding: 0.25rem 0.5rem;
background-color: transparent;
color: var(--link-color, #3498db);
border: 1px solid var(--link-color, #3498db);
border-radius: 4px;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
}
.show-more-btn:hover,
.show-less-btn:hover {
background-color: var(--link-color, #3498db);
color: white;
}
.btn-close {
padding: 0.5rem 1.5rem;
background-color: var(--secondary-color, #95a5a6);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.2s;
}
.btn-close:hover {
background-color: var(--secondary-color-dark, #7f8c8d);
}
</style>

View File

@@ -120,6 +120,7 @@
<td>{{ member.email }}</td>
<td>
<button @click.stop="openNotesModal(member)">Notizen</button>
<button @click.stop="openActivitiesModal(member)" class="btn-activities">Übungen</button>
</td>
</tr>
@@ -143,6 +144,13 @@
@confirm="handleConfirmResult(true)"
@cancel="handleConfirmResult(false)"
/>
<!-- Member Activities Dialog -->
<MemberActivitiesDialog
v-model="showActivitiesModal"
:member="selectedMemberForActivities"
:club-id="currentClub"
/>
</template>
</tbody>
</table>
@@ -205,6 +213,7 @@ import ConfirmDialog from '../components/ConfirmDialog.vue';
import ImageViewerDialog from '../components/ImageViewerDialog.vue';
import BaseDialog from '../components/BaseDialog.vue';
import MemberNotesDialog from '../components/MemberNotesDialog.vue';
import MemberActivitiesDialog from '../components/MemberActivitiesDialog.vue';
export default {
name: 'MembersView',
components: {
@@ -212,7 +221,8 @@ export default {
ConfirmDialog,
ImageViewerDialog,
BaseDialog,
MemberNotesDialog
MemberNotesDialog,
MemberActivitiesDialog
},
computed: {
...mapGetters(['isAuthenticated', 'currentClub']),
@@ -271,7 +281,9 @@ export default {
showInactiveMembers: false,
newPicsInInternetAllowed: false,
isUpdatingRatings: false,
showMemberInfo: false
showMemberInfo: false,
showActivitiesModal: false,
selectedMemberForActivities: null
}
},
async mounted() {
@@ -483,6 +495,10 @@ export default {
closeNotesModal() {
this.showNotesModal = false;
},
openActivitiesModal(member) {
this.selectedMemberForActivities = member;
this.showActivitiesModal = true;
},
openImageModal(imageUrl, memberId) {
this.selectedImageUrl = imageUrl;
this.selectedMemberId = memberId;
@@ -915,4 +931,21 @@ table td {
min-width: 2rem;
text-align: center;
}
/* Button Styles */
.btn-activities {
margin-left: 0.5rem;
background-color: #28a745;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s ease;
}
.btn-activities:hover {
background-color: #218838;
}
</style>