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:
318
frontend/src/components/MemberActivitiesDialog.vue
Normal file
318
frontend/src/components/MemberActivitiesDialog.vue
Normal 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user